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,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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user