agentic-rag / frontend /src /components /QueryInterface.tsx
vksepm
updated ui
0dcdb24
Raw
History Blame Contribute Delete
7.09 kB
import React, { useEffect, useRef, useState } from "react";
import { submitQuery } from "../api/client";
import type { QueryResponse } from "../types";
interface Props {
onResponse: (r: QueryResponse) => void;
onPending?: (query: string) => void;
onSettled?: () => void;
initialQuery?: string;
vizEnabled?: boolean;
onVizToggle?: (v: boolean) => void;
}
const EXAMPLE_QUERIES = [
"What are the key differences between BERT and GPT architectures?",
"Compare LoRA vs full fine-tuning on common benchmarks",
"How does RAG improve factual accuracy?",
"Effect of model scale on few-shot learning?",
];
export default function QueryInterface({
onResponse,
onPending,
onSettled,
initialQuery = "",
vizEnabled = false,
onVizToggle,
}: Props) {
const [query, setQuery] = useState(initialQuery);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (initialQuery) setQuery(initialQuery);
}, [initialQuery]);
// Auto-grow textarea
useEffect(() => {
const el = textareaRef.current;
if (!el) return;
el.style.height = "auto";
el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
}, [query]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const trimmed = query.trim();
if (!trimmed) return;
setError("");
setLoading(true);
onPending?.(trimmed);
try {
const result = await submitQuery(trimmed, vizEnabled);
setQuery("");
onResponse(result);
} catch (err: unknown) {
const msg =
(err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ??
"Query failed — please try again.";
setError(String(msg));
onSettled?.();
} finally {
setLoading(false);
}
}
return (
<div className="space-y-3">
<form onSubmit={handleSubmit}>
{/* Textarea */}
<textarea
ref={textareaRef}
rows={2}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as unknown as React.FormEvent);
}
}}
disabled={loading}
placeholder="Ask a question about AI / ML research papers…"
aria-label="Research query"
className="w-full border border-slate-200 rounded-xl px-4 py-3 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-brand-400 focus:border-transparent placeholder:text-slate-400 disabled:opacity-60 bg-slate-50 focus:bg-white transition-colors leading-relaxed"
/>
{/* Controls row */}
<div className="flex items-center gap-2 mt-2.5 flex-wrap">
{/* Example chips */}
<div className="flex items-center gap-1.5 flex-1 min-w-0 overflow-x-auto pb-0.5 hide-scrollbar">
{EXAMPLE_QUERIES.map((q) => (
<button
key={q}
type="button"
onClick={() => {
setQuery(q);
textareaRef.current?.focus();
}}
disabled={loading}
className="shrink-0 text-xs bg-slate-100 hover:bg-brand-50 hover:text-brand-700 text-slate-500 border border-transparent hover:border-brand-200 px-2.5 py-1 rounded-lg transition-colors disabled:opacity-50 whitespace-nowrap"
>
{q.length > 38 ? q.slice(0, 38) + "…" : q}
</button>
))}
</div>
{/* Viz toggle */}
{onVizToggle && (
<button
type="button"
onClick={() => onVizToggle(!vizEnabled)}
disabled={loading}
aria-pressed={vizEnabled}
title={vizEnabled ? "Disable visualization" : "Enable visualization"}
className={`shrink-0 flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border transition-colors disabled:opacity-50 font-medium ${
vizEnabled
? "bg-violet-50 border-violet-200 text-violet-700"
: "bg-slate-50 border-slate-200 text-slate-500 hover:border-slate-300 hover:text-slate-700"
}`}
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
</svg>
Viz
{/* Dot indicator */}
<span className={`w-1.5 h-1.5 rounded-full transition-colors ${vizEnabled ? "bg-violet-500" : "bg-slate-300"}`} />
</button>
)}
{/* Submit */}
<button
type="submit"
disabled={loading || !query.trim()}
aria-label="Submit query"
className="shrink-0 bg-brand-600 hover:bg-brand-700 active:bg-brand-800 text-white text-sm font-medium px-4 py-1.5 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 shadow-sm"
>
{loading ? (
<>
<svg className="animate-spin h-3.5 w-3.5" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
</svg>
Thinking…
</>
) : (
<>
Ask
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg>
</>
)}
</button>
</div>
</form>
{/* Error banner */}
{error && (
<div className="flex items-start gap-2 text-red-600 text-sm bg-red-50 border border-red-100 rounded-xl px-3 py-2.5" role="alert">
<svg className="w-4 h-4 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<span>{error}</span>
</div>
)}
</div>
);
}