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
|
|
@ -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