feat(frontend): build analysis form flow

This commit is contained in:
KIMATHI EMMANUEL 2026-03-23 05:04:43 +00:00
parent 1cc2a3edde
commit 41e645392c
4 changed files with 1918 additions and 109 deletions

1734
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,7 @@ function todayIso(): string {
export default function App() { export default function App() {
const [options, setOptions] = useState<OptionsResponse | null>(null); const [options, setOptions] = useState<OptionsResponse | null>(null);
const [loadingOptions, setLoadingOptions] = useState(true); const [loadingOptions, setLoadingOptions] = useState(true);
const [optionsError, setOptionsError] = useState<string | null>(null);
const [ticker, setTicker] = useState("SPY"); const [ticker, setTicker] = useState("SPY");
const [analysisDate, setAnalysisDate] = useState(todayIso()); const [analysisDate, setAnalysisDate] = useState(todayIso());
const [researchDepth, setResearchDepth] = useState(1); const [researchDepth, setResearchDepth] = useState(1);
@ -65,8 +66,11 @@ export default function App() {
const [quickThink, setQuickThink] = useState(""); const [quickThink, setQuickThink] = useState("");
const [deepThink, setDeepThink] = useState(""); const [deepThink, setDeepThink] = useState("");
const [googleThinkingLevel, setGoogleThinkingLevel] = useState("high"); const [googleThinkingLevel, setGoogleThinkingLevel] = useState("high");
const [openaiReasoningEffort, setOpenaiReasoningEffort] = useState("medium");
const [backendUrlOverride, setBackendUrlOverride] = useState("");
const [job, setJob] = useState<AnalysisJob | null>(null); const [job, setJob] = useState<AnalysisJob | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const providers = options?.providers ?? []; const providers = options?.providers ?? [];
const providerMeta = useMemo( const providerMeta = useMemo(
@ -78,7 +82,11 @@ export default function App() {
let mounted = true; let mounted = true;
(async () => { (async () => {
try { try {
setOptionsError(null);
const response = await fetch(`${API_BASE}/api/options`); const response = await fetch(`${API_BASE}/api/options`);
if (!response.ok) {
throw new Error(`Failed to load options (${response.status})`);
}
const data: OptionsResponse = await response.json(); const data: OptionsResponse = await response.json();
if (!mounted) { if (!mounted) {
return; return;
@ -89,6 +97,11 @@ export default function App() {
setQuickThink(google.models[0]); setQuickThink(google.models[0]);
setDeepThink(google.models[0]); setDeepThink(google.models[0]);
} }
} catch (error) {
if (mounted) {
const message = error instanceof Error ? error.message : "Failed to load options";
setOptionsError(message);
}
} finally { } finally {
if (mounted) { if (mounted) {
setLoadingOptions(false); setLoadingOptions(false);
@ -116,14 +129,42 @@ export default function App() {
if (!job || (job.status !== "queued" && job.status !== "running")) { if (!job || (job.status !== "queued" && job.status !== "running")) {
return; return;
} }
const timer = setTimeout(async () => { const timer = setInterval(async () => {
const response = await fetch(`${API_BASE}/api/analysis/jobs/${job.id}`); try {
const updated = (await response.json()) as AnalysisJob; const response = await fetch(`${API_BASE}/api/analysis/jobs/${job.id}`);
setJob(updated); if (!response.ok) {
throw new Error(`Polling failed (${response.status})`);
}
const updated = (await response.json()) as AnalysisJob;
setJob(updated);
} catch (error) {
const message = error instanceof Error ? error.message : "Polling failed";
setJob((current) => {
if (!current) {
return current;
}
return {
...current,
status: "failed",
error: message,
completed_at: new Date().toISOString(),
};
});
}
}, 2500); }, 2500);
return () => clearTimeout(timer); return () => clearInterval(timer);
}, [job]); }, [job]);
const formValid = useMemo(() => {
const normalizedTicker = ticker.trim();
return (
normalizedTicker.length > 0 &&
analysisDate.length === 10 &&
selectedAnalysts.length > 0 &&
provider.length > 0
);
}, [ticker, analysisDate, selectedAnalysts, provider]);
const toggleAnalyst = (value: string) => { const toggleAnalyst = (value: string) => {
setSelectedAnalysts((current) => { setSelectedAnalysts((current) => {
if (current.includes(value)) { if (current.includes(value)) {
@ -134,20 +175,28 @@ export default function App() {
}); });
}; };
const submitJob = async () => { const submitJob = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!formValid) {
setSubmitError("Complete ticker, date, and analyst selection before submitting.");
return;
}
setIsSubmitting(true); setIsSubmitting(true);
setSubmitError(null);
setJob(null); setJob(null);
try { try {
const payload = { const payload = {
ticker, ticker: ticker.trim().toUpperCase(),
analysis_date: analysisDate, analysis_date: analysisDate,
analysts: selectedAnalysts, analysts: selectedAnalysts,
research_depth: researchDepth, research_depth: researchDepth,
llm_provider: provider, llm_provider: provider,
backend_url: providerMeta?.base_url, backend_url: backendUrlOverride.trim() || providerMeta?.base_url,
quick_think_llm: quickThink, quick_think_llm: quickThink,
deep_think_llm: deepThink, deep_think_llm: deepThink,
google_thinking_level: provider === "google" ? googleThinkingLevel : null, google_thinking_level: provider === "google" ? googleThinkingLevel : null,
openai_reasoning_effort:
provider === "openai" ? openaiReasoningEffort : null,
}; };
const response = await fetch(`${API_BASE}/api/analysis/jobs`, { const response = await fetch(`${API_BASE}/api/analysis/jobs`, {
method: "POST", method: "POST",
@ -161,19 +210,7 @@ export default function App() {
setJob(created); setJob(created);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
setJob({ setSubmitError(message);
id: "n/a",
status: "failed",
created_at: new Date().toISOString(),
request: {
ticker,
analysis_date: analysisDate,
analysts: selectedAnalysts,
research_depth: researchDepth,
llm_provider: provider,
},
error: message,
});
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -195,102 +232,138 @@ export default function App() {
<section className="grid"> <section className="grid">
<aside className="panel stack"> <aside className="panel stack">
<h2>Run Setup</h2> <h2>Run Setup</h2>
{optionsError && <div className="card error">{optionsError}</div>}
<label> <form className="stack" onSubmit={submitJob}>
Ticker
<input value={ticker} onChange={(e) => setTicker(e.target.value.toUpperCase())} />
</label>
<label>
Analysis Date
<input
type="date"
value={analysisDate}
onChange={(e) => setAnalysisDate(e.target.value)}
/>
</label>
<label>
Provider
<select value={provider} onChange={(e) => setProvider(e.target.value)}>
{providers.map((item) => (
<option value={item.id} key={item.id}>
{item.id}
</option>
))}
</select>
</label>
<label>
Quick-Thinking Model
<select value={quickThink} onChange={(e) => setQuickThink(e.target.value)}>
{providerMeta?.models?.map((model) => (
<option value={model} key={model}>
{model}
</option>
))}
</select>
</label>
<label>
Deep-Thinking Model
<select value={deepThink} onChange={(e) => setDeepThink(e.target.value)}>
{providerMeta?.models?.map((model) => (
<option value={model} key={model}>
{model}
</option>
))}
</select>
</label>
{provider === "google" && (
<label> <label>
Gemini Thinking Mode Ticker
<select <input
value={googleThinkingLevel} value={ticker}
onChange={(e) => setGoogleThinkingLevel(e.target.value)} onChange={(e) => setTicker(e.target.value.toUpperCase())}
> placeholder="e.g. NVDA"
<option value="high">high</option> />
<option value="minimal">minimal</option> </label>
<label>
Analysis Date
<input
type="date"
value={analysisDate}
onChange={(e) => setAnalysisDate(e.target.value)}
/>
</label>
<label>
Provider
<select value={provider} onChange={(e) => setProvider(e.target.value)}>
{providers.map((item) => (
<option value={item.id} key={item.id}>
{item.id}
</option>
))}
</select> </select>
</label> </label>
)}
<label> <label>
Research Depth Provider Base URL Override (optional)
<div className="depth"> <input
{[1, 3, 5].map((depth) => ( value={backendUrlOverride}
<button onChange={(e) => setBackendUrlOverride(e.target.value)}
key={depth} placeholder={providerMeta?.base_url || "https://..."}
type="button" />
className={researchDepth === depth ? "active" : ""} </label>
onClick={() => setResearchDepth(depth)}
<label>
Quick-Thinking Model
<select value={quickThink} onChange={(e) => setQuickThink(e.target.value)}>
{providerMeta?.models?.map((model) => (
<option value={model} key={model}>
{model}
</option>
))}
</select>
</label>
<label>
Deep-Thinking Model
<select value={deepThink} onChange={(e) => setDeepThink(e.target.value)}>
{providerMeta?.models?.map((model) => (
<option value={model} key={model}>
{model}
</option>
))}
</select>
</label>
{provider === "google" && (
<label>
Gemini Thinking Mode
<select
value={googleThinkingLevel}
onChange={(e) => setGoogleThinkingLevel(e.target.value)}
> >
{depth === 1 ? "Shallow" : depth === 3 ? "Medium" : "Deep"} <option value="high">high</option>
</button> <option value="minimal">minimal</option>
))} </select>
</div> </label>
</label> )}
<div className="stack"> {provider === "openai" && (
<span className="mono">Analyst Team</span> <label>
<div className="checks"> OpenAI Reasoning Effort
{(options?.analysts || []).map((analyst) => ( <select
<label key={analyst}> value={openaiReasoningEffort}
<input onChange={(e) => setOpenaiReasoningEffort(e.target.value)}
type="checkbox" >
checked={selectedAnalysts.includes(analyst)} <option value="low">low</option>
onChange={() => toggleAnalyst(analyst)} <option value="medium">medium</option>
/> <option value="high">high</option>
{analyst} </select>
</label> </label>
))} )}
</div>
</div>
<button className="run" type="button" onClick={submitJob} disabled={isSubmitting || loadingOptions}> <label>
{isSubmitting ? "Submitting..." : "Start Analysis Job"} Research Depth
</button> <div className="depth">
{[1, 3, 5].map((depth) => (
<button
key={depth}
type="button"
className={researchDepth === depth ? "active" : ""}
onClick={() => setResearchDepth(depth)}
>
{depth === 1 ? "Shallow" : depth === 3 ? "Medium" : "Deep"}
</button>
))}
</div>
</label>
<div className="stack">
<span className="mono">Analyst Team</span>
<div className="checks">
{(options?.analysts || []).map((analyst) => (
<label key={analyst}>
<input
type="checkbox"
checked={selectedAnalysts.includes(analyst)}
onChange={() => toggleAnalyst(analyst)}
/>
{analyst}
</label>
))}
</div>
</div>
{submitError && <div className="card error">{submitError}</div>}
<button
className="run"
type="submit"
disabled={isSubmitting || loadingOptions || !formValid}
>
{isSubmitting ? "Submitting..." : "Start Analysis Job"}
</button>
</form>
</aside> </aside>
<article className="panel"> <article className="panel">

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts"],"version":"5.9.3"}