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:
asepharyana
2026-09-15 12:36:48 +07:00
parent 0db2cf28b3
commit 8c184ccae1
111 changed files with 12349 additions and 62 deletions
+45
View File
@@ -0,0 +1,45 @@
import { For, Show } from "solid-js";
import { fmtIDR } from "../lib/api";
import { Citations } from "./Citations";
export function AccumulationTable(props: { rows: { ticker: string; net_sum: number; brokers: number }[] }) {
return (
<div class="fs-card">
<h3>Top accumulation</h3>
<table class="fs-table">
<thead><tr><th>Ticker</th><th>Net</th><th>Brokers</th></tr></thead>
<tbody>
<For each={props.rows}>
{(r) => <tr><td><a href={`/report/${r.ticker}`}>{r.ticker}</a></td><td>{fmtIDR(r.net_sum)}</td><td>{r.brokers}</td></tr>}
</For>
</tbody>
</table>
<Show when={!props.rows?.length}><p class="fs-muted">No accumulation snapshots yet.</p></Show>
</div>
);
}
export function RotationMap(props: { signal?: string; citations?: Parameters<typeof Citations>[0]["items"] }) {
return (
<div class="fs-card">
<h3>Sector rotation</h3>
<p>{props.signal || "No rotation flip detected."}</p>
<Citations items={props.citations} />
</div>
);
}
export function ActivityFeed(props: { events: { id?: number; ticker?: string; message?: string; date?: string }[] }) {
return (
<div class="fs-card">
<h3>Activity</h3>
<Show when={props.events?.length} fallback={<p class="fs-muted">No events yet.</p>}>
<ul>
<For each={props.events}>
{(e) => <li><strong>{e.ticker}</strong> — {e.message} <span class="fs-muted">{e.date}</span></li>}
</For>
</ul>
</Show>
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { For } from "solid-js";
import type { Citation } from "../lib/api";
export function Citations(props: { items?: Citation[] | string }) {
const list = (): Citation[] => {
if (!props.items) return [];
if (typeof props.items === "string") {
try { return JSON.parse(props.items); } catch { return []; }
}
return props.items;
};
return (
<span>
<For each={list()}>
{(c) => (
<span class={`fs-cite${c.stale ? " stale" : ""}`} title={`${c.endpoint} @ ${c.snapshot_at}${c.stale ? " (stale)" : ""}`}>
{c.endpoint} {c.ticker || ""} @ {(c.snapshot_at || "").slice(0, 10)}{c.stale ? " · stale" : ""}
</span>
)}
</For>
</span>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { createResource, createSignal, For, Show } from "solid-js";
import { api } from "../lib/api";
// WatchlistDrawer: global watchlist add/remove backed by /api/watchlist.
export function WatchlistDrawer() {
const [wl, { refetch }] = createResource(() => api.watchlist());
const [ticker, setTicker] = createSignal("");
const [err, setErr] = createSignal("");
async function add() {
setErr("");
try {
await api.addWatch(ticker().toUpperCase().trim());
setTicker("");
refetch();
} catch (e) { setErr(String(e)); }
}
async function del(t: string) {
await api.removeWatch(t);
refetch();
}
return (
<div class="fs-card">
<h3>Watchlist</h3>
<input class="fs-input" placeholder="BBCA" value={ticker()} onInput={(e) => setTicker(e.currentTarget.value)} style={{ width: "90px" }} />
<button class="fs-btn" onClick={add}>Add</button>
<Show when={err()}><p class="fs-muted">{err()}</p></Show>
<ul><For each={wl()?.watchlist || []}>{(t) => <li><a href={`/report/${t}`}>{t}</a> <button class="fs-btn ghost" onClick={() => del(t)}>×</button></li>}</For></ul>
</div>
);
}
// ChatSidebar: context-aware Q&A backed by POST /api/chat.
export function ChatSidebar() {
const [msg, setMsg] = createSignal("");
const [log, setLog] = createSignal<{ q: string; a: string }[]>([]);
async function send() {
const q = msg().trim();
if (!q) return;
setMsg("");
try {
const r = await api.chat(q);
setLog((p) => [...p, { q, a: r.answer }].slice(-10));
} catch (e) {
setLog((p) => [...p, { q, a: String(e) }].slice(-10));
}
}
return (
<div class="fs-card">
<h3>AI Chat</h3>
<For each={log()}>
{(m) => <div><p><strong>You:</strong> {m.q}</p><p class="fs-muted">{m.a.slice(0, 500)}</p></div>}
</For>
<input class="fs-input" placeholder="Ask about your watchlist…" value={msg()} onInput={(e) => setMsg(e.currentTarget.value)} style={{ width: "220px" }} />
<button class="fs-btn" onClick={send}>Send</button>
</div>
);
}