learning_ai_notes/web/src/components/IntakeUrlBar.tsx

135 lines
4.0 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button, Card } from "@/components/ui/Primitives";
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-danger)",
article: "var(--nl-accent-primary)",
pdf: "var(--nl-warning)",
tweet: "var(--nl-accent-secondary)",
reddit: "var(--nl-warning)",
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 (
<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-default)",
borderRadius: "var(--nl-radius-md)",
background: "var(--nl-input-bg)",
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"
disabled={loading || !url.trim() || !isValidUrl(url)}
loading={loading}
onClick={() => void handleSubmit()}
style={{ whiteSpace: "nowrap" }}
aria-label="Process URL"
>
Process
</Button>
</Card>
);
}