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 () => {
try {
const response = await fetch(`${API_BASE}/api/analysis/jobs/${job.id}`); const response = await fetch(`${API_BASE}/api/analysis/jobs/${job.id}`);
if (!response.ok) {
throw new Error(`Polling failed (${response.status})`);
}
const updated = (await response.json()) as AnalysisJob; const updated = (await response.json()) as AnalysisJob;
setJob(updated); 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,10 +232,16 @@ 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>}
<form className="stack" onSubmit={submitJob}>
<label> <label>
Ticker Ticker
<input value={ticker} onChange={(e) => setTicker(e.target.value.toUpperCase())} /> <input
value={ticker}
onChange={(e) => setTicker(e.target.value.toUpperCase())}
placeholder="e.g. NVDA"
/>
</label> </label>
<label> <label>
@ -221,6 +264,15 @@ export default function App() {
</select> </select>
</label> </label>
<label>
Provider Base URL Override (optional)
<input
value={backendUrlOverride}
onChange={(e) => setBackendUrlOverride(e.target.value)}
placeholder={providerMeta?.base_url || "https://..."}
/>
</label>
<label> <label>
Quick-Thinking Model Quick-Thinking Model
<select value={quickThink} onChange={(e) => setQuickThink(e.target.value)}> <select value={quickThink} onChange={(e) => setQuickThink(e.target.value)}>
@ -256,6 +308,20 @@ export default function App() {
</label> </label>
)} )}
{provider === "openai" && (
<label>
OpenAI Reasoning Effort
<select
value={openaiReasoningEffort}
onChange={(e) => setOpenaiReasoningEffort(e.target.value)}
>
<option value="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
</select>
</label>
)}
<label> <label>
Research Depth Research Depth
<div className="depth"> <div className="depth">
@ -288,9 +354,16 @@ export default function App() {
</div> </div>
</div> </div>
<button className="run" type="button" onClick={submitJob} disabled={isSubmitting || loadingOptions}> {submitError && <div className="card error">{submitError}</div>}
<button
className="run"
type="submit"
disabled={isSubmitting || loadingOptions || !formValid}
>
{isSubmitting ? "Submitting..." : "Start Analysis Job"} {isSubmitting ? "Submitting..." : "Start Analysis Job"}
</button> </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"}