135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { submitIntake, type IntakeContentType } from "@/lib/intake-client";
|
|
import { toast } from "@/lib/toast";
|
|
|
|
const CONTENT_TYPE_LABELS: Record<IntakeContentType, string> = {
|
|
youtube: "YouTube",
|
|
article: "Article",
|
|
pdf: "PDF",
|
|
tweet: "Tweet",
|
|
reddit: "Reddit",
|
|
github: "GitHub",
|
|
generic: "Link",
|
|
};
|
|
|
|
const CONTENT_TYPE_COLORS: Record<IntakeContentType, string> = {
|
|
youtube: "var(--nl-red)",
|
|
article: "var(--nl-primary)",
|
|
pdf: "var(--nl-orange)",
|
|
tweet: "var(--nl-blue)",
|
|
reddit: "var(--nl-orange)",
|
|
github: "var(--nl-text-primary)",
|
|
generic: "var(--nl-text-secondary)",
|
|
};
|
|
|
|
function classifyUrlClient(url: string): IntakeContentType {
|
|
if (/youtube\.com\/watch|youtu\.be\/|youtube\.com\/shorts/i.test(url)) return "youtube";
|
|
if (/(?:twitter|x)\.com\/.*\/status/i.test(url)) return "tweet";
|
|
if (/github\.com\/[^/]+\/[^/]+/i.test(url)) return "github";
|
|
if (/reddit\.com\/r\//i.test(url)) return "reddit";
|
|
if (/\.pdf(\?.*)?$/i.test(url)) return "pdf";
|
|
return "article";
|
|
}
|
|
|
|
function isValidUrl(s: string): boolean {
|
|
try {
|
|
new URL(s);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
interface IntakeUrlBarProps {
|
|
workspaceId?: string;
|
|
onIntakeSubmitted?: (noteId: string, jobId: string) => void;
|
|
}
|
|
|
|
export function IntakeUrlBar({ workspaceId, onIntakeSubmitted }: IntakeUrlBarProps) {
|
|
const [url, setUrl] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const detectedType = url && isValidUrl(url) ? classifyUrlClient(url) : null;
|
|
|
|
async function handleSubmit() {
|
|
if (!url.trim() || !isValidUrl(url)) {
|
|
toast.error("Please enter a valid URL");
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const result = await submitIntake(url.trim(), workspaceId);
|
|
toast.success(`Processing ${CONTENT_TYPE_LABELS[result.contentType]} — note will be ready soon`);
|
|
setUrl("");
|
|
onIntakeSubmitted?.(result.noteId, result.jobId);
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "Failed to submit URL");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="surface-card"
|
|
style={{
|
|
padding: "var(--nl-space-4)",
|
|
display: "flex",
|
|
gap: "var(--nl-space-3)",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<div style={{ flex: 1, position: "relative" }}>
|
|
<input
|
|
type="url"
|
|
value={url}
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
onKeyDown={(e) => { if (e.key === "Enter") void handleSubmit(); }}
|
|
placeholder="Paste a URL to auto-process (YouTube, article, tweet, PDF, GitHub…)"
|
|
aria-label="URL to process"
|
|
style={{
|
|
width: "100%",
|
|
padding: "var(--nl-space-3) var(--nl-space-4)",
|
|
paddingRight: detectedType ? "5rem" : "var(--nl-space-4)",
|
|
border: "1px solid var(--nl-border)",
|
|
borderRadius: "var(--nl-radius-md)",
|
|
background: "var(--nl-surface)",
|
|
color: "var(--nl-text-primary)",
|
|
fontSize: "var(--nl-fs-base)",
|
|
}}
|
|
/>
|
|
{detectedType && (
|
|
<span
|
|
style={{
|
|
position: "absolute",
|
|
right: "var(--nl-space-3)",
|
|
top: "50%",
|
|
transform: "translateY(-50%)",
|
|
padding: "2px 8px",
|
|
borderRadius: "var(--nl-radius-sm)",
|
|
fontSize: "var(--nl-fs-xs)",
|
|
fontWeight: 600,
|
|
background: CONTENT_TYPE_COLORS[detectedType],
|
|
color: "white",
|
|
opacity: 0.9,
|
|
}}
|
|
>
|
|
{CONTENT_TYPE_LABELS[detectedType]}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
disabled={loading || !url.trim() || !isValidUrl(url)}
|
|
onClick={() => void handleSubmit()}
|
|
style={{ whiteSpace: "nowrap" }}
|
|
aria-label="Process URL"
|
|
>
|
|
{loading ? "Processing…" : "Process"}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|