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 <noreply@anthropic.com>
This commit is contained in:
parent
cabaff8d35
commit
6339b33270
4 changed files with 100 additions and 4 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ export function getLatestInsight(): Promise<AIInsight | null> {
|
|||
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' })
|
||||
}
|
||||
|
|
|
|||
64
frontend/src/components/AIInsightHistory.tsx
Normal file
64
frontend/src/components/AIInsightHistory.tsx
Normal file
|
|
@ -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<AIInsight[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
getInsightHistory(20)
|
||||
.then(res => setInsights(res.insights))
|
||||
.finally(() => setLoading(false))
|
||||
}, [refreshKey])
|
||||
|
||||
return (
|
||||
<div className="card" style={{ marginTop: 16 }}>
|
||||
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<History size={16} strokeWidth={1.75} color="var(--gold)" />
|
||||
Insight History
|
||||
</div>
|
||||
|
||||
{loading && <div className="state-center">Loading history…</div>}
|
||||
{!loading && insights.length === 0 && <div className="state-center">No past insights yet.</div>}
|
||||
|
||||
{insights.map((item, i) => {
|
||||
const isOpen = expandedId === item.id
|
||||
return (
|
||||
<div key={item.id} style={{ borderTop: i === 0 ? 'none' : '1px solid var(--border)' }}>
|
||||
<button
|
||||
onClick={() => setExpandedId(isOpen ? null : item.id)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '10px 0',
|
||||
background: 'none', border: 'none', cursor: 'pointer', textAlign: 'left', font: 'inherit',
|
||||
}}
|
||||
>
|
||||
{isOpen
|
||||
? <ChevronDown size={14} strokeWidth={1.75} color="var(--text-muted)" style={{ flexShrink: 0 }} />
|
||||
: <ChevronRight size={14} strokeWidth={1.75} color="var(--text-muted)" style={{ flexShrink: 0 }} />}
|
||||
<span style={{ flex: 1, fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||
{new Date(item.generated_at).toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'var(--text-muted)', flexShrink: 0 }}>
|
||||
<Clock size={11} strokeWidth={1.75} />
|
||||
{formatAge(item.generated_at)}
|
||||
</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div style={{ padding: '0 0 16px 24px', fontSize: 13.5, lineHeight: 1.65, color: 'var(--text-primary)' }}>
|
||||
{renderContent(item.content)}
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 12 }}>
|
||||
{item.input_tokens}↑ / {item.output_tokens}↓ tokens · {item.triggered_by}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string | null>(null)
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [genError, setGenError] = useState<string | null>(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() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AIInsightHistory refreshKey={historyKey} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue