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,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ChatRequest is POST /api/chat {message, scope?: {report_id}}.
|
||||
type ChatRequest struct {
|
||||
Message string `json:"message" validate:"required"`
|
||||
Scope *struct {
|
||||
ReportID int64 `json:"report_id"`
|
||||
} `json:"scope"`
|
||||
}
|
||||
|
||||
// Chat serves POST /api/chat: cited answers. With scope.report_id the
|
||||
// grounding is restricted to that report's citations (report interrogation);
|
||||
// without scope it answers from latest snapshots. The LLM refines prose only
|
||||
// — numbers always come from stored data, never from generation.
|
||||
func (s *Server) Chat(w http.ResponseWriter, r *http.Request) {
|
||||
var req ChatRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
writeErr(w, http.StatusUnprocessableEntity, "message is required")
|
||||
return
|
||||
}
|
||||
// Scope grounding: report citations when scoped.
|
||||
var ground, citesRaw string
|
||||
if req.Scope != nil && req.Scope.ReportID > 0 {
|
||||
var cites string
|
||||
var at string
|
||||
err := s.DB.QueryRow(`SELECT payload_json, citations_json, generated_at FROM reports WHERE id=?`,
|
||||
req.Scope.ReportID).Scan(&ground, &cites, &at)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusNotFound, "report not found")
|
||||
return
|
||||
}
|
||||
citesRaw = cites
|
||||
} else {
|
||||
// Unscoped: ground on the latest briefing + watchlist.
|
||||
_, payload, cites, err := s.DB.LatestBriefing()
|
||||
if err != nil {
|
||||
ground = "no briefing or report data yet"
|
||||
} else {
|
||||
ground, citesRaw = payload, cites
|
||||
}
|
||||
}
|
||||
answer := "Based on stored data: " + head(ground, 600)
|
||||
if s.LLM.Available() {
|
||||
if text, err := s.LLM.Complete(r.Context(), s.Cfg.LLMTriage,
|
||||
"You answer questions about Indonesian stocks using ONLY the grounded data below. "+
|
||||
"Every number in your answer must cite its source. If the data lacks the answer, say so.",
|
||||
"Question: "+req.Message+"\n\nGrounded data:\n"+head(ground, 3000), 400); err == nil {
|
||||
answer = strings.TrimSpace(text)
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"answer": answer, "grounding": head(ground, 600), "citations": citesRaw,
|
||||
})
|
||||
}
|
||||
|
||||
func head(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
Reference in New Issue
Block a user