@@ -11,36 +11,79 @@ import { useToast } from '@panwatch/base-ui/components/ui/toast'
1111interface HistoryRecord {
1212 id : number
1313 agent_name : string
14+ agent_kind ?: 'workflow' | 'capability'
1415 stock_symbol : string
1516 analysis_date : string
1617 title : string
1718 content : string
19+ context_payload ?: Record < string , unknown > | null
20+ prompt_context ?: string | null
21+ prompt_stats ?: Record < string , unknown > | null
22+ news_debug ?: Record < string , unknown > | null
1823 created_at : string
1924 updated_at : string
2025}
2126
2227const AGENT_LABELS : Record < string , string > = {
23- daily_report : '盘后日报 ' ,
28+ daily_report : '收盘复盘 ' ,
2429 premarket_outlook : '盘前分析' ,
2530 intraday_monitor : '盘中监测' ,
2631 news_digest : '新闻速递' ,
2732 chart_analyst : '技术分析' ,
2833}
2934
35+ const WORKFLOW_AGENT_KEYS = [ 'daily_report' , 'premarket_outlook' , 'intraday_monitor' ]
36+ const CAPABILITY_AGENT_KEYS = [ 'news_digest' , 'chart_analyst' ]
37+
3038export default function HistoryPage ( ) {
3139 const { toast } = useToast ( )
3240 const [ records , setRecords ] = useState < HistoryRecord [ ] > ( [ ] )
3341 const [ loading , setLoading ] = useState ( true )
3442 const [ selectedAgent , setSelectedAgent ] = useState < string > ( 'all' )
43+ const [ historyKind , setHistoryKind ] = useState < 'workflow' | 'capability' | 'all' > ( 'workflow' )
3544 const [ selectedId , setSelectedId ] = useState < number | null > ( null )
3645 const [ mobileView , setMobileView ] = useState < 'list' | 'reader' > ( 'list' )
3746 const [ detailRecord , setDetailRecord ] = useState < HistoryRecord | null > ( null )
3847
48+ const displayTime = ( record : HistoryRecord ) => record . updated_at || record . created_at
49+ const formatDateTime = ( iso ?: string ) => {
50+ if ( ! iso ) return '--'
51+ const s = String ( iso ) . trim ( )
52+ if ( ! s ) return '--'
53+ // Keep original offset semantics; only normalize display format and strip fractional seconds.
54+ let normalized = s . replace ( ' ' , 'T' ) . replace ( / Z $ / , '+00:00' )
55+ normalized = normalized . replace ( / \. \d + (? = [ + - ] \d { 2 } : \d { 2 } $ ) / , '' )
56+ if ( / ^ \d { 4 } - \d { 2 } - \d { 2 } T \d { 2 } : \d { 2 } : \d { 2 } [ + - ] \d { 2 } : \d { 2 } $ / . test ( normalized ) ) {
57+ return normalized
58+ }
59+ const d = new Date ( s )
60+ if ( isNaN ( d . getTime ( ) ) ) return s
61+ const pad = ( n : number ) => String ( n ) . padStart ( 2 , '0' )
62+ const year = d . getFullYear ( )
63+ const month = pad ( d . getMonth ( ) + 1 )
64+ const day = pad ( d . getDate ( ) )
65+ const hour = pad ( d . getHours ( ) )
66+ const minute = pad ( d . getMinutes ( ) )
67+ const second = pad ( d . getSeconds ( ) )
68+ const tz = - d . getTimezoneOffset ( )
69+ const sign = tz >= 0 ? '+' : '-'
70+ const tzHour = pad ( Math . floor ( Math . abs ( tz ) / 60 ) )
71+ const tzMinute = pad ( Math . abs ( tz ) % 60 )
72+ return `${ year } -${ month } -${ day } T${ hour } :${ minute } :${ second } ${ sign } ${ tzHour } :${ tzMinute } `
73+ }
74+
75+ const formatTimeShort = ( iso ?: string ) => {
76+ const full = formatDateTime ( iso )
77+ const m = full . match ( / T ( \d { 2 } : \d { 2 } ) : \d { 2 } [ + - ] \d { 2 } : \d { 2 } $ / )
78+ return m ? m [ 1 ] : '--:--'
79+ }
80+
3981 const load = async ( ) => {
4082 setLoading ( true )
4183 try {
4284 const params = new URLSearchParams ( )
4385 if ( selectedAgent && selectedAgent !== 'all' ) params . set ( 'agent_name' , selectedAgent )
86+ params . set ( 'kind' , historyKind )
4487 params . set ( 'limit' , '50' )
4588 const data = await fetchAPI < HistoryRecord [ ] > ( `/history?${ params . toString ( ) } ` )
4689 setRecords ( data || [ ] )
@@ -51,7 +94,18 @@ export default function HistoryPage() {
5194 }
5295 }
5396
54- useEffect ( ( ) => { load ( ) } , [ selectedAgent ] )
97+ useEffect ( ( ) => { load ( ) } , [ selectedAgent , historyKind ] )
98+
99+ useEffect ( ( ) => {
100+ const available = historyKind === 'workflow'
101+ ? WORKFLOW_AGENT_KEYS
102+ : historyKind === 'capability'
103+ ? CAPABILITY_AGENT_KEYS
104+ : [ ...WORKFLOW_AGENT_KEYS , ...CAPABILITY_AGENT_KEYS ]
105+ if ( selectedAgent !== 'all' && ! available . includes ( selectedAgent ) ) {
106+ setSelectedAgent ( 'all' )
107+ }
108+ } , [ historyKind , selectedAgent ] )
55109
56110 useEffect ( ( ) => {
57111 if ( ! records . length ) {
@@ -84,6 +138,11 @@ export default function HistoryPage() {
84138 }
85139
86140 const selectedRecord = selectedId ? records . find ( r => r . id === selectedId ) || null : null
141+ const agentOptions = historyKind === 'workflow'
142+ ? WORKFLOW_AGENT_KEYS
143+ : historyKind === 'capability'
144+ ? CAPABILITY_AGENT_KEYS
145+ : [ ...WORKFLOW_AGENT_KEYS , ...CAPABILITY_AGENT_KEYS ]
87146
88147 const selectRecord = ( id : number ) => {
89148 setSelectedId ( id )
@@ -112,17 +171,29 @@ export default function HistoryPage() {
112171 </ div >
113172 </ div >
114173
115- < Select value = { selectedAgent } onValueChange = { setSelectedAgent } >
116- < SelectTrigger className = "w-full sm:w-[180px] h-9" >
117- < SelectValue placeholder = "全部 Agent" />
118- </ SelectTrigger >
119- < SelectContent >
120- < SelectItem value = "all" > 全部 Agent</ SelectItem >
121- { Object . entries ( AGENT_LABELS ) . map ( ( [ key , label ] ) => (
122- < SelectItem key = { key } value = { key } > { label } </ SelectItem >
123- ) ) }
124- </ SelectContent >
125- </ Select >
174+ < div className = "flex items-center gap-2" >
175+ < Select value = { historyKind } onValueChange = { ( v ) => setHistoryKind ( v as 'workflow' | 'capability' | 'all' ) } >
176+ < SelectTrigger className = "w-full sm:w-[150px] h-9" >
177+ < SelectValue placeholder = "历史范围" />
178+ </ SelectTrigger >
179+ < SelectContent >
180+ < SelectItem value = "workflow" > 主流程</ SelectItem >
181+ < SelectItem value = "capability" > 能力层</ SelectItem >
182+ < SelectItem value = "all" > 全部</ SelectItem >
183+ </ SelectContent >
184+ </ Select >
185+ < Select value = { selectedAgent } onValueChange = { setSelectedAgent } >
186+ < SelectTrigger className = "w-full sm:w-[180px] h-9" >
187+ < SelectValue placeholder = "全部 Agent" />
188+ </ SelectTrigger >
189+ < SelectContent >
190+ < SelectItem value = "all" > 全部 Agent</ SelectItem >
191+ { agentOptions . map ( ( key ) => (
192+ < SelectItem key = { key } value = { key } > { AGENT_LABELS [ key ] || key } </ SelectItem >
193+ ) ) }
194+ </ SelectContent >
195+ </ Select >
196+ </ div >
126197 </ div >
127198
128199 { loading ? (
@@ -177,7 +248,7 @@ export default function HistoryPage() {
177248 </ div >
178249 < div className = "mt-1 flex items-center justify-between text-[11px] text-muted-foreground" >
179250 < span className = "font-mono" > { r . analysis_date } </ span >
180- < span > { new Date ( r . created_at ) . toLocaleTimeString ( 'zh-CN' , { hour : '2-digit' , minute : '2-digit' } ) } </ span >
251+ < span > { formatTimeShort ( displayTime ( r ) ) } </ span >
181252 </ div >
182253 </ button >
183254 )
@@ -202,7 +273,7 @@ export default function HistoryPage() {
202273 目录
203274 </ Button >
204275 < Badge variant = "outline" className = "text-[10px]" > { AGENT_LABELS [ selectedRecord . agent_name ] || selectedRecord . agent_name } </ Badge >
205- < span className = "text-[11px] text-muted-foreground font-mono" > { selectedRecord . created_at } </ span >
276+ < span className = "text-[11px] text-muted-foreground font-mono" > { formatDateTime ( displayTime ( selectedRecord ) ) } </ span >
206277 </ div >
207278 < div className = "mt-1 text-[15px] md:text-[16px] font-semibold text-foreground truncate" >
208279 { formatTitle ( selectedRecord ) }
@@ -249,6 +320,30 @@ export default function HistoryPage() {
249320 < div className = "mt-4 p-4 bg-accent/20 rounded-lg prose prose-sm dark:prose-invert max-w-none" >
250321 { detailRecord && < ReactMarkdown > { detailRecord . content } </ ReactMarkdown > }
251322 </ div >
323+ { detailRecord ?. prompt_stats ? (
324+ < div className = "mt-3 rounded-lg border border-border/50 p-3" >
325+ < div className = "text-[12px] font-medium mb-1" > Prompt 统计</ div >
326+ < pre className = "text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto" > { JSON . stringify ( detailRecord . prompt_stats , null , 2 ) } </ pre >
327+ </ div >
328+ ) : null }
329+ { detailRecord ?. context_payload ? (
330+ < div className = "mt-3 rounded-lg border border-border/50 p-3" >
331+ < div className = "text-[12px] font-medium mb-1" > 上下文快照</ div >
332+ < pre className = "text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto max-h-[280px] overflow-y-auto" > { JSON . stringify ( detailRecord . context_payload , null , 2 ) } </ pre >
333+ </ div >
334+ ) : null }
335+ { detailRecord ?. news_debug ? (
336+ < div className = "mt-3 rounded-lg border border-border/50 p-3" >
337+ < div className = "text-[12px] font-medium mb-1" > 新闻注入明细</ div >
338+ < pre className = "text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto" > { JSON . stringify ( detailRecord . news_debug , null , 2 ) } </ pre >
339+ </ div >
340+ ) : null }
341+ { detailRecord ?. prompt_context ? (
342+ < div className = "mt-3 rounded-lg border border-border/50 p-3" >
343+ < div className = "text-[12px] font-medium mb-1" > Prompt 原文</ div >
344+ < pre className = "text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto max-h-[280px] overflow-y-auto" > { detailRecord . prompt_context } </ pre >
345+ </ div >
346+ ) : null }
252347 </ DialogContent >
253348 </ Dialog >
254349 </ div >
0 commit comments