/** * TalkingQ Platform - Session Detail Page */ const SESSION_TOPIC_DRILLDOWN_KEY = 'talkingq_session_topic_drilldown'; const SESSION_VIEW_STATE_KEY = 'talkingq_session_view_state'; const SESSION_DEFAULT_DATE = '2026-05-25'; const SESSION_TOPIC_OPTIONS = ['讲故事', '唱儿歌', '问答学习', '天气查询', '闹钟设置', '游戏互动', '英语学习', '百科问答', '情感陪伴', '其他']; function getMockSessionData() { const rows = [ ['sess-a251', 'company_a', '智慧星科技', 'DEV-A001', '小Q故事机器人', '2026-05-25 14:22', '讲故事', 12, '8m 32s', 'good'], ['sess-a252', 'company_a', '智慧星科技', 'DEV-A001', '小Q故事机器人', '2026-05-25 10:12', '百科问答', 8, '5m 28s', 'good'], ['sess-a253', 'company_a', '智慧星科技', 'DEV-A005', '英语学习助手', '2026-05-25 13:48', '英语学习', 7, '5m 10s', 'good'], ['sess-a254', 'company_a', '智慧星科技', 'DEV-A008', '小Q音乐伙伴', '2026-05-25 13:08', '唱儿歌', 10, '6m 48s', 'good'], ['sess-a255', 'company_a', '智慧星科技', 'DEV-A011', '生活助手', '2026-05-25 12:36', '天气查询', 6, '4m 12s', 'neutral'], ['sess-a256', 'company_a', '智慧星科技', 'DEV-A018', '设备小助手', '2026-05-25 11:52', '闹钟设置', 4, '2m 20s', 'good'], ['sess-a257', 'company_a', '智慧星科技', 'DEV-A021', '小Q游戏伙伴', '2026-05-25 11:18', '游戏互动', 15, '11m 05s', 'good'], ['sess-a258', 'company_a', '智慧星科技', 'DEV-A026', '陪伴机器人', '2026-05-25 10:40', '情感陪伴', 8, '5m 44s', 'good'], ['sess-a241', 'company_a', '智慧星科技', 'DEV-A001', '小Q故事机器人', '2026-05-24 16:06', '讲故事', 9, '6m 16s', 'good'], ['sess-a242', 'company_a', '智慧星科技', 'DEV-A005', '英语学习助手', '2026-05-24 14:20', '问答学习', 6, '4m 38s', 'neutral'], ['sess-a231', 'company_a', '智慧星科技', 'DEV-A021', '小Q游戏伙伴', '2026-05-23 19:42', '游戏互动', 13, '9m 52s', 'good'], ['sess-b251', 'company_b', '未来玩具厂', 'DEV-B002', '学习机助手', '2026-05-25 14:05', '问答学习', 3, '1m 45s', 'bad'], ['sess-b252', 'company_b', '未来玩具厂', 'DEV-B004', '科学百科探索者', '2026-05-25 13:42', '百科问答', 9, '7m 03s', 'neutral'], ['sess-b253', 'company_b', '未来玩具厂', 'DEV-B008', '小Q故事机器人', '2026-05-25 13:18', '讲故事', 11, '8m 15s', 'good'], ['sess-b254', 'company_b', '未来玩具厂', 'DEV-B015', '小Q音乐伙伴', '2026-05-25 12:46', '唱儿歌', 8, '4m 50s', 'good'], ['sess-b255', 'company_b', '未来玩具厂', 'DEV-B019', '小Q游戏伙伴', '2026-05-25 11:56', '游戏互动', 14, '10m 22s', 'good'], ['sess-b256', 'company_b', '未来玩具厂', 'DEV-B024', '生活助手', '2026-05-25 11:20', '天气查询', 5, '3m 06s', 'neutral'], ['sess-b241', 'company_b', '未来玩具厂', 'DEV-B008', '小Q故事机器人', '2026-05-24 15:32', '讲故事', 10, '7m 30s', 'good'], ['sess-b242', 'company_b', '未来玩具厂', 'DEV-B004', '科学百科探索者', '2026-05-24 12:14', '百科问答', 7, '5m 48s', 'good'], ['sess-c251', 'company_c', '童趣电子', 'DEV-C001', '小Q故事机器人', '2026-05-25 13:55', '讲故事', 18, '15m 20s', 'good'], ['sess-c252', 'company_c', '童趣电子', 'DEV-C006', '小Q音乐伙伴', '2026-05-25 13:08', '唱儿歌', 9, '5m 35s', 'good'], ['sess-c253', 'company_c', '童趣电子', 'DEV-C012', '学习机助手', '2026-05-25 12:32', '问答学习', 10, '6m 18s', 'good'], ['sess-c254', 'company_c', '童趣电子', 'DEV-C018', '英语学习助手', '2026-05-25 11:54', '英语学习', 6, '4m 27s', 'neutral'], ['sess-c255', 'company_c', '童趣电子', 'DEV-C022', '科学百科探索者', '2026-05-25 11:16', '百科问答', 12, '9m 11s', 'good'], ['sess-c256', 'company_c', '童趣电子', 'DEV-C027', '陪伴机器人', '2026-05-25 10:45', '情感陪伴', 7, '4m 58s', 'good'], ['sess-c257', 'company_c', '童趣电子', 'DEV-C031', '小Q助手', '2026-05-25 10:20', '其他', 2, '1m 02s', 'neutral'], ['sess-c241', 'company_c', '童趣电子', 'DEV-C001', '小Q故事机器人', '2026-05-24 18:18', '讲故事', 16, '13m 44s', 'good'], ['sess-c242', 'company_c', '童趣电子', 'DEV-C012', '学习机助手', '2026-05-24 09:48', '问答学习', 9, '6m 01s', 'good'], ]; return rows.map(([id, companyId, companyName, device, agent, start, intent, turns, duration, quality]) => ({ id, companyId, companyName, device, agent, start, date: start.slice(0, 10), intent, turns, duration, quality, })); } function readSessionTopicDrilldown() { try { const raw = sessionStorage.getItem(SESSION_TOPIC_DRILLDOWN_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed?.topic) return null; if (parsed.createdAt && Date.now() - parsed.createdAt > 30 * 60 * 1000) { sessionStorage.removeItem(SESSION_TOPIC_DRILLDOWN_KEY); return null; } return parsed; } catch (error) { sessionStorage.removeItem(SESSION_TOPIC_DRILLDOWN_KEY); return null; } } function readSessionViewState() { try { return JSON.parse(sessionStorage.getItem(SESSION_VIEW_STATE_KEY) || '{}') || {}; } catch (error) { return {}; } } function updateSessionViewState(patch) { const next = { ...readSessionViewState(), ...patch }; sessionStorage.setItem(SESSION_VIEW_STATE_KEY, JSON.stringify(next)); } function setSessionTopicDrilldown(topic, context = {}) { if (!topic) { sessionStorage.removeItem(SESSION_TOPIC_DRILLDOWN_KEY); return; } sessionStorage.setItem(SESSION_TOPIC_DRILLDOWN_KEY, JSON.stringify({ topic, companyId: context.companyId || 'all', companyName: context.companyName || '全部公司', source: context.source || 'session-filter', createdAt: Date.now(), })); } function getSessionCurrentCompanyId() { const currentRole = window.__getUserRole ? window.__getUserRole() : 'admin'; const currentCompanyId = window.__getCurrentCompanyId ? window.__getCurrentCompanyId() : 'all'; if (currentRole !== 'client') return currentCompanyId; if (currentCompanyId !== 'all') return currentCompanyId; try { const user = JSON.parse(sessionStorage.getItem('talkingq_user') || 'null'); return user?.companyId || user?.allowedCompanies?.[0] || currentCompanyId; } catch (error) { return currentCompanyId; } } function getSessionCompanyName(companyId, fallback = '当前公司') { if (companyId === 'all') return '全部公司'; return window.__getCompanyNameById ? window.__getCompanyNameById(companyId) : fallback; } function sessionMatchesTopic(session, topic) { if (!topic) return true; const intent = String(session.intent || ''); if (intent === topic) return true; if (intent.includes(topic) || topic.includes(intent)) return true; const aliases = { '讲故事': ['故事', '睡前故事', '绘本'], '唱儿歌': ['儿歌', '音乐', '唱歌'], '问答学习': ['问答', '学习', '作业'], '天气查询': ['天气'], '闹钟设置': ['闹钟', '提醒'], '游戏互动': ['游戏', '互动'], '英语学习': ['英语', '英文'], '百科问答': ['百科', '科普'], '情感陪伴': ['陪伴', '情感', '聊天'], '其他': ['其他'], }; return (aliases[topic] || []).some(keyword => intent.includes(keyword)); } function sessionEscapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[char])); } function sessionEscapeAttr(value) { return sessionEscapeHtml(value).replace(/"/g, '"'); } function formatSessionDateLabel(date) { const [year, month, day] = String(date || '').split('-'); return year && month && day ? `${year}.${month}.${day}` : date; } function getSessionQualityText(quality) { if (quality === 'good') return '优质'; if (quality === 'bad') return '待优化'; return '正常'; } function getSessionQualityBadge(quality) { if (quality === 'good') return 'badge-success'; if (quality === 'bad') return 'badge-danger'; return 'badge-default'; } function getTopicOptionsForCompany(sessions) { const seen = new Set(sessions.map(item => item.intent)); return SESSION_TOPIC_OPTIONS.filter(topic => seen.has(topic)); } function getCompanyDevices(companySessions, daySessions) { const byDevice = new Map(); companySessions.forEach(session => { if (!byDevice.has(session.device)) { byDevice.set(session.device, { device: session.device, companyId: session.companyId, companyName: session.companyName, agent: session.agent, sessions: [], }); } }); daySessions.forEach(session => { if (!byDevice.has(session.device)) { byDevice.set(session.device, { device: session.device, companyId: session.companyId, companyName: session.companyName, agent: session.agent, sessions: [], }); } byDevice.get(session.device).sessions.push(session); }); return Array.from(byDevice.values()).map(item => { const topics = Array.from(new Set(item.sessions.map(session => session.intent))); const turns = item.sessions.reduce((sum, session) => sum + session.turns, 0); const lastSession = item.sessions.slice().sort((a, b) => b.start.localeCompare(a.start))[0]; return { ...item, topics, turns, count: item.sessions.length, lastSession, }; }).sort((a, b) => b.count - a.count || a.device.localeCompare(b.device)); } function getConversationLines(topic) { const samples = { '讲故事': ['小Q,给我讲一个关于恐龙的故事吧!', '好的!从前,在一片茂密的丛林里,住着一只叫做小雷的霸王龙宝宝……', '霸王龙是最厉害的恐龙吗?', '霸王龙很厉害,但恐龙世界里还有很多不同种类的大家伙。我们可以一起认识它们。'], '唱儿歌': ['小Q,唱一首上学路上的儿歌。', '当然可以,我会用轻快一点的节奏来唱。', '再唱一首动物相关的。', '好的,这次我们唱一首关于小兔子的儿歌。'], '问答学习': ['小Q,为什么月亮会变圆又变弯?', '这是因为月亮围着地球转,我们看到被太阳照亮的部分会变化。', '那满月是什么时候?', '当我们看到月亮被照亮的一面几乎完整朝向地球时,就是满月。'], '天气查询': ['小Q,今天要带雨伞吗?', '今天下午可能有小雨,建议带一把轻便雨伞。', '那适合出去玩吗?', '可以短时间外出,尽量避开下午降雨时段。'], '闹钟设置': ['小Q,明天早上七点叫我起床。', '好的,已为你设置明天早上七点的起床提醒。', '再加一个七点十分。', '已添加七点十分的第二个提醒。'], '游戏互动': ['小Q,我们玩猜动物吧。', '好,我来描述一个动物,你来猜。它有长长的鼻子和大大的耳朵。', '是大象!', '答对了!下一题要不要难一点?'], '英语学习': ['How do I say 小熊 in English?', '小熊 can be said as little bear or teddy bear, depending on context.', 'Can you make a sentence?', 'Sure. The little bear is sleeping under the tree.'], '百科问答': ['小Q,火山为什么会喷发?', '地下的岩浆和气体压力变大时,会沿着裂缝冲出来,就形成了火山喷发。', '岩浆冷了会变成什么?', '岩浆冷却后会变成岩石,有些还会形成新的地貌。'], '情感陪伴': ['小Q,我今天有点不开心。', '我在呢。你愿意告诉我发生了什么吗?', '我和朋友吵架了。', '听起来你很难过。我们可以先深呼吸,再想想怎么和朋友好好说。'], '其他': ['小Q,你现在能做什么?', '我可以聊天、讲故事、唱儿歌、回答问题,也可以帮你做一些简单提醒。', '那我们随便聊聊吧。', '好呀,你今天最开心的一件事是什么?'], }; return samples[topic] || samples['其他']; } function renderSessionConversation(session, options = {}) { if (!session) { return '

暂无可查看的对话内容

'; } const lines = getConversationLines(session.intent); const hiddenTurns = Math.max(session.turns - 4, 0); return `
用户
${sessionEscapeHtml(lines[0])}
Q
${sessionEscapeHtml(lines[1])}
用户
${sessionEscapeHtml(lines[2])}
Q
${sessionEscapeHtml(lines[3])}
${options.showMore !== false ? `
还有 ${hiddenTurns} 轮对话
` : ''} `; } function renderDeviceRows(devices, selectedDeviceId, showCompanyColumn) { if (!devices.length) { return `暂无设备数据`; } return devices.map(device => { const active = device.device === selectedDeviceId; return `
${sessionEscapeHtml(device.device)}
${sessionEscapeHtml(device.agent)}
${showCompanyColumn ? `${sessionEscapeHtml(getSessionCompanyName(device.companyId, device.companyName))}` : ''} ${device.count} 条 ${device.turns} 轮 ${device.topics.length ? device.topics.slice(0, 3).map(topic => `${sessionEscapeHtml(topic)}`).join('') : '当天无对话'} ${device.lastSession ? sessionEscapeHtml(device.lastSession.start.slice(11)) : '-'} `; }).join(''); } function renderDeviceSessionList(device, selectedDate) { if (!device) { return `

设备对话历史

${formatSessionDateLabel(selectedDate)} · 从左侧选择设备查看对话内容

请选择设备查看当天对话历史

`; } if (!device.sessions.length) { return `

${sessionEscapeHtml(device.device)}

${formatSessionDateLabel(selectedDate)} 当天暂无对话

该设备在所选日期没有产生对话

`; } return `

${sessionEscapeHtml(device.device)}

${formatSessionDateLabel(selectedDate)} · ${device.sessions.length} 条对话 · ${device.turns} 轮互动

${sessionEscapeHtml(device.agent)}
${device.sessions.map(session => `
${sessionEscapeHtml(session.intent)} ${sessionEscapeHtml(session.start.slice(11))} · ${session.turns} 轮 · ${sessionEscapeHtml(session.duration)}
${getSessionQualityText(session.quality)}
${renderSessionConversation(session)}
`).join('')}
`; } function renderTopicAggregateView(sessions, selectedTopic, selectedDate, companyName) { const totalTurns = sessions.reduce((sum, session) => sum + session.turns, 0); const agents = new Set(sessions.map(session => session.agent)); return `
话题 ${sessionEscapeHtml(selectedTopic)}
日期 ${formatSessionDateLabel(selectedDate)}
会话数 ${sessions.length}
总轮次 ${totalTurns}
「${sessionEscapeHtml(selectedTopic)}」对话内容整合
${sessionEscapeHtml(companyName)} 全部设备聚合 · ${agents.size} 个智能体参与 · 不按设备区分
${sessions.length ? `
${sessions.map(session => `
${sessionEscapeHtml(session.agent)} ${sessionEscapeHtml(session.start.slice(11))} · ${session.turns} 轮 · ${sessionEscapeHtml(session.duration)}
${getSessionQualityText(session.quality)}
${renderSessionConversation(session)}
`).join('')}
` : `

所选日期暂无「${sessionEscapeHtml(selectedTopic)}」相关对话

`}
`; } registerPage('session', function(container) { const currentRole = window.__getUserRole ? window.__getUserRole() : 'admin'; const isClient = currentRole === 'client'; const currentCompanyId = getSessionCurrentCompanyId(); const currentCompanyName = getSessionCompanyName(currentCompanyId, '当前公司'); const viewState = readSessionViewState(); const topicDrilldown = readSessionTopicDrilldown(); const selectedTopic = topicDrilldown?.topic || ''; const fromDashboardDrilldown = topicDrilldown?.source === 'dashboard-topic-ranking'; const selectedDate = viewState.date || topicDrilldown?.date || SESSION_DEFAULT_DATE; const allSessions = getMockSessionData(); const companySessions = currentCompanyId === 'all' ? allSessions : allSessions.filter(session => session.companyId === currentCompanyId); const daySessions = companySessions.filter(session => session.date === selectedDate); const topicSessions = selectedTopic ? daySessions.filter(session => sessionMatchesTopic(session, selectedTopic)) : []; const devices = getCompanyDevices(companySessions, daySessions); const selectedDeviceId = selectedTopic ? '' : (viewState.deviceId && devices.some(device => device.device === viewState.deviceId) ? viewState.deviceId : ''); const selectedDevice = devices.find(device => device.device === selectedDeviceId); const showCompanyColumn = !isClient && currentCompanyId === 'all'; const topicOptions = getTopicOptionsForCompany(companySessions); const deviceCount = devices.length; const dayTurns = daySessions.reduce((sum, session) => sum + session.turns, 0); const pageDesc = selectedTopic ? `${currentCompanyName} · 按热门话题聚合查看对话内容` : `${currentCompanyName} · 按设备查看所选日期的对话历史`; container.innerHTML = ` ${selectedTopic ? `
热门话题聚合:${sessionEscapeHtml(selectedTopic)} ${sessionEscapeHtml(currentCompanyName)} · ${formatSessionDateLabel(selectedDate)} · 当前结果 ${topicSessions.length} 条 · 不区分设备
` : ''}
设备 ${deviceCount}
当日会话 ${selectedTopic ? topicSessions.length : daySessions.length}
互动轮次 ${selectedTopic ? topicSessions.reduce((sum, session) => sum + session.turns, 0) : dayTurns}
${selectedTopic ? renderTopicAggregateView(topicSessions, selectedTopic, selectedDate, currentCompanyName) : `
全部设备
${formatSessionDateLabel(selectedDate)} · 点击设备查看当天对话历史
${showCompanyColumn ? '' : ''} ${renderDeviceRows(devices, selectedDeviceId, showCompanyColumn)}
设备所属公司当日会话 轮次 涉及话题 最后对话 操作
${renderDeviceSessionList(selectedDevice, selectedDate)}
`} `; lucide.createIcons({ elements: [container] }); const backToAllTopics = () => { sessionStorage.removeItem(SESSION_TOPIC_DRILLDOWN_KEY); navigateTo('session'); }; container.querySelector('#sessionStepBackBtn')?.addEventListener('click', () => { sessionStorage.removeItem(SESSION_TOPIC_DRILLDOWN_KEY); if (fromDashboardDrilldown) { navigateTo('dashboard'); return; } navigateTo('session'); }); container.querySelector('#clearSessionTopicFilter')?.addEventListener('click', backToAllTopics); container.querySelectorAll('[data-session-back-topic]').forEach(button => { button.addEventListener('click', backToAllTopics); }); container.querySelectorAll('[data-session-back-device]').forEach(button => { button.addEventListener('click', () => { updateSessionViewState({ deviceId: '' }); navigateTo('session'); }); }); container.querySelector('#sessionDateFilter')?.addEventListener('change', event => { updateSessionViewState({ date: event.target.value || SESSION_DEFAULT_DATE, deviceId: '' }); navigateTo('session'); }); container.querySelector('#sessionTopicFilter')?.addEventListener('change', event => { const topic = event.target.value; setSessionTopicDrilldown(topic, { companyId: currentCompanyId, companyName: currentCompanyName, source: 'session-filter', date: selectedDate, }); navigateTo('session'); }); const selectDevice = deviceId => { if (!deviceId) return; updateSessionViewState({ deviceId }); navigateTo('session'); }; container.querySelectorAll('.session-device-row').forEach(row => { row.addEventListener('click', () => { selectDevice(row.dataset.deviceId); }); }); container.querySelectorAll('.view-device-btn').forEach(button => { button.addEventListener('click', event => { event.stopPropagation(); selectDevice(button.dataset.deviceId); }); }); });