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:
1 parent
0db2cf28b3
commit
8c184ccae1
111 files changed
+12349
-62
No files matched your search
@@ -0,0 +1,161 @@
|
||||
// Package routines turns agent output into scheduled deliveries (R1..R7).
|
||||
// Each run records a routine_runs row; each delivery carries citations.
|
||||
// Routines read snapshots, never live-fetch inside delivery.
|
||||
package routines
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"flowsight/internal/alerts"
|
||||
"flowsight/internal/model"
|
||||
"flowsight/internal/store"
|
||||
)
|
||||
|
||||
// Routine types R1..R7.
|
||||
const (
|
||||
RBriefing = "morning-briefing"
|
||||
RRadar = "accumulation-radar"
|
||||
RReversal = "foreign-reversal"
|
||||
RInsider = "insider-tape"
|
||||
REarnings = "earnings-countdown"
|
||||
RDividend = "dividend-calendar"
|
||||
RWeekend = "weekend-review"
|
||||
)
|
||||
|
||||
// KnownType reports whether typ is one of R1..R7.
|
||||
func KnownType(typ string) bool {
|
||||
switch typ {
|
||||
case RBriefing, RRadar, RReversal, RInsider, REarnings, RDividend, RWeekend:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultSchedule returns the spec cron for a routine type (WIB).
|
||||
func DefaultSchedule(typ string) string {
|
||||
switch typ {
|
||||
case RBriefing:
|
||||
return "30 7 * * *" // 07:30 daily
|
||||
case RRadar, RReversal, RInsider:
|
||||
return "*/30 9-16 * * 1-5" // 30-min market hours
|
||||
case REarnings, RDividend:
|
||||
return "0 8 * * *" // 08:00 daily
|
||||
case RWeekend:
|
||||
return "0 9 * * 6" // Sat 09:00
|
||||
default:
|
||||
return "0 8 * * *"
|
||||
}
|
||||
}
|
||||
|
||||
// Engine runs routines on demand (called by the scheduler) and by hand.
|
||||
type Engine struct {
|
||||
DB *store.DB
|
||||
Notifier *alerts.Notifier
|
||||
UserKey string
|
||||
// Publish, when set, receives ("activity", text) per completed run so
|
||||
// the dashboard SSE feed shows routine activity live.
|
||||
Publish func(channel, data string)
|
||||
}
|
||||
|
||||
// Run executes one routine row and records the run.
|
||||
// Prefer RunFor: it scopes the run to the row owner (watchlist + delivery).
|
||||
func (e *Engine) Run(ctx context.Context, r store.Routine) (string, error) {
|
||||
return e.RunFor(ctx, r.UserKey, r)
|
||||
}
|
||||
|
||||
// RunFor executes one routine row owned by owner: tickers come from the
|
||||
// owner's watchlist and delivery fans out to the owner's destinations.
|
||||
func (e *Engine) RunFor(ctx context.Context, owner string, r store.Routine) (string, error) {
|
||||
// Scope this run to the owner without mutating shared engine state.
|
||||
scoped := *e
|
||||
if owner != "" {
|
||||
scoped.UserKey = owner
|
||||
}
|
||||
se := &scoped
|
||||
var (
|
||||
payload string
|
||||
cites []model.Citation
|
||||
err error
|
||||
)
|
||||
switch r.Type {
|
||||
case RBriefing:
|
||||
payload, cites, err = se.Briefing(ctx)
|
||||
case RRadar:
|
||||
payload, cites, err = se.Radar(ctx)
|
||||
case RReversal:
|
||||
payload, cites, err = se.ReversalWatch(ctx)
|
||||
case RInsider:
|
||||
payload, cites, err = se.InsiderTape(ctx)
|
||||
case REarnings:
|
||||
payload, cites, err = se.EarningsCountdown(ctx)
|
||||
case RDividend:
|
||||
payload, cites, err = se.DividendCalendar(ctx)
|
||||
case RWeekend:
|
||||
payload, cites, err = se.WeekendReview(ctx)
|
||||
default:
|
||||
err = fmt.Errorf("routines: unknown type %q", r.Type)
|
||||
}
|
||||
status := "ok"
|
||||
if err != nil {
|
||||
status = "error"
|
||||
payload = err.Error()
|
||||
}
|
||||
cc, _ := json.Marshal(cites)
|
||||
if _, rerr := se.DB.RecordRun(r.ID, status, summarize(payload, cc), 0, scoped.UserKey); rerr != nil && err == nil {
|
||||
err = rerr
|
||||
}
|
||||
if err == nil && status == "ok" {
|
||||
_ = se.deliver(ctx, r, payload, cites)
|
||||
}
|
||||
if e.Publish != nil {
|
||||
e.Publish("activity", r.Type+" "+status+": "+head(payload, 160))
|
||||
}
|
||||
return payload, err
|
||||
}
|
||||
|
||||
func head(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
func summarize(payload string, cites []byte) string {
|
||||
return `{"payload":` + strconv(payload) + `,"citations":` + string(cites) + `}`
|
||||
}
|
||||
|
||||
func strconv(s string) string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// deliver pushes the payload to the routine's channels via the notifier,
|
||||
// WITH citations (alert cards render context + citations + report link).
|
||||
// Delivery is scoped to the routine owner: the owner's enabled destinations
|
||||
// (managed at /alerts → Push destinations, i.e. POST /api/destinations)
|
||||
// plus the server env fallback. With no targets anywhere it is record-only.
|
||||
func (e *Engine) deliver(ctx context.Context, r store.Routine, payload string, cites []model.Citation) error {
|
||||
if len(r.Channels) == 0 || e.Notifier.DryRunFor(e.UserKey) {
|
||||
return nil
|
||||
}
|
||||
cc, _ := json.Marshal(cites)
|
||||
f := alerts.Finding{Rule: r.Type, Ticker: "IDX", Message: payload, Citations: cites}
|
||||
return e.Notifier.SendTo(ctx, e.UserKey, f, string(cc))
|
||||
}
|
||||
|
||||
// tickers returns the user's watchlist (never empty: falls back to demo set).
|
||||
func (e *Engine) tickers() []string {
|
||||
wl, err := e.DB.Watchlist(e.UserKey)
|
||||
if err == nil && len(wl) > 0 {
|
||||
return wl
|
||||
}
|
||||
return []string{"BBCA"}
|
||||
}
|
||||
|
||||
// todayStr is overridable in tests via Now.
|
||||
var nowFn = time.Now
|
||||
|
||||
func today() string { return nowFn().Format("2006-01-02") }
|
||||
Reference in new issue
Block a user