import { useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import { CalendarDays, Code, Copy, MessageSquare, RefreshCw, Search, Sparkles, X } from 'lucide-react' import { Avatar } from '../components/Avatar' import type { InsightRecord, InsightRecordContactFacet, InsightRecordFilters, InsightRecordListResult, InsightRecordSourceType, InsightRecordSummary, InsightRecordTriggerReason } from '../types/electron' import './InsightInboxPage.scss' const INSIGHT_AVATAR_URL = './assets/insight/AI_Insight.png' type DateFilterMode = 'all' | 'today' | 'week' | 'custom' type SourceFilterMode = InsightRecordSourceType | 'all' function getStartOfDay(date: Date): number { const next = new Date(date) next.setHours(0, 0, 0, 0) return next.getTime() } function getEndOfDay(date: Date): number { const next = new Date(date) next.setHours(23, 59, 59, 999) return next.getTime() } function formatDateInput(date: Date): string { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function parseDateInput(value: string, endOfDay = false): number | undefined { if (!value) return undefined const date = new Date(`${value}T00:00:00`) if (Number.isNaN(date.getTime())) return undefined return endOfDay ? getEndOfDay(date) : getStartOfDay(date) } function formatRecordTime(timestamp: number): string { return new Date(timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) } function formatGroupDate(timestamp: number): string { const date = new Date(timestamp) const today = new Date() const yesterday = new Date() yesterday.setDate(today.getDate() - 1) if (getStartOfDay(date) === getStartOfDay(today)) return '今天' if (getStartOfDay(date) === getStartOfDay(yesterday)) return '昨天' return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) } function getTriggerLabel(reason: InsightRecordTriggerReason): string { if (reason === 'message_analysis') return '深度解析' if (reason === 'silence') return '沉默提醒' if (reason === 'test') return '测试见解' if (reason === 'manual') return '手动触发' return '活跃分析' } function getSourceLabel(sourceType?: InsightRecordSourceType): string { return sourceType === 'message_analysis' ? '深度解析' : 'AI 见解' } function buildLogText(record: InsightRecord): string { const log = record.log const lines = [ `时间:${new Date(record.createdAt).toLocaleString('zh-CN')}`, `联系人:${record.displayName} (${record.sessionId})`, `来源:${getSourceLabel(record.sourceType)}`, `触发类型:${getTriggerLabel(record.triggerReason)}`, `接口地址:${log.endpoint}`, `模型:${log.model}`, `Max Tokens:${log.maxTokens}`, `Temperature:${log.temperature}`, `耗时:${log.durationMs}ms`, '', '系统提示词:', log.systemPrompt, '', '用户提示词:', log.userPrompt, '', '模型输出原文:', log.rawOutput, '', '最终见解:', log.finalInsight ] if (record.sourceType === 'message_analysis') { lines.splice(8, 0, `JSON Mode:${log.responseFormatJson ? '启用' : '未启用'}`, `JSON Mode 降级:${log.responseFormatFallback ? '是' : '否'}`, `降级原因:${log.responseFormatFallbackReason || '无'}`, `上下文:请求 ${log.contextStats?.requested ?? log.contextCount} 条,前 ${log.contextStats?.beforeTarget ?? 0} 条,后 ${log.contextStats?.afterTarget ?? 0} 条`, `上下文读取异常:${log.contextStats?.readError || '无'}` ) lines.splice(4, 0, `目标消息:${record.messageInsight?.targetSenderName || log.targetMessage?.senderName || ''}:${record.messageInsight?.targetTextPreview || log.targetMessage?.textPreview || ''}`, `目标定位:localId=${record.messageInsight?.targetLocalId || log.targetMessage?.localId || 0}, createTime=${record.messageInsight?.targetCreateTime || log.targetMessage?.createTime || 0}, key=${record.messageInsight?.targetMessageKey || log.targetMessage?.messageKey || ''}` ) } return lines.join('\n') } export default function InsightInboxPage() { const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const [records, setRecords] = useState([]) const [contacts, setContacts] = useState([]) const [keyword, setKeyword] = useState('') const [contactSearch, setContactSearch] = useState('') const [selectedSessionId, setSelectedSessionId] = useState('') const [sourceType, setSourceType] = useState('all') const [dateMode, setDateMode] = useState('all') const [customStart, setCustomStart] = useState(formatDateInput(new Date())) const [customEnd, setCustomEnd] = useState(formatDateInput(new Date())) const [stats, setStats] = useState({ total: 0, todayCount: 0, unreadCount: 0 }) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [focusedRecordId, setFocusedRecordId] = useState(searchParams.get('recordId') || '') const [logRecord, setLogRecord] = useState(null) const [message, setMessage] = useState('') const dateRange = useMemo(() => { const now = new Date() if (dateMode === 'today') { return { startTime: getStartOfDay(now), endTime: getEndOfDay(now) } } if (dateMode === 'week') { const start = new Date(now) start.setDate(now.getDate() - 6) return { startTime: getStartOfDay(start), endTime: getEndOfDay(now) } } if (dateMode === 'custom') { return { startTime: parseDateInput(customStart), endTime: parseDateInput(customEnd, true) } } return {} }, [customEnd, customStart, dateMode]) const filters = useMemo(() => ({ keyword: keyword.trim() || undefined, sessionId: selectedSessionId || undefined, sourceType, startTime: dateRange.startTime, endTime: dateRange.endTime, limit: 200, offset: 0 }), [dateRange.endTime, dateRange.startTime, keyword, selectedSessionId, sourceType]) const loadRecords = useCallback(async () => { setLoading(true) setError('') try { const result: InsightRecordListResult = await window.electronAPI.insight.listRecords(filters) if (!result.success) { setError(result.error || '加载灵感信箱失败') return } setRecords(result.records) setContacts(result.contacts) setStats({ total: result.total, todayCount: result.todayCount, unreadCount: result.unreadCount }) } catch (err) { setError((err as Error).message || '加载灵感信箱失败') } finally { setLoading(false) } }, [filters]) useEffect(() => { void loadRecords() }, [loadRecords]) useEffect(() => { const recordId = searchParams.get('recordId') || '' if (!recordId) return setFocusedRecordId(recordId) window.setTimeout(() => { document.getElementById(`insight-record-${recordId}`)?.scrollIntoView({ block: 'center', behavior: 'smooth' }) }, 120) void window.electronAPI.insight.markRecordRead(recordId) }, [searchParams]) const groupedRecords = useMemo(() => { const groups: Array<{ label: string; records: InsightRecordSummary[] }> = [] for (const record of records) { const label = formatGroupDate(record.createdAt) const last = groups[groups.length - 1] if (last?.label === label) { last.records.push(record) } else { groups.push({ label, records: [record] }) } } return groups }, [records]) const filteredContacts = useMemo(() => { const normalized = contactSearch.trim().toLowerCase() if (!normalized) return contacts return contacts.filter((contact) => { const text = `${contact.displayName}\n${contact.sessionId}`.toLowerCase() return text.includes(normalized) }) }, [contactSearch, contacts]) const openChat = (record: InsightRecordSummary) => { if (record.sourceType === 'message_analysis' && record.messageInsight) { const query = new URLSearchParams({ sessionId: record.sessionId, jumpSource: 'messageAnalysis', jumpLocalId: String(record.messageInsight.targetLocalId || 0), jumpCreateTime: String(record.messageInsight.targetCreateTime || 0) }) navigate(`/chat?${query.toString()}`) return } navigate(`/chat?sessionId=${encodeURIComponent(record.sessionId)}`) } const copyText = async (text: string, successText: string) => { try { await navigator.clipboard.writeText(text) setMessage(successText) window.setTimeout(() => setMessage(''), 1800) } catch { setMessage('复制失败') window.setTimeout(() => setMessage(''), 1800) } } const openLog = async (recordId: string) => { const result = await window.electronAPI.insight.getRecord(recordId) if (!result.success || !result.record) { setMessage(result.error || '读取请求日志失败') window.setTimeout(() => setMessage(''), 1800) return } setLogRecord(result.record) void window.electronAPI.insight.markRecordRead(recordId) setRecords((prev) => prev.map((record) => record.id === recordId ? { ...record, read: true } : record)) } const clearFocusedRecord = () => { setFocusedRecordId('') searchParams.delete('recordId') setSearchParams(searchParams, { replace: true }) } return (

灵感信箱

共 {stats.total} 条 今天 {stats.todayCount} 条 未读 {stats.unreadCount} 条
{focusedRecordId && (
已定位通知中的见解
)}
{error && (
{error}
)} {!error && loading && records.length === 0 && (
正在加载灵感信箱...
)} {!error && !loading && records.length === 0 && (
暂无见解 AI 见解生成后会自动保存在这里。
)} {groupedRecords.map((group) => (
{group.label}
{group.records.map((record) => (
发给 {record.displayName} {record.sessionId}
{getSourceLabel(record.sourceType)} {getTriggerLabel(record.triggerReason)} {formatRecordTime(record.createdAt)}
{record.sourceType === 'message_analysis' && record.messageInsight && (
目标消息 {record.messageInsight.targetSenderName}:{record.messageInsight.targetTextPreview}
)}

{record.insight}

{record.sourceType === 'message_analysis' && record.messageInsight && (
情绪:{record.messageInsight.analysis.emotion} 意图:{record.messageInsight.analysis.intent} 话题:{record.messageInsight.analysis.topic}
)}
))}
))}
{logRecord && (
setLogRecord(null)}>
event.stopPropagation()}>

请求日志

{logRecord.displayName} · {formatRecordTime(logRecord.createdAt)}

请求参数

{[
                  `Endpoint: ${logRecord.log.endpoint}`,
                  `Model: ${logRecord.log.model}`,
                  `Max Tokens: ${logRecord.log.maxTokens}`,
                  `Temperature: ${logRecord.log.temperature}`,
                  `Duration: ${logRecord.log.durationMs}ms`,
                  `Source: ${getSourceLabel(logRecord.sourceType)}`,
                  `Trigger: ${getTriggerLabel(logRecord.triggerReason)}`,
                  ...(logRecord.sourceType === 'message_analysis'
                    ? [
                        `JSON Mode: ${logRecord.log.responseFormatJson ? 'enabled' : 'disabled'}`,
                        `JSON Fallback: ${logRecord.log.responseFormatFallback ? 'yes' : 'no'}`,
                        `Fallback Reason: ${logRecord.log.responseFormatFallbackReason || 'none'}`
                      ]
                    : [])
                ].join('\n')}
{logRecord.sourceType === 'message_analysis' && (

深度解析目标

{[
                    `Sender: ${logRecord.messageInsight?.targetSenderName || logRecord.log.targetMessage?.senderName || ''}`,
                    `Preview: ${logRecord.messageInsight?.targetTextPreview || logRecord.log.targetMessage?.textPreview || ''}`,
                    `LocalId: ${logRecord.messageInsight?.targetLocalId || logRecord.log.targetMessage?.localId || 0}`,
                    `CreateTime: ${logRecord.messageInsight?.targetCreateTime || logRecord.log.targetMessage?.createTime || 0}`,
                    `MessageKey: ${logRecord.messageInsight?.targetMessageKey || logRecord.log.targetMessage?.messageKey || ''}`,
                    `Context Requested: ${logRecord.log.contextStats?.requested ?? logRecord.log.contextCount}`,
                    `Context Before: ${logRecord.log.contextStats?.beforeTarget ?? 0}`,
                    `Context After: ${logRecord.log.contextStats?.afterTarget ?? 0}`,
                    `Context Error: ${logRecord.log.contextStats?.readError || 'none'}`
                  ].join('\n')}
)} {logRecord.sourceType === 'message_analysis' && logRecord.log.parsedAnalysis && (

解析字段

{[
                    `explicitText: ${logRecord.log.parsedAnalysis.explicitText}`,
                    `emotion: ${logRecord.log.parsedAnalysis.emotion}`,
                    `intent: ${logRecord.log.parsedAnalysis.intent}`,
                    `topic: ${logRecord.log.parsedAnalysis.topic}`
                  ].join('\n')}
)}

System Prompt

{logRecord.log.systemPrompt}

User Prompt

{logRecord.log.userPrompt}

模型输出

{logRecord.log.rawOutput}

最终见解

{logRecord.log.finalInsight}
)} {message &&
{message}
}
) }