feat: FE solid-ui (shadcn port) — Tailwind + 14 components, all pages rewritten
CI / go test + web typecheck (push) Canceled after 0s
CI / build + deploy (Nix) — flowsight (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-09-15 17:40:39 +07:00
parent debc907188
commit 8392dbe724
34 changed files with 2390 additions and 542 deletions
+46 -37
View File
@@ -2,7 +2,12 @@ import { createSignal, For, Show, onMount } from "solid-js";
import { useParams } from "@solidjs/router";
import { api, type ReportPayload } from "../lib/api";
import { Citations } from "../components/Citations";
import { RecBadge, Skeleton } from "../components/ui";
import { PageHead, RecBadge } from "../components/ui";
import { Badge } from "../components/ui/badge";
import { Button } from "../components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
import { Skeleton } from "../components/ui/skeleton";
import { TextField, TextFieldInput } from "../components/ui/text-field";
export default function Report() {
const params = useParams();
@@ -35,54 +40,58 @@ export default function Report() {
a.href = url; a.download = name; a.click();
}
return (
<div>
<div class="fs-page-head">
<h1>Report: {params.ticker}</h1>
<button class="fs-btn ghost" onClick={load}>↻ Regenerate</button>
<div class="space-y-4">
<div class="flex items-center justify-between">
<PageHead title={`Report: ${params.ticker}`} />
<Button variant="outline" onClick={load}>↻ Regenerate</Button>
</div>
<Show when={err()}><p style={{ color: "var(--fs-avoid)" }}>{err()}</p></Show>
<Show when={busy()}><Skeleton rows={5} /></Show>
<Show when={err()}><p class="text-sm text-destructive">{err()}</p></Show>
<Show when={busy()}><div class="space-y-2"><Skeleton class="h-32 w-full" /><Skeleton class="h-24 w-full" /></div></Show>
<Show when={rep()}>
<div class="fs-card" style={{ "border-left": "4px solid var(--fs-accent)" }}>
<div class="fs-row">
<Card class="border-l-4 border-l-primary">
<CardContent class="flex flex-wrap items-center gap-2 pt-6">
<RecBadge rec={rep()!.synthesis.recommendation} />
<strong>conviction {rep()!.synthesis.conviction}/5</strong>
<Show when={rep()!.synthesis.conflict}><span class="fs-badge info">conflict flagged</span></Show>
<span class="fs-muted">position {rep()!.synthesis.position_pct}%</span>
</div>
<p>{rep()!.synthesis.thesis}</p>
<Citations items={rep()!.citations} />
</div>
<div class="fs-card" style={{ "margin-top": "14px" }}>
<h3>Export</h3>
<div class="fs-row">
<button class="fs-btn ghost" onClick={exportMd}>Markdown</button>
<button class="fs-btn ghost" onClick={async () => {
<Show when={rep()!.synthesis.conflict}><Badge variant="secondary">conflict flagged</Badge></Show>
<span class="text-sm text-muted-foreground">position {rep()!.synthesis.position_pct}%</span>
</CardContent>
<CardContent>
<p class="text-sm">{rep()!.synthesis.thesis}</p>
<div class="mt-2"><Citations items={rep()!.citations} /></div>
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>Export</CardTitle></CardHeader>
<CardContent class="flex flex-wrap gap-2">
<Button variant="outline" onClick={exportMd}>Markdown</Button>
<Button variant="outline" onClick={async () => {
const r = await fetch(`/api/report/${params.ticker}?format=html`, { method: "POST", headers: authHeaders() });
dl(URL.createObjectURL(new Blob([await r.text()], { type: "text/html" })), `${params.ticker}-report.html`);
}}>HTML</button>
<button class="fs-btn ghost" onClick={() => {
}}>HTML</Button>
<Button variant="outline" onClick={() => {
dl(URL.createObjectURL(new Blob([JSON.stringify(rep(), null, 1)], { type: "application/json" })), `${params.ticker}-report.json`);
}}>JSON</button>
<button class="fs-btn ghost" onClick={async () => {
}}>JSON</Button>
<Button variant="outline" onClick={async () => {
const r = await fetch(`/api/report/${params.ticker}?format=pdf`, { method: "POST", headers: authHeaders() });
dl(URL.createObjectURL(await r.blob()), `${params.ticker}-report.pdf`);
}}>PDF</button>
</div>
</div>
}}>PDF</Button>
</CardContent>
</Card>
<For each={rep()!.sections}>
{(s) => <div class="fs-card" style={{ "margin-top": "14px" }}><h3>{s.name}</h3><p style={{ "white-space": "pre-wrap" }}>{s.body}</p><Citations items={s.citations} /></div>}
{(s) => <Card><CardHeader><CardTitle>{s.name}</CardTitle></CardHeader><CardContent><p class="whitespace-pre-wrap text-sm">{s.body}</p><div class="mt-2"><Citations items={s.citations} /></div></CardContent></Card>}
</For>
<div class="fs-card" style={{ "margin-top": "14px" }}>
<h3>Interrogate this report</h3>
<div class="fs-row">
<input class="fs-input" placeholder="Ask about this report…" value={question()} onInput={(e) => setQuestion(e.currentTarget.value)} style={{ flex: 1 }} />
<button class="fs-btn" onClick={ask} disabled={asking()}>{asking() ? "…" : "Ask"}</button>
</div>
<Show when={answer()}><p style={{ "margin-top": "10px" }}>{answer()}</p></Show>
</div>
<Card>
<CardHeader><CardTitle>Interrogate this report</CardTitle></CardHeader>
<CardContent class="space-y-3">
<div class="flex gap-2">
<TextField class="flex-1"><TextFieldInput placeholder="Ask about this report…" value={question()} onInput={(e) => setQuestion(e.currentTarget.value)} /></TextField>
<Button onClick={ask} disabled={asking()}>{asking() ? "…" : "Ask"}</Button>
</div>
<Show when={answer()}><p class="text-sm">{answer()}</p></Show>
</CardContent>
</Card>
</Show>
<Show when={md()}><div class="fs-card" style={{ "margin-top": "14px" }}><pre class="fs-muted">{md().slice(0, 2000)}</pre></div></Show>
<Show when={md()}><Card><CardContent class="pt-6"><pre class="whitespace-pre-wrap text-sm text-muted-foreground">{md().slice(0, 2000)}</pre></CardContent></Card></Show>
</div>
);
}