feat: add Citations and WatchlistChat components, integrate with API
- Implemented Citations component to display citation data. - Created WatchlistDrawer and ChatSidebar components for managing watchlists and AI chat functionality. - Integrated API calls for watchlist management and chat interactions. - Updated index.tsx to include new components in the main application layout. - Added API client in lib/api.ts for structured API interactions. - Developed Alerts, Dashboard, Portfolio, Routines, Screener, and Report pages with relevant data fetching and UI components. - Introduced styles in tokens.css for consistent theming across the application. - Configured TypeScript and Vite for project setup and development.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { createResource, createSignal, For } from "solid-js";
|
||||
import { api } from "../lib/api";
|
||||
|
||||
export default function Alerts() {
|
||||
const [alerts, { refetch }] = createResource(() => api.alerts());
|
||||
const [events, { refetch: refetchEv }] = createResource(() => api.alertEvents("2000-01-01").then((r) => r.events.slice(0, 30)));
|
||||
const [dests, { refetch: refetchDests }] = createResource(() => api.destinations());
|
||||
const [name, setName] = createSignal("accumulation watcher");
|
||||
const [rule, setRule] = createSignal('{"all": true}');
|
||||
const [channels, setChannels] = createSignal("");
|
||||
const [dkind, setDkind] = createSignal("telegram");
|
||||
const [dlabel, setDlabel] = createSignal("");
|
||||
const [dbot, setDbot] = createSignal("");
|
||||
const [dchat, setDchat] = createSignal("");
|
||||
const [dhook, setDhook] = createSignal("");
|
||||
const [derr, setDerr] = createSignal("");
|
||||
async function create() {
|
||||
await api.createAlert({
|
||||
name: name(),
|
||||
rule: JSON.parse(rule()),
|
||||
channels: channels().split(",").map((c) => c.trim()).filter(Boolean),
|
||||
});
|
||||
refetch();
|
||||
}
|
||||
async function del(id: number) { await api.deleteAlert(id); refetch(); }
|
||||
async function addDest() {
|
||||
setDerr("");
|
||||
try {
|
||||
const body: Record<string, unknown> = dkind() === "telegram"
|
||||
? { kind: "telegram", label: dlabel().trim(), bot_token: dbot().trim(), chat_id: dchat().trim() }
|
||||
: { kind: "discord", label: dlabel().trim(), webhook_url: dhook().trim() };
|
||||
await api.createDestination(body);
|
||||
setDbot(""); setDchat(""); setDhook(""); setDlabel("");
|
||||
refetchDests();
|
||||
} catch (e) { setDerr(String(e)); }
|
||||
}
|
||||
async function toggleDest(id: number, enabled: boolean) {
|
||||
await api.updateDestination(id, { enabled: !enabled });
|
||||
refetchDests();
|
||||
}
|
||||
async function delDest(id: number) { await api.deleteDestination(id); refetchDests(); }
|
||||
return (
|
||||
<div>
|
||||
<h1>Alerts</h1>
|
||||
<div class="fs-card">
|
||||
<h3>Push destinations (per login)</h3>
|
||||
<p class="fs-muted">Secrets stay server-side — the list never shows tokens. Delivery uses your enabled destinations; server env is fallback.</p>
|
||||
<select class="fs-input" value={dkind()} onChange={(e) => setDkind(e.currentTarget.value)}>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="discord">Discord</option>
|
||||
</select>
|
||||
<input class="fs-input" placeholder="label (e.g. my phone)" value={dlabel()} onInput={(e) => setDlabel(e.currentTarget.value)} style={{ width: "160px" }} />
|
||||
{dkind() === "telegram" ? (
|
||||
<>
|
||||
<input class="fs-input" placeholder="bot token" value={dbot()} onInput={(e) => setDbot(e.currentTarget.value)} style={{ width: "220px" }} />
|
||||
<input class="fs-input" placeholder="chat id" value={dchat()} onInput={(e) => setDchat(e.currentTarget.value)} style={{ width: "140px" }} />
|
||||
</>
|
||||
) : (
|
||||
<input class="fs-input" placeholder="https://discord webhook url" value={dhook()} onInput={(e) => setDhook(e.currentTarget.value)} style={{ width: "300px" }} />
|
||||
)}
|
||||
<button class="fs-btn" onClick={addDest}>Add destination</button>
|
||||
{derr() ? <p class="fs-muted">{derr()}</p> : null}
|
||||
<ul>
|
||||
{(dests()?.destinations || []).map((d) => (
|
||||
<li>{d.kind} — {d.label || `#${d.id}`} <span class="fs-muted">{d.enabled ? "on" : "off"} · {d.configured ? "configured" : "missing secret"}</span>
|
||||
<button class="fs-btn ghost" onClick={() => toggleDest(d.id, d.enabled)}>{d.enabled ? "Disable" : "Enable"}</button>
|
||||
<button class="fs-btn ghost" onClick={() => delDest(d.id)}>del</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="fs-card">
|
||||
<h3>New rule</h3>
|
||||
<input class="fs-input" value={name()} onInput={(e) => setName(e.currentTarget.value)} />
|
||||
<input class="fs-input" value={rule()} onInput={(e) => setRule(e.currentTarget.value)} style={{ width: "280px" }} />
|
||||
<input class="fs-input" placeholder="Telegram/Discord channels, comma-separated" value={channels()} onInput={(e) => setChannels(e.currentTarget.value)} style={{ width: "300px" }} />
|
||||
<button class="fs-btn" onClick={create}>Create</button>
|
||||
<p class="fs-muted">Delivery: your enabled destinations above (server env fallback). Empty = record only.</p>
|
||||
</div>
|
||||
<div class="fs-grid">
|
||||
<div class="fs-card"><h3>Rules</h3>
|
||||
<ul><For each={alerts()?.alerts || []}>{(a) => <li>{a.name} <span class="fs-muted">last: {a.last_fired || "never"}</span> <button class="fs-btn ghost" onClick={() => del(a.id)}>del</button></li>}</For></ul>
|
||||
</div>
|
||||
<div class="fs-card"><h3>Event history</h3>
|
||||
<button class="fs-btn ghost" onClick={() => refetchEv()}>Refresh</button>
|
||||
<ul><For each={events() || []}>{(e) => <li><strong>{String(e.ticker)}</strong> — {String(e.message)}</li>}</For></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createResource, createSignal, onCleanup, For, Show } from "solid-js";
|
||||
import { api, subscribeSSE, fmtIDR } from "../lib/api";
|
||||
import { AccumulationTable, ActivityFeed, RotationMap } from "../components/Cards";
|
||||
import { Citations } from "../components/Citations";
|
||||
import { Line } from "solid-chartjs";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
Chart.register(...registerables);
|
||||
|
||||
export default function Dashboard() {
|
||||
const [health] = createResource(() => api.health());
|
||||
const [flow, { refetch }] = createResource(() => api.flowSummary());
|
||||
const [events, { refetch: refetchEv }] = createResource(() => api.alertEvents("2000-01-01").then((r) => r.events.slice(0, 10)));
|
||||
const [foreign] = createResource(() => api.flowForeign("BBCA").catch(() => null));
|
||||
const [live, setLive] = createSignal<string[]>([]);
|
||||
const stop = subscribeSSE((ch, data) => {
|
||||
setLive((p) => [`${ch}: ${data.slice(0, 120)}`, ...p].slice(0, 5));
|
||||
if (ch === "alerts" || ch === "activity") { refetch(); refetchEv(); }
|
||||
});
|
||||
onCleanup(stop);
|
||||
const flowChart = () => ({
|
||||
labels: (foreign()?.dates || []) as string[],
|
||||
datasets: [{ label: "BBCA foreign net", data: (foreign()?.nets || []) as number[] }],
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<h1>Smart Money Dashboard</h1>
|
||||
<Show when={health()?.stale_flags?.length}>
|
||||
<p class="fs-muted">Stale: <For each={health()!.stale_flags}>{(f) => <span>{f}; </span>}</For></p>
|
||||
</Show>
|
||||
<p class="fs-muted">
|
||||
Credits today: {health()?.credits_today ?? "…"} · Scheduler: {health()?.scheduler_ok ? "ok" : "down"} ·
|
||||
Last cycle: {health()?.last_cycle_at || "never"}
|
||||
</p>
|
||||
<div class="fs-grid">
|
||||
<div class="fs-card">
|
||||
<h3>Foreign net total</h3>
|
||||
<p style={{ "font-size": "24px" }}>{flow() ? fmtIDR(flow()!.foreign_net_total) : "…"}</p>
|
||||
<Citations items={flow()?.citations} />
|
||||
</div>
|
||||
<AccumulationTable rows={flow()?.top_accumulation || []} />
|
||||
<RotationMap signal={undefined} citations={flow()?.citations} />
|
||||
<div class="fs-card">
|
||||
<h3>Foreign flow (BBCA)</h3>
|
||||
<Show when={(foreign()?.dates || []).length} fallback={<p class="fs-muted">No foreign-flow series yet.</p>}>
|
||||
<Line data={flowChart()} options={{ responsive: true }} />
|
||||
<Show when={foreign()?.reversal}><p>Reversal flag set.</p></Show>
|
||||
</Show>
|
||||
</div>
|
||||
<ActivityFeed events={(events() || []).map((e) => ({ ticker: String(e.ticker || ""), message: String(e.message || ""), date: String(e.date || "") }))} />
|
||||
<div class="fs-card">
|
||||
<h3>Live agent panel</h3>
|
||||
<Show when={live().length} fallback={<p class="fs-muted">Waiting for SSE events…</p>}>
|
||||
<ul><For each={live()}>{(l) => <li class="fs-muted">{l}</li>}</For></ul>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createResource, For } from "solid-js";
|
||||
import { api } from "../lib/api";
|
||||
import { Bar } from "solid-chartjs";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
Chart.register(...registerables);
|
||||
|
||||
export default function Portfolio() {
|
||||
const [risk] = createResource(() => api.risk());
|
||||
const [acc] = createResource(() => api.accuracy());
|
||||
const chartData = () => ({
|
||||
labels: (risk()?.concentration || []).map((c) => c.ticker),
|
||||
datasets: [{ label: "Weight", data: (risk()?.concentration || []).map((c) => +(c.weight * 100).toFixed(1)) }],
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<h1>Portfolio Risk + Accuracy Ledger</h1>
|
||||
<div class="fs-grid">
|
||||
<div class="fs-card">
|
||||
<h3>Concentration</h3>
|
||||
<Bar data={chartData()} options={{ responsive: true }} />
|
||||
<ul><For each={risk()?.warnings || []}>{(w) => <li>{w}</li>}</For></ul>
|
||||
</div>
|
||||
<div class="fs-card">
|
||||
<h3>Beta vs index</h3>
|
||||
<p style={{ "font-size": "24px" }}>{risk()?.beta ?? "…"}</p>
|
||||
<h3>Correlation heatmap</h3>
|
||||
<CorrHeatmap matrix={risk()?.correlation || {}} />
|
||||
</div>
|
||||
<div class="fs-card">
|
||||
<h3>Agent accuracy</h3>
|
||||
<table class="fs-table">
|
||||
<thead><tr><th>Agent</th><th>Calls</th><th>Resolved</th><th>Hit %</th></tr></thead>
|
||||
<tbody>
|
||||
<For each={acc()?.agents || []}>
|
||||
{(a) => <tr><td>{a.agent}</td><td>{a.calls}</td><td>{a.resolved}</td><td>{(a.hit_rate * 100).toFixed(0)}%</td></tr>}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CorrHeatmap(props: { matrix: Record<string, Record<string, number>> }) {
|
||||
const keys = () => Object.keys(props.matrix);
|
||||
const cell = (v: number) => {
|
||||
const a = Math.min(1, Math.abs(v));
|
||||
const bg = v >= 0 ? `rgba(63,185,80,${(0.15 + 0.65 * a).toFixed(2)})` : `rgba(248,81,73,${(0.15 + 0.65 * a).toFixed(2)})`;
|
||||
return bg;
|
||||
};
|
||||
return (
|
||||
<div class="fs-scroll"><table class="fs-table">
|
||||
<thead><tr><th></th><For each={keys()}>{(k) => <th>{k}</th>}</For></tr></thead>
|
||||
<tbody>
|
||||
<For each={keys()}>
|
||||
{(r) => (
|
||||
<tr>
|
||||
<td><strong>{r}</strong></td>
|
||||
<For each={keys()}>
|
||||
{(c) => {
|
||||
const v = props.matrix[r]?.[c] ?? 0;
|
||||
return <td style={{ background: cell(v) }} title={`${r}/${c} = ${v.toFixed(2)}`}>{v.toFixed(2)}</td>;
|
||||
}}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table></div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { api, type ReportPayload } from "../lib/api";
|
||||
import { Citations } from "../components/Citations";
|
||||
|
||||
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 [answer, setAnswer] = createSignal("");
|
||||
const [err, setErr] = createSignal("");
|
||||
async function load() {
|
||||
setErr("");
|
||||
try { setRep(await api.report(params.ticker)); }
|
||||
catch (e) { setErr(String(e)); }
|
||||
}
|
||||
async function exportMd() { setMd(await api.reportMd(params.ticker)); }
|
||||
async function ask() {
|
||||
const r = await api.interrogate(params.ticker, question());
|
||||
setAnswer(r.answer);
|
||||
}
|
||||
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>
|
||||
<Show when={rep()}>
|
||||
<div class="fs-card">
|
||||
<h2 class={`rec-${rep()!.synthesis.recommendation}`}>{rep()!.synthesis.recommendation} · conviction {rep()!.synthesis.conviction}/5</h2>
|
||||
<p>{rep()!.synthesis.thesis}</p>
|
||||
<Citations items={rep()!.citations} />
|
||||
</div>
|
||||
<For each={rep()!.sections}>
|
||||
{(s) => <div class="fs-card"><h3>{s.name}</h3><p>{s.body}</p><Citations items={s.citations} /></div>}
|
||||
</For>
|
||||
<div class="fs-card">
|
||||
<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>
|
||||
</Show>
|
||||
<Show when={md()}><div class="fs-card"><pre class="fs-muted">{md().slice(0, 2000)}</pre></div></Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createResource, createSignal, For, Show } from "solid-js";
|
||||
import { api } from "../lib/api";
|
||||
import { Citations } from "../components/Citations";
|
||||
|
||||
export default function Routines() {
|
||||
const [data, { refetch }] = createResource(() => api.routines());
|
||||
const [runs, { refetch: refetchRuns }] = createResource(() => api.runs().then((r) => r.runs.slice(0, 20)));
|
||||
const [type_, setType] = createSignal("morning-briefing");
|
||||
const [schedule, setSchedule] = createSignal("");
|
||||
const [channels, setChannels] = createSignal("");
|
||||
const [briefing] = createResource(() => api.briefing().catch(() => null));
|
||||
async function subscribe() {
|
||||
await api.createRoutine({
|
||||
type: type_(),
|
||||
schedule_cron: schedule().trim() || undefined,
|
||||
channels: channels().split(",").map((c) => c.trim()).filter(Boolean),
|
||||
});
|
||||
refetch();
|
||||
}
|
||||
async function toggle(id: number, enabled: boolean) {
|
||||
await api.updateRoutine(id, { enabled: !enabled });
|
||||
refetch();
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>Routine Manager</h1>
|
||||
<div class="fs-card">
|
||||
<h3>Subscribe</h3>
|
||||
<select class="fs-input" value={type_()} onChange={(e) => setType(e.currentTarget.value)}>
|
||||
<For each={["morning-briefing","accumulation-radar","foreign-reversal","insider-tape","earnings-countdown","dividend-calendar","weekend-review"]}>
|
||||
{(t) => <option value={t}>{t}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<input class="fs-input" placeholder="schedule cron (blank = default)" value={schedule()} onInput={(e) => setSchedule(e.currentTarget.value)} style={{ width: "260px" }} />
|
||||
<input class="fs-input" placeholder="channels, comma-separated" value={channels()} onInput={(e) => setChannels(e.currentTarget.value)} style={{ width: "220px" }} />
|
||||
<button class="fs-btn" onClick={subscribe}>Subscribe</button>
|
||||
</div>
|
||||
<div class="fs-grid">
|
||||
<For each={data()?.routines || []}>
|
||||
{(r) => (
|
||||
<div class="fs-card">
|
||||
<strong>{r.type}</strong> <span class="fs-muted">{r.schedule_cron} · {r.enabled ? "on" : "off"}</span>
|
||||
<div><button class="fs-btn ghost" onClick={() => toggle(r.id, r.enabled)}>{r.enabled ? "Disable" : "Enable"}</button></div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={!data()?.routines?.length}><p class="fs-muted">No routines yet.</p></Show>
|
||||
<div class="fs-card">
|
||||
<h3>Latest briefing</h3>
|
||||
<Show when={briefing()} fallback={<p class="fs-muted">No briefing yet.</p>}>
|
||||
<pre class="fs-muted">{(briefing() as { payload: string })?.payload?.slice(0, 800)}</pre>
|
||||
<Citations items={(briefing() as { citations: string })?.citations} />
|
||||
</Show>
|
||||
</div>
|
||||
<div class="fs-card">
|
||||
<h3>Run history</h3>
|
||||
<button class="fs-btn ghost" onClick={() => refetchRuns()}>Refresh</button>
|
||||
<ul><For each={runs() || []}>{(r) => <li class="fs-muted">{String(r.started_at)} — {String(r.status)}</li>}</For></ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { api, type ScreenRow } from "../lib/api";
|
||||
import { Citations } from "../components/Citations";
|
||||
|
||||
const SAVED_KEY = "fs-saved-screeners";
|
||||
|
||||
export default function Screener() {
|
||||
const [mode, setMode] = createSignal<"where" | "q">("q");
|
||||
const [q, setQ] = createSignal("");
|
||||
const [brokerMin, setBrokerMin] = createSignal(0);
|
||||
const [rows, setRows] = createSignal<ScreenRow[]>([]);
|
||||
const [err, setErr] = createSignal("");
|
||||
const loadSaved = (): Record<string, string> => {
|
||||
try { return JSON.parse(localStorage.getItem(SAVED_KEY) || "{}"); } catch { return {}; }
|
||||
};
|
||||
const [saved, setSaved] = createSignal<Record<string, string>>(loadSaved());
|
||||
const [sname, setSname] = createSignal("");
|
||||
async function run() {
|
||||
setErr("");
|
||||
try {
|
||||
const body = mode() === "q"
|
||||
? { q: q() || undefined, institutional: { broker_score_min: brokerMin() }, limit: 20 }
|
||||
: { where: q() || undefined, institutional: { broker_score_min: brokerMin() }, limit: 20 };
|
||||
const r = await api.screen(body);
|
||||
setRows(r.rows);
|
||||
} catch (e) { setErr(String(e)); }
|
||||
}
|
||||
function save() {
|
||||
const all = { ...saved(), [sname().trim() || q().slice(0, 24) || "untitled"]: q() };
|
||||
localStorage.setItem(SAVED_KEY, JSON.stringify(all));
|
||||
setSaved(all);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>Institutional Screener</h1>
|
||||
<div class="fs-card">
|
||||
<select class="fs-input" value={mode()} onChange={(e) => setMode(e.currentTarget.value as "where" | "q")}>
|
||||
<option value="q">Natural language (q)</option>
|
||||
<option value="where">SQL-like (where)</option>
|
||||
</select>
|
||||
<input class="fs-input" placeholder={mode() === "q" ? "e.g. large banks with foreign inflow" : "e.g. market_cap > 10T"} value={q()} onInput={(e) => setQ(e.currentTarget.value)} style={{ width: "320px" }} />
|
||||
<label class="fs-muted"> broker ≥ <input class="fs-input" type="number" value={brokerMin()} onInput={(e) => setBrokerMin(Number(e.currentTarget.value))} style={{ width: "80px" }} /></label>
|
||||
<button class="fs-btn" onClick={run}>Screen</button>
|
||||
<Show when={err()}><p class="fs-muted">{err()}</p></Show>
|
||||
</div>
|
||||
<div class="fs-scroll"><table class="fs-table">
|
||||
<thead><tr><th>Symbol</th><th>Composite</th><th>Breakdown</th><th>Citations</th></tr></thead>
|
||||
<tbody>
|
||||
<For each={rows()}>
|
||||
{(r) => (
|
||||
<tr>
|
||||
<td><a href={`/report/${r.symbol}`}>{r.symbol}</a></td>
|
||||
<td>{r.composite.toFixed(1)}</td>
|
||||
<td class="fs-muted">{JSON.stringify(r.breakdown)}</td>
|
||||
<td><Citations items={r.citations} /></td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="fs-card">
|
||||
<h3>Saved screeners</h3>
|
||||
<input class="fs-input" placeholder="name" value={sname()} onInput={(e) => setSname(e.currentTarget.value)} style={{ width: "140px" }} />
|
||||
<button class="fs-btn ghost" onClick={save}>Save current</button>
|
||||
<ul><For each={Object.entries(saved())}>{([n, query]) => <li><button class="fs-btn ghost" onClick={() => { setQ(query); run(); }}>{n}</button> <span class="fs-muted">{query.slice(0, 60)}</span></li>}</For></ul>
|
||||
</div>
|
||||
<Show when={!rows().length}><p class="fs-muted">Run a screen to see ranked rows with per-row signal breakdown.</p></Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user