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 committed 2026-09-15 12:36:48 +07:00
1 parent 0db2cf28b3
commit 8c184ccae1
111 files changed
+12349 -62

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
package reports
import (
"bytes"
"fmt"
"strings"
"time"
"github.com/jung-kurt/gofpdf"
)
// todayStr stamps report generation time (UTC, date precision is enough).
func todayStr() string { return time.Now().UTC().Format("2006-01-02 15:04") }
// ToMarkdown renders the report as Markdown.
func (r Report) ToMarkdown() string {
var b strings.Builder
fmt.Fprintf(&b, "# %s — FlowSight Report (%s)\n\n", r.Ticker, r.GeneratedAt)
for _, s := range r.Sections {
fmt.Fprintf(&b, "## %s\n\n%s\n\n", s.Name, s.Body)
if len(s.Citations) > 0 {
b.WriteString("Citations:\n")
for _, c := range s.Citations {
fmt.Fprintf(&b, "- %s %s @ %s\n", c.Endpoint, c.Ticker, c.SnapshotAt)
}
b.WriteString("\n")
}
}
return b.String()
}
// ToHTML renders the report as a standalone page.
func (r Report) ToHTML() string {
var b strings.Builder
b.WriteString(`<!doctype html><html><head><meta charset="utf-8"><title>`)
b.WriteString(r.Ticker + " — FlowSight Report</title></head><body>")
fmt.Fprintf(&b, "<h1>%s — FlowSight Report (%s)</h1>", r.Ticker, r.GeneratedAt)
for _, s := range r.Sections {
fmt.Fprintf(&b, "<h2>%s</h2><p>%s</p>", s.Name, s.Body)
if len(s.Citations) > 0 {
b.WriteString("<ul>")
for _, c := range s.Citations {
fmt.Fprintf(&b, "<li>%s %s @ %s</li>", c.Endpoint, c.Ticker, c.SnapshotAt)
}
b.WriteString("</ul>")
}
}
b.WriteString("</body></html>")
return b.String()
}
// ToPDF renders the report server-side (pure Go, no system deps).
func (r Report) ToPDF() ([]byte, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddPage()
pdf.SetFont("Helvetica", "B", 16)
pdf.Cell(0, 10, r.Ticker+" — FlowSight Report")
pdf.Ln(12)
pdf.SetFont("Helvetica", "", 10)
pdf.Cell(0, 6, "Generated "+r.GeneratedAt)
pdf.Ln(8)
for _, s := range r.Sections {
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, s.Name)
pdf.Ln(8)
pdf.SetFont("Helvetica", "", 10)
pdf.MultiCell(0, 5, latin1(s.Body), "", "", false)
if len(s.Citations) > 0 {
pdf.SetFont("Helvetica", "I", 8)
for _, c := range s.Citations {
pdf.MultiCell(0, 4, latin1(c.Endpoint+" "+c.Ticker+" @ "+c.SnapshotAt), "", "", false)
}
}
pdf.Ln(4)
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// latin1 drops non-latin runes gofpdf core fonts cannot render.
func latin1(s string) string {
return strings.Map(func(r rune) rune {
if r > 255 {
return '?'
}
return r
}, s)
}
+115
View File
@@ -0,0 +1,115 @@
// Package reports builds the 7-section One-Click Report (overview, valuation,
// institutional, earnings, risk, calendar, recommendation) with citations[]
// per section, and exports PDF/HTML/MD/JSON. Every section populates from
// live/seed data; nothing renders empty without saying which snapshot is
// missing.
package reports
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"flowsight/internal/agents"
"flowsight/internal/model"
"flowsight/internal/store"
)
// Section is one report section with its own citations.
type Section struct {
Name string `json:"name"`
Body string `json:"body"`
Citations []model.Citation `json:"citations"`
}
// Report is the full 7-section payload.
type Report struct {
Ticker string `json:"ticker"`
GeneratedAt string `json:"generated_at"`
Sections []Section `json:"sections"`
Synthesis agents.Synthesis `json:"synthesis"`
AgentScores []model.AgentResult `json:"agent_scores"`
AllCitations []model.Citation `json:"citations"`
}
// Builder assembles reports from agent runs over stored snapshots.
type Builder struct {
DB *store.DB
Deps agents.Deps
Risk RiskFn
}
// RiskFn computes the portfolio-risk lines reused in the risk section.
type RiskFn func(ticker string) (concentration, beta string)
// Build runs A1..A6 + A7 and assembles all 7 sections, returning the
// persisted report id for citation-scoped chat interrogation.
func (b *Builder) Build(ctx context.Context, ticker, profile string) (Report, int64, error) {
ticker = strings.ToUpper(ticker)
results := agents.RunAll(ctx, b.Deps, ticker)
synth := agents.Synthesize(ctx, b.Deps, ticker, agents.RiskProfile(profile), results)
byAgent := map[string]model.AgentResult{}
for _, r := range results {
byAgent[r.Agent] = r
}
sec := func(name, body string, cites []model.Citation) Section {
if strings.TrimSpace(body) == "" {
body = "no snapshots for this section yet"
}
return Section{Name: name, Body: body, Citations: cites}
}
sections := []Section{
sec("overview", overviewLine(ticker, byAgent), citesOf(byAgent, "sentiment", "technical")),
sec("valuation", byAgent["fundamental"].Summary, citesOf(byAgent, "fundamental")),
sec("institutional", institutionalLine(byAgent), citesOf(byAgent, "smart-money", "broker-intel")),
sec("earnings", byAgent["fundamental"].Summary+" | "+byAgent["catalyst"].Summary, citesOf(byAgent, "fundamental", "catalyst")),
sec("risk", riskLine(ticker, b.Risk), citesOf(byAgent, "technical")),
sec("calendar", byAgent["catalyst"].Summary, citesOf(byAgent, "catalyst")),
sec("recommendation", synth.Thesis, synth.Citations),
}
all := []model.Citation{}
for _, s := range sections {
all = append(all, s.Citations...)
}
all = model.MarkStale(all, model.StaleSession(time.Now()))
raw, _ := json.Marshal(map[string]any{"ticker": ticker, "sections": sections, "synthesis": synth})
citesRaw, _ := json.Marshal(all)
id, err := b.DB.SaveReport(ticker, string(raw), string(citesRaw))
if err != nil {
return Report{}, 0, err
}
return Report{
Ticker: ticker, GeneratedAt: todayStr(),
Sections: sections, Synthesis: synth,
AgentScores: results, AllCitations: all,
}, id, nil
}
func citesOf(byAgent map[string]model.AgentResult, names ...string) []model.Citation {
var out []model.Citation
for _, n := range names {
out = append(out, byAgent[n].Citations...)
}
return out
}
func overviewLine(ticker string, byAgent map[string]model.AgentResult) string {
return fmt.Sprintf("%s — sentiment %s; technical %s.",
ticker, byAgent["sentiment"].Summary, byAgent["technical"].Summary)
}
func institutionalLine(byAgent map[string]model.AgentResult) string {
return fmt.Sprintf("smart-money %s; broker-intel %s.",
byAgent["smart-money"].Summary, byAgent["broker-intel"].Summary)
}
func riskLine(ticker string, fn RiskFn) string {
if fn == nil {
return ticker + ": concentration/beta from /portfolio/risk (run screener first)."
}
c, b := fn(ticker)
return fmt.Sprintf("%s: %s; %s.", ticker, c, b)
}