From 6339b332700fc97556d00d32bb196db500097f97 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Fri, 24 Jul 2026 22:01:13 +0000 Subject: [PATCH] Add insight history view; feed recent insights back into the prompt Dashboard now shows a collapsible history of past AI insights (reusing the existing /history endpoint), and generation now includes the last 3 insights in the prompt so the model can reference what it already said instead of repeating the same points every day. Co-Authored-By: Claude Sonnet 5 --- backend/src/jobs/ai-insights.js | 31 ++++++++-- frontend/src/api.ts | 4 ++ frontend/src/components/AIInsightHistory.tsx | 64 ++++++++++++++++++++ frontend/src/pages/Dashboard.tsx | 5 ++ 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/AIInsightHistory.tsx diff --git a/backend/src/jobs/ai-insights.js b/backend/src/jobs/ai-insights.js index a5a5207..9392f4e 100644 --- a/backend/src/jobs/ai-insights.js +++ b/backend/src/jobs/ai-insights.js @@ -46,6 +46,16 @@ export async function getAiInsightsConfig() { } } +// Last few generated insights, most recent first — fed back into the prompt so the +// model can reference what it already said instead of repeating itself verbatim. +export async function getRecentInsights(limit = 3) { + const res = await pool.query( + `SELECT generated_at, content FROM ai_insights ORDER BY generated_at DESC LIMIT $1`, + [limit] + ) + return res.rows +} + export async function checkDailyBudget(budgetTokens) { const res = await pool.query( `SELECT COALESCE(SUM(input_tokens + output_tokens), 0) AS total @@ -315,17 +325,29 @@ export async function gatherRevenueCorrelation(monthProgress) { } } -export function buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation) { +export function buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation, recentInsights = []) { const systemMsg = "You are an AI assistant for a wage cost controller in a UK hospitality business. Analyze the data below and " + "provide a concise daily briefing (3-5 bullet points). Focus on: how this month's wage cost is tracking " + "against budget, the month-end forecast, notable variance between rota and actual cost by department and " + "what might be causing it, individual pay anomalies worth a look, and wage cost as a percentage of revenue. " + "Use UK date format (DD/MM/YYYY) and GBP (£) for all monetary values. Be specific with department/employee " + - "names and numbers. Keep it actionable — no fluff or generic advice." + "names and numbers. Keep it actionable — no fluff or generic advice.\n\n" + + "A 'Recent Previous Insights' section may be included below — compare against them explicitly: call out " + + "what's changed, what's resolved, and what's still an open issue. Don't just repeat the same points verbatim." const lines = [] + if (recentInsights.length) { + lines.push('## Recent Previous Insights (most recent first — reference these, do not just repeat them)') + for (const r of recentInsights) { + const when = new Date(r.generated_at).toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) + lines.push(`${when}:`) + lines.push(r.content) + lines.push('') + } + } + lines.push(`## Month to Date (${monthProgress.monthLabel})`) lines.push( `Day ${monthProgress.daysElapsed} of ${monthProgress.daysInMonth} (${fmtPct(monthProgress.pctMonthElapsed)} of month elapsed)` @@ -430,15 +452,16 @@ export async function generateInsight(triggeredBy = 'scheduler') { } const monthProgress = await gatherMonthProgressData() - const [priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation] = await Promise.all([ + const [priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation, recentInsights] = await Promise.all([ gatherPriorPeriodData(), gatherRotaVsActualData(), gatherForecastData(monthProgress), gatherEmployeeAnomalies(), gatherRevenueCorrelation(monthProgress), + getRecentInsights(3), ]) - const { systemMsg, userMsg } = buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation) + const { systemMsg, userMsg } = buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation, recentInsights) let result try { diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 092bd8d..70eb971 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -94,6 +94,10 @@ export function getLatestInsight(): Promise { return request('/ai-insights/latest') } +export function getInsightHistory(limit = 20, offset = 0): Promise<{ insights: AIInsight[]; total: number }> { + return request(`/ai-insights/history?limit=${limit}&offset=${offset}`) +} + export function generateInsight(): Promise<{ success: boolean; content: string; input_tokens: number; output_tokens: number; model: string }> { return request('/ai-insights/generate', { method: 'POST' }) } diff --git a/frontend/src/components/AIInsightHistory.tsx b/frontend/src/components/AIInsightHistory.tsx new file mode 100644 index 0000000..b06f56e --- /dev/null +++ b/frontend/src/components/AIInsightHistory.tsx @@ -0,0 +1,64 @@ +import { useState, useEffect } from 'react' +import { History, ChevronDown, ChevronRight, Clock } from 'lucide-react' +import { getInsightHistory } from '../api' +import { formatAge, renderContent } from '../lib/aiInsight' +import type { AIInsight } from '../types' + +export default function AIInsightHistory({ refreshKey }: { refreshKey?: number }) { + const [insights, setInsights] = useState([]) + const [loading, setLoading] = useState(true) + const [expandedId, setExpandedId] = useState(null) + + useEffect(() => { + setLoading(true) + getInsightHistory(20) + .then(res => setInsights(res.insights)) + .finally(() => setLoading(false)) + }, [refreshKey]) + + return ( +
+
+ + Insight History +
+ + {loading &&
Loading history…
} + {!loading && insights.length === 0 &&
No past insights yet.
} + + {insights.map((item, i) => { + const isOpen = expandedId === item.id + return ( +
+ + {isOpen && ( +
+ {renderContent(item.content)} +
+ {item.input_tokens}↑ / {item.output_tokens}↓ tokens · {item.triggered_by} +
+
+ )} +
+ ) + })} +
+ ) +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 6a86df8..42daa3f 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react' import { Bot, Clock, RefreshCw } from 'lucide-react' import { getLatestInsight, generateInsight } from '../api' import { formatAge, renderContent } from '../lib/aiInsight' +import AIInsightHistory from '../components/AIInsightHistory' import type { AIInsight } from '../types' const REFRESH_MS = 5 * 60_000 @@ -12,6 +13,7 @@ export default function Dashboard() { const [error, setError] = useState(null) const [generating, setGenerating] = useState(false) const [genError, setGenError] = useState(null) + const [historyKey, setHistoryKey] = useState(0) const load = useCallback(() => { getLatestInsight() @@ -32,6 +34,7 @@ export default function Dashboard() { try { await generateInsight() load() + setHistoryKey(k => k + 1) } catch (e) { setGenError(e instanceof Error ? e.message : 'Failed to generate insight') } finally { @@ -88,6 +91,8 @@ export default function Dashboard() { )} + + ) }