feat: briefing auto-generate, pages overhaul (dashboard/screener/report/routines), delete routine
This commit is contained in:
+57
-36
@@ -1,67 +1,88 @@
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
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";
|
||||
|
||||
export default function Report() {
|
||||
const params = useParams();
|
||||
const [rep, setRep] = createSignal<ReportPayload | null>(null);
|
||||
const [md, setMd] = createSignal("");
|
||||
const [question, setQuestion] = createSignal("kenapa conviction segitu?");
|
||||
const [question, setQuestion] = createSignal("");
|
||||
const [answer, setAnswer] = createSignal("");
|
||||
const [busy, setBusy] = createSignal(true);
|
||||
const [asking, setAsking] = createSignal(false);
|
||||
const [err, setErr] = createSignal("");
|
||||
const authHeaders = () => ({ "X-User-Key": localStorage.getItem("fs-key") || "demo" });
|
||||
async function load() {
|
||||
setErr("");
|
||||
setErr(""); setBusy(true);
|
||||
try { setRep(await api.report(params.ticker)); }
|
||||
catch (e) { setErr(String(e)); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
onMount(load);
|
||||
async function exportMd() { setMd(await api.reportMd(params.ticker)); }
|
||||
async function ask() {
|
||||
const r = await api.interrogate(params.ticker, question());
|
||||
setAnswer(r.answer);
|
||||
if (!question().trim()) return;
|
||||
setAsking(true);
|
||||
try {
|
||||
const r = await api.interrogate(params.ticker, question());
|
||||
setAnswer(r.answer);
|
||||
} finally { setAsking(false); }
|
||||
}
|
||||
function dl(url: string, name: string) {
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = name; a.click();
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>Report: {params.ticker}</h1>
|
||||
<button class="fs-btn" onClick={load}>Generate report</button>
|
||||
<button class="fs-btn ghost" onClick={exportMd}>Export MD</button>
|
||||
<button class="fs-btn ghost" onClick={async () => {
|
||||
const r = await fetch(`/api/report/${params.ticker}?format=html`, { method: "POST", headers: { "X-User-Key": localStorage.getItem("fs-key") || "demo" } });
|
||||
const html = await r.text();
|
||||
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
||||
window.open(url, "_blank");
|
||||
}}>HTML</button>
|
||||
<button class="fs-btn ghost" onClick={() => {
|
||||
const blob = new Blob([JSON.stringify(rep(), null, 1)], { type: "application/json" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${params.ticker}-report.json`;
|
||||
a.click();
|
||||
}}>JSON</button>
|
||||
<button class="fs-btn ghost" onClick={async () => {
|
||||
const r = await fetch(`/api/report/${params.ticker}?format=pdf`, { method: "POST", headers: { "X-User-Key": localStorage.getItem("fs-key") || "demo" } });
|
||||
const blob = await r.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank");
|
||||
}}>PDF</button>
|
||||
<Show when={err()}><p class="fs-muted">{err()}</p></Show>
|
||||
<div class="fs-page-head">
|
||||
<h1>Report: {params.ticker}</h1>
|
||||
<button class="fs-btn ghost" 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={rep()}>
|
||||
<div class="fs-card">
|
||||
<h2 class={`rec-${rep()!.synthesis.recommendation}`}>{rep()!.synthesis.recommendation} · conviction {rep()!.synthesis.conviction}/5</h2>
|
||||
<div class="fs-card" style={{ "border-left": "4px solid var(--fs-accent)" }}>
|
||||
<div class="fs-row">
|
||||
<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 () => {
|
||||
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={() => {
|
||||
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 () => {
|
||||
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>
|
||||
<For each={rep()!.sections}>
|
||||
{(s) => <div class="fs-card"><h3>{s.name}</h3><p>{s.body}</p><Citations items={s.citations} /></div>}
|
||||
{(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>}
|
||||
</For>
|
||||
<div class="fs-card">
|
||||
<div class="fs-card" style={{ "margin-top": "14px" }}>
|
||||
<h3>Interrogate this report</h3>
|
||||
<input class="fs-input" value={question()} onInput={(e) => setQuestion(e.currentTarget.value)} style={{ width: "320px" }} />
|
||||
<button class="fs-btn" onClick={ask}>Ask</button>
|
||||
<Show when={answer()}><p>{answer()}</p></Show>
|
||||
<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>
|
||||
</Show>
|
||||
<Show when={md()}><div class="fs-card"><pre class="fs-muted">{md().slice(0, 2000)}</pre></div></Show>
|
||||
<Show when={md()}><div class="fs-card" style={{ "margin-top": "14px" }}><pre class="fs-muted">{md().slice(0, 2000)}</pre></div></Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user