feat: 第五轮开发 - 14项未来路线图功能完整实现
W1-W14 全部完成: - W1: 消息搜索 (ILIKE全文检索 + SearchModal) - W2: 对话导出 (JSON/Markdown/TXT三格式) - W3: 记忆时间线 DevTools 可视化 - W4: 通知推送系统 (WebSocket + Browser Notification API) - W5: 定时提醒 (30s轮询 + 重复提醒 + WebSocket推送) - W6: 每日简报 (08:00自动生成: 天气+新闻+提醒+AI摘要) - W7: IoT场景自动化 (规则引擎 10s轮询 + 条件评估 + 场景执行) - W8: 语音输入 (浏览器 Speech Recognition API) - W9: STT服务 (voice-service + whisper.cpp) - W10: TTS服务 (浏览器 Speech Synthesis + edge-tts三档回退) - W11: 文件管理 (上传/下载/缩略图/纯Go bilinear缩放) - W12: 知识库RAG (PostgreSQL tsvector + 文档分块 + 检索) - W13: 多模态 (图片上传+分析: Vision API + 本地Go分析回退) - W14: PWA (Service Worker + 离线页 + install prompt) 总计: 6个Go微服务 + 10+前端组件 + 10+ PostgreSQL表 + 4个后台调度器
This commit is contained in:
+449
-10
@@ -419,6 +419,181 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
|
||||
border-color: var(--accent) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========== 记忆时间线样式 ========== */
|
||||
.timeline-container {
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
}
|
||||
.timeline-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(180deg, var(--blue) 0%, var(--border2) 50%, #a855f7 100%);
|
||||
border-radius: 1px;
|
||||
}
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.timeline-item:hover {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -28px;
|
||||
top: 14px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
z-index: 2;
|
||||
border: 2px solid var(--border2);
|
||||
background: var(--bg2);
|
||||
transition: all .2s;
|
||||
}
|
||||
.timeline-dot.memory {
|
||||
border-color: var(--blue);
|
||||
background: var(--blue-bg);
|
||||
box-shadow: 0 0 8px rgba(59,130,246,.3);
|
||||
}
|
||||
.timeline-dot.thinking {
|
||||
border-color: #a855f7;
|
||||
background: rgba(168,85,247,.12);
|
||||
box-shadow: 0 0 8px rgba(168,85,247,.3);
|
||||
}
|
||||
.timeline-card {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.timeline-card:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.2);
|
||||
}
|
||||
.timeline-card.memory-card {
|
||||
border-left: 3px solid var(--blue);
|
||||
}
|
||||
.timeline-card.thinking-card {
|
||||
border-left: 3px solid #a855f7;
|
||||
}
|
||||
.timeline-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.timeline-card-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.timeline-card-title.memory { color: #60a5fa; }
|
||||
.timeline-card-title.thinking { color: #c084fc; }
|
||||
.timeline-card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.timeline-card-body {
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.timeline-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 10px;
|
||||
color: var(--text3);
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.timeline-importance-stars {
|
||||
color: #f59e0b;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.timeline-trigger-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.timeline-trigger-badge.scheduled { background: var(--blue-bg); color: var(--blue); }
|
||||
.timeline-trigger-badge.manual { background: var(--orange-bg); color: var(--orange); }
|
||||
.timeline-detail {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.timeline-detail.open {
|
||||
display: block;
|
||||
}
|
||||
.timeline-detail .detail-section {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.timeline-detail .detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.timeline-detail .detail-label {
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
color: var(--text3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.timeline-detail .detail-content {
|
||||
color: var(--text);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.timeline-detail .tool-call-item {
|
||||
padding: 6px 10px;
|
||||
background: var(--bg3);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
/* 筛选标签样式 */
|
||||
.timeline-filter-tab {
|
||||
padding: 4px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
background: var(--bg3);
|
||||
color: var(--text2);
|
||||
transition: all .15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.timeline-filter-tab:hover { background: var(--bg4); color: var(--text); }
|
||||
.timeline-filter-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -460,6 +635,9 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
|
||||
<button class="nav-item" data-panel="thinking">
|
||||
<span class="nav-icon">💭</span><span class="nav-label">自主思考</span>
|
||||
</button>
|
||||
<button class="nav-item" data-panel="timeline">
|
||||
<span class="nav-icon">⏱️</span><span class="nav-label">记忆时间线</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span id="ws-dot" class="disconnected"></span>
|
||||
@@ -492,6 +670,8 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
|
||||
<div class="panel" id="panel-toolCalls"></div>
|
||||
<!-- 自主思考日志 -->
|
||||
<div class="panel" id="panel-thinking"></div>
|
||||
<!-- 记忆时间线 -->
|
||||
<div class="panel" id="panel-timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -534,6 +714,12 @@ const STATE = {
|
||||
memoryFilterImportance: 0,
|
||||
memorySearchText: '',
|
||||
memoryPanelInitialized: false,
|
||||
// 时间线面板状态
|
||||
timelineData: [],
|
||||
timelineUserId: 'admin_admin',
|
||||
timelineFilterType: 'all',
|
||||
timelineAutoRefresh: null,
|
||||
timelineLimit: 100,
|
||||
};
|
||||
|
||||
// ========== WebSocket ==========
|
||||
@@ -705,7 +891,7 @@ function switchPanel(name) {
|
||||
const titles = {
|
||||
dashboard: '🏠 仪表盘', memory: '🧠 记忆管理', sessions: '💬 会话监看',
|
||||
services: '🖥 服务管理', iot: '🏠 IoT 设备控制', performance: '📊 性能监控', database: '🗄️ 数据库监看',
|
||||
toolCalls: '🔧 工具调用记录', thinking: '💭 自主思考',
|
||||
toolCalls: '🔧 工具调用记录', thinking: '💭 自主思考', timeline: '⏱️ 记忆时间线',
|
||||
};
|
||||
document.getElementById('panel-title').textContent = titles[name] || name;
|
||||
|
||||
@@ -718,15 +904,16 @@ function switchPanel(name) {
|
||||
|
||||
// 渲染面板
|
||||
switch (name) {
|
||||
case 'dashboard': renderDashboard(); startDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
|
||||
case 'memory': renderMemoryPanel(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
|
||||
case 'sessions': renderSessionsPanel(); startSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
|
||||
case 'services': renderServicesPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
|
||||
case 'iot': renderIoTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); startIoTRefresh(); break;
|
||||
case 'performance': renderPerformancePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
|
||||
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); stopIoTRefresh(); break;
|
||||
case 'toolCalls': renderToolCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
|
||||
case 'thinking': renderThinkingPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
|
||||
case 'dashboard': renderDashboard(); startDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'memory': renderMemoryPanel(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'sessions': renderSessionsPanel(); startSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'services': renderServicesPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'iot': renderIoTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); startIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'performance': renderPerformancePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'toolCalls': renderToolCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'thinking': renderThinkingPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'timeline': renderTimelinePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); startTimelineAutoRefresh(); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2628,6 +2815,258 @@ function toggleThinkingAutoRefresh(on) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 面板10: 记忆时间线 ==========
|
||||
function stopTimelineAutoRefresh() {
|
||||
if (STATE.timelineAutoRefresh) { clearInterval(STATE.timelineAutoRefresh); STATE.timelineAutoRefresh = null; }
|
||||
}
|
||||
|
||||
function startTimelineAutoRefresh() {
|
||||
stopTimelineAutoRefresh();
|
||||
STATE.timelineAutoRefresh = setInterval(function() {
|
||||
if (STATE.activePanel === 'timeline') renderTimelinePanel();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function importanceToStarsTimeline(imp) {
|
||||
var full = Math.round(imp / 2);
|
||||
var empty = 5 - full;
|
||||
return '<span style="color:#f59e0b">' + '★'.repeat(full) + '</span><span style="color:var(--text3)">' + '☆'.repeat(empty) + '</span>';
|
||||
}
|
||||
|
||||
async function renderTimelinePanel() {
|
||||
var container = document.getElementById('panel-timeline');
|
||||
if (!container) return;
|
||||
|
||||
var actionsEl = document.getElementById('panel-actions');
|
||||
var autoRefreshOn = STATE.timelineAutoRefresh !== null;
|
||||
actionsEl.innerHTML = '<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text2);cursor:pointer;">' +
|
||||
'<input type="checkbox" id="timeline-autorefresh" ' + (autoRefreshOn ? 'checked' : '') + ' onchange="toggleTimelineAutoRefresh(this.checked)">' +
|
||||
'自动刷新 (30s)</label>' +
|
||||
'<button class="btn btn-sm" onclick="renderTimelinePanel()" style="margin-left:8px">🔄 刷新</button>';
|
||||
|
||||
// 加载数据
|
||||
var userId = STATE.timelineUserId || 'admin_admin';
|
||||
var data = await api('/api/memory-timeline?user_id=' + encodeURIComponent(userId) + '&limit=' + STATE.timelineLimit);
|
||||
|
||||
if (data.error) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="icon">⚠️</div>' + escHtml(data.error) +
|
||||
(data.hint ? '<br><small>' + escHtml(data.hint) + '</small>' : '') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var timeline = data.timeline || [];
|
||||
var stats = data.stats || {};
|
||||
STATE.timelineData = timeline;
|
||||
|
||||
// 筛选
|
||||
var filtered = timeline;
|
||||
if (STATE.timelineFilterType && STATE.timelineFilterType !== 'all') {
|
||||
filtered = timeline.filter(function(item) { return item.type === STATE.timelineFilterType; });
|
||||
}
|
||||
|
||||
// 统计卡片
|
||||
var memCount = stats.total_memories || 0;
|
||||
var thinkCount = stats.total_thinking || 0;
|
||||
var latestMemTime = stats.latest_memory_time ? formatTime(stats.latest_memory_time) : '—';
|
||||
var latestThinkTime = stats.latest_thinking_time ? formatTime(stats.latest_thinking_time) : '—';
|
||||
|
||||
var statsCardsHtml = '<div class="cards-grid cards-4" style="margin-bottom:14px;">' +
|
||||
'<div class="stat-card accent"><div class="stat-value">' + memCount + '</div><div class="stat-label">🧠 总记忆数</div></div>' +
|
||||
'<div class="stat-card blue"><div class="stat-value">' + thinkCount + '</div><div class="stat-label">💭 总思考次数</div></div>' +
|
||||
'<div class="stat-card green"><div class="stat-value">' + latestMemTime + '</div><div class="stat-label">📅 最新记忆</div></div>' +
|
||||
'<div class="stat-card orange"><div class="stat-value">' + latestThinkTime + '</div><div class="stat-label">🕐 最新思考</div></div>' +
|
||||
'</div>';
|
||||
|
||||
// 筛选栏
|
||||
var allActive = STATE.timelineFilterType === 'all' ? ' active' : '';
|
||||
var memActive = STATE.timelineFilterType === 'memory' ? ' active' : '';
|
||||
var thinkActive = STATE.timelineFilterType === 'thinking' ? ' active' : '';
|
||||
|
||||
var filterHtml = '<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap;">' +
|
||||
'<span style="font-size:12px;color:var(--text2);">筛选类型:</span>' +
|
||||
'<button class="timeline-filter-tab' + allActive + '" onclick="filterTimeline(\'all\')">📋 全部</button>' +
|
||||
'<button class="timeline-filter-tab' + memActive + '" onclick="filterTimeline(\'memory\')">🧠 记忆</button>' +
|
||||
'<button class="timeline-filter-tab' + thinkActive + '" onclick="filterTimeline(\'thinking\')">💭 思考</button>' +
|
||||
'<span style="font-size:11px;color:var(--text3);margin-left:auto;">显示 ' + filtered.length + ' / ' + timeline.length + ' 条</span>' +
|
||||
'</div>';
|
||||
|
||||
// 时间线主体
|
||||
var timelineHtml = '';
|
||||
if (filtered.length === 0) {
|
||||
timelineHtml = '<div class="empty-state"><div class="icon">📭</div>暂无匹配的时间线条目</div>';
|
||||
} else {
|
||||
timelineHtml = '<div class="timeline-container">';
|
||||
for (var i = 0; i < filtered.length; i++) {
|
||||
var item = filtered[i];
|
||||
var itemId = 'tl-' + i;
|
||||
var isMemory = item.type === 'memory';
|
||||
|
||||
// 圆点图标
|
||||
var dotIcon = isMemory ? '🧠' : '💭';
|
||||
var dotClass = isMemory ? 'memory' : 'thinking';
|
||||
var cardClass = isMemory ? 'memory-card' : 'thinking-card';
|
||||
var titleClass = isMemory ? 'memory' : 'thinking';
|
||||
|
||||
// 标题
|
||||
var title = item.title || (isMemory ? '记忆' : '思考');
|
||||
|
||||
// 摘要
|
||||
var summary = '';
|
||||
if (isMemory) {
|
||||
summary = item.summary || item.content || '';
|
||||
if (summary.length > 200) summary = summary.substring(0, 197) + '...';
|
||||
} else {
|
||||
summary = item.summary || '';
|
||||
}
|
||||
|
||||
// 重要性星级
|
||||
var starsHtml = '';
|
||||
if (isMemory && item.importance) {
|
||||
starsHtml = '<span class="timeline-importance-stars">' + importanceToStarsTimeline(item.importance) + '</span>';
|
||||
}
|
||||
|
||||
// 分类标签
|
||||
var catLabel = '';
|
||||
if (isMemory && item.category) {
|
||||
var cc = getCatColor(item.category);
|
||||
catLabel = '<span style="display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:500;background:' + cc.bg + ';color:' + cc.text + ';">' + cc.icon + ' ' + cc.name + '</span>';
|
||||
}
|
||||
|
||||
// 工具调用数
|
||||
var toolCallLabel = '';
|
||||
if (!isMemory && item.tool_call_count > 0) {
|
||||
toolCallLabel = '<span style="display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:500;background:var(--accent-bg);color:var(--accent);">🔧 ' + item.tool_call_count + ' 次工具调用</span>';
|
||||
}
|
||||
|
||||
// 触发方式
|
||||
var triggerLabel = '';
|
||||
if (!isMemory) {
|
||||
var trigger = item.trigger || '定时';
|
||||
var triggerClass = trigger === '手动' ? 'manual' : 'scheduled';
|
||||
triggerLabel = '<span class="timeline-trigger-badge ' + triggerClass + '">' + (trigger === '手动' ? '👆 手动' : '⏰ 定时') + '</span>';
|
||||
}
|
||||
|
||||
// 来源
|
||||
var sourceLabel = '';
|
||||
if (isMemory) {
|
||||
var srcText = item.source === 'thinking' ? '🤔 后台思考' : item.source === 'conversation' ? '💬 对话' : '📝 ' + (item.source || '未知');
|
||||
sourceLabel = '<span>' + srcText + '</span>';
|
||||
}
|
||||
|
||||
timelineHtml += '<div class="timeline-item" id="' + itemId + '">' +
|
||||
'<div class="timeline-dot ' + dotClass + '">' + dotIcon + '</div>' +
|
||||
'<div class="timeline-card ' + cardClass + '" onclick="toggleTimelineDetail(\'' + itemId + '\')">' +
|
||||
'<div class="timeline-card-header">' +
|
||||
'<span class="timeline-card-title ' + titleClass + '">' + escHtml(title) + '</span>' +
|
||||
'<div class="timeline-card-meta">' +
|
||||
starsHtml +
|
||||
'<span style="font-size:10px;white-space:nowrap;">' + formatTime(item.timestamp) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
(summary ? '<div class="timeline-card-body">' + escHtml(summary) + '</div>' : '') +
|
||||
'<div class="timeline-card-footer">' +
|
||||
catLabel +
|
||||
toolCallLabel +
|
||||
triggerLabel +
|
||||
sourceLabel +
|
||||
(item.session_id ? '<span style="font-size:10px;color:var(--text3);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">💬 ' + escHtml((item.session_id || '').substring(0, 20)) + '</span>' : '') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// 详情展开区
|
||||
'<div class="timeline-detail" id="' + itemId + '-detail">' +
|
||||
renderTimelineDetail(item) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
timelineHtml += '</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = statsCardsHtml + filterHtml + timelineHtml;
|
||||
}
|
||||
|
||||
function renderTimelineDetail(item) {
|
||||
var html = '';
|
||||
if (item.type === 'memory') {
|
||||
// 记忆详情
|
||||
html += '<div class="detail-section">' +
|
||||
'<div class="detail-label">📝 完整内容</div>' +
|
||||
'<div class="detail-content">' + escHtml(item.content || '') + '</div>' +
|
||||
'</div>';
|
||||
if (item.keywords && item.keywords.length > 0) {
|
||||
html += '<div class="detail-section">' +
|
||||
'<div class="detail-label">🏷️ 关键词</div>' +
|
||||
'<div style="display:flex;gap:4px;flex-wrap:wrap;">' +
|
||||
item.keywords.map(function(k) { return '<span style="padding:2px 8px;background:var(--bg3);border-radius:10px;font-size:10px;">' + escHtml(k) + '</span>'; }).join('') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
html += '<div class="detail-section" style="display:flex;gap:20px;flex-wrap:wrap;">' +
|
||||
'<span><strong>重要性:</strong> ' + (item.importance || 0) + '/10</span>' +
|
||||
'<span><strong>访问次数:</strong> ' + (item.access_count || 0) + '</span>' +
|
||||
'<span><strong>来源:</strong> ' + escHtml(item.source || '未知') + '</span>' +
|
||||
'<span><strong>ID:</strong> <code style="font-size:10px;">' + escHtml((item.id || '').substring(0, 16)) + '</code></span>' +
|
||||
'</div>';
|
||||
} else {
|
||||
// 思考详情
|
||||
html += '<div class="detail-section">' +
|
||||
'<div class="detail-label">💭 思考内容</div>' +
|
||||
'<div class="detail-content">' + escHtml(item.content || '') + '</div>' +
|
||||
'</div>';
|
||||
if (item.tool_calls) {
|
||||
var toolCalls = item.tool_calls;
|
||||
try {
|
||||
if (typeof toolCalls === 'string') toolCalls = JSON.parse(toolCalls);
|
||||
} catch(e) { toolCalls = null; }
|
||||
if (toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||
html += '<div class="detail-section">' +
|
||||
'<div class="detail-label">🔧 工具调用详情 (' + toolCalls.length + ')</div>';
|
||||
for (var i = 0; i < toolCalls.length; i++) {
|
||||
var tc = toolCalls[i];
|
||||
var tcName = tc.function ? (tc.function.name || '未知') : (tc.name || '未知');
|
||||
var tcArgs = tc.function ? (tc.function.arguments || '') : (tc.arguments || '');
|
||||
if (typeof tcArgs === 'object') tcArgs = JSON.stringify(tcArgs);
|
||||
html += '<div class="tool-call-item">' +
|
||||
'<strong style="color:var(--accent2);">' + escHtml(tcName) + '</strong>' +
|
||||
'<pre style="font-size:10px;margin:4px 0 0;white-space:pre-wrap;color:var(--text2);">' + escHtml(String(tcArgs).slice(0, 300)) + '</pre>' +
|
||||
'</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
html += '<div class="detail-section" style="display:flex;gap:20px;flex-wrap:wrap;">' +
|
||||
'<span><strong>内容长度:</strong> ' + (item.content_length || 0) + ' 字符</span>' +
|
||||
'<span><strong>工具调用数:</strong> ' + (item.tool_call_count || 0) + '</span>' +
|
||||
'<span><strong>触发方式:</strong> ' + escHtml(item.trigger || '定时') + '</span>' +
|
||||
'<span><strong>ID:</strong> <code style="font-size:10px;">' + escHtml((item.id || '').substring(0, 16)) + '</code></span>' +
|
||||
'</div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function toggleTimelineDetail(itemId) {
|
||||
var detail = document.getElementById(itemId + '-detail');
|
||||
if (detail) {
|
||||
detail.classList.toggle('open');
|
||||
}
|
||||
}
|
||||
|
||||
function filterTimeline(type) {
|
||||
STATE.timelineFilterType = type;
|
||||
renderTimelinePanel();
|
||||
}
|
||||
|
||||
function toggleTimelineAutoRefresh(on) {
|
||||
if (STATE.timelineAutoRefresh) {
|
||||
clearInterval(STATE.timelineAutoRefresh);
|
||||
STATE.timelineAutoRefresh = null;
|
||||
}
|
||||
if (on) {
|
||||
STATE.timelineAutoRefresh = setInterval(function() {
|
||||
if (STATE.activePanel === 'timeline') renderTimelinePanel();
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="iot-panel.js"></script>
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user