579 lines
26 KiB
JavaScript
579 lines
26 KiB
JavaScript
/**
|
||
* 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 '<div class="empty-state" style="padding:40px 12px"><p>暂无可查看的对话内容</p></div>';
|
||
}
|
||
const lines = getConversationLines(session.intent);
|
||
const hiddenTurns = Math.max(session.turns - 4, 0);
|
||
return `
|
||
<div class="session-message user">
|
||
<div class="session-message-avatar">用户</div>
|
||
<div class="session-message-text">${sessionEscapeHtml(lines[0])}</div>
|
||
</div>
|
||
<div class="session-message bot">
|
||
<div class="session-message-avatar">Q</div>
|
||
<div class="session-message-text">${sessionEscapeHtml(lines[1])}</div>
|
||
</div>
|
||
<div class="session-message user">
|
||
<div class="session-message-avatar">用户</div>
|
||
<div class="session-message-text">${sessionEscapeHtml(lines[2])}</div>
|
||
</div>
|
||
<div class="session-message bot">
|
||
<div class="session-message-avatar">Q</div>
|
||
<div class="session-message-text">${sessionEscapeHtml(lines[3])}</div>
|
||
</div>
|
||
${options.showMore !== false ? `<div class="session-more">还有 ${hiddenTurns} 轮对话</div>` : ''}
|
||
`;
|
||
}
|
||
|
||
function renderDeviceRows(devices, selectedDeviceId, showCompanyColumn) {
|
||
if (!devices.length) {
|
||
return `<tr><td colspan="${showCompanyColumn ? 7 : 6}" style="text-align:center;padding:40px;color:var(--text-muted)">暂无设备数据</td></tr>`;
|
||
}
|
||
return devices.map(device => {
|
||
const active = device.device === selectedDeviceId;
|
||
return `
|
||
<tr class="${active ? 'session-device-row active' : 'session-device-row'}" data-device-id="${sessionEscapeAttr(device.device)}">
|
||
<td>
|
||
<div style="font-weight:600">${sessionEscapeHtml(device.device)}</div>
|
||
<div style="font-size:12px;color:var(--text-muted)">${sessionEscapeHtml(device.agent)}</div>
|
||
</td>
|
||
${showCompanyColumn ? `<td><span class="badge badge-default">${sessionEscapeHtml(getSessionCompanyName(device.companyId, device.companyName))}</span></td>` : ''}
|
||
<td><span class="badge badge-info">${device.count} 条</span></td>
|
||
<td style="font-weight:600">${device.turns} 轮</td>
|
||
<td>${device.topics.length ? device.topics.slice(0, 3).map(topic => `<span class="badge badge-default" style="margin-right:4px">${sessionEscapeHtml(topic)}</span>`).join('') : '<span style="color:var(--text-muted)">当天无对话</span>'}</td>
|
||
<td style="font-size:12px;color:var(--text-muted)">${device.lastSession ? sessionEscapeHtml(device.lastSession.start.slice(11)) : '-'}</td>
|
||
<td><button class="btn btn-outline btn-sm view-device-btn" data-device-id="${sessionEscapeAttr(device.device)}">查看</button></td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderDeviceSessionList(device, selectedDate) {
|
||
if (!device) {
|
||
return `
|
||
<div class="session-device-panel">
|
||
<div class="session-panel-header">
|
||
<div>
|
||
<h3>设备对话历史</h3>
|
||
<p>${formatSessionDateLabel(selectedDate)} · 从左侧选择设备查看对话内容</p>
|
||
</div>
|
||
</div>
|
||
<div class="empty-state" style="padding:56px 16px"><p>请选择设备查看当天对话历史</p></div>
|
||
</div>
|
||
`;
|
||
}
|
||
if (!device.sessions.length) {
|
||
return `
|
||
<div class="session-device-panel">
|
||
<div class="session-panel-header">
|
||
<div>
|
||
<h3>${sessionEscapeHtml(device.device)}</h3>
|
||
<p>${formatSessionDateLabel(selectedDate)} 当天暂无对话</p>
|
||
</div>
|
||
<button class="btn btn-outline btn-sm" data-session-back-device>
|
||
<i data-lucide="arrow-left"></i> 返回设备列表
|
||
</button>
|
||
</div>
|
||
<div class="empty-state" style="padding:42px 16px"><p>该设备在所选日期没有产生对话</p></div>
|
||
</div>
|
||
`;
|
||
}
|
||
return `
|
||
<div class="session-device-panel">
|
||
<div class="session-panel-header">
|
||
<div>
|
||
<h3>${sessionEscapeHtml(device.device)}</h3>
|
||
<p>${formatSessionDateLabel(selectedDate)} · ${device.sessions.length} 条对话 · ${device.turns} 轮互动</p>
|
||
</div>
|
||
<div class="session-panel-actions">
|
||
<span class="badge badge-success">${sessionEscapeHtml(device.agent)}</span>
|
||
<button class="btn btn-outline btn-sm" data-session-back-device>
|
||
<i data-lucide="arrow-left"></i> 返回设备列表
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="session-history-list">
|
||
${device.sessions.map(session => `
|
||
<div class="session-history-card">
|
||
<div class="session-history-head">
|
||
<div>
|
||
<strong>${sessionEscapeHtml(session.intent)}</strong>
|
||
<span>${sessionEscapeHtml(session.start.slice(11))} · ${session.turns} 轮 · ${sessionEscapeHtml(session.duration)}</span>
|
||
</div>
|
||
<span class="badge ${getSessionQualityBadge(session.quality)}">${getSessionQualityText(session.quality)}</span>
|
||
</div>
|
||
<div class="session-transcript">
|
||
${renderSessionConversation(session)}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 `
|
||
<div class="session-topic-layout">
|
||
<div class="session-topic-summary">
|
||
<div>
|
||
<span>话题</span>
|
||
<strong>${sessionEscapeHtml(selectedTopic)}</strong>
|
||
</div>
|
||
<div>
|
||
<span>日期</span>
|
||
<strong>${formatSessionDateLabel(selectedDate)}</strong>
|
||
</div>
|
||
<div>
|
||
<span>会话数</span>
|
||
<strong>${sessions.length}</strong>
|
||
</div>
|
||
<div>
|
||
<span>总轮次</span>
|
||
<strong>${totalTurns}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card full">
|
||
<div class="card-header">
|
||
<div>
|
||
<div class="card-title">「${sessionEscapeHtml(selectedTopic)}」对话内容整合</div>
|
||
<div class="card-subtitle">${sessionEscapeHtml(companyName)} 全部设备聚合 · ${agents.size} 个智能体参与 · 不按设备区分</div>
|
||
</div>
|
||
</div>
|
||
${sessions.length ? `
|
||
<div class="session-aggregate-list">
|
||
${sessions.map(session => `
|
||
<div class="session-aggregate-card">
|
||
<div class="session-history-head">
|
||
<div>
|
||
<strong>${sessionEscapeHtml(session.agent)}</strong>
|
||
<span>${sessionEscapeHtml(session.start.slice(11))} · ${session.turns} 轮 · ${sessionEscapeHtml(session.duration)}</span>
|
||
</div>
|
||
<span class="badge ${getSessionQualityBadge(session.quality)}">${getSessionQualityText(session.quality)}</span>
|
||
</div>
|
||
<div class="session-transcript">
|
||
${renderSessionConversation(session)}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
` : `<div class="empty-state" style="padding:56px 16px"><p>所选日期暂无「${sessionEscapeHtml(selectedTopic)}」相关对话</p></div>`}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 = `
|
||
<div class="page-header">
|
||
<div>
|
||
<div class="page-title">会话详情</div>
|
||
<div class="page-desc">${sessionEscapeHtml(pageDesc)}</div>
|
||
</div>
|
||
</div>
|
||
|
||
${selectedTopic ? `
|
||
<div class="session-view-backbar">
|
||
<button class="btn btn-outline btn-sm" id="sessionStepBackBtn">
|
||
<i data-lucide="arrow-left"></i> ${fromDashboardDrilldown ? '返回数据仪表盘' : '返回全部话题'}
|
||
</button>
|
||
</div>
|
||
<div class="session-drilldown-banner">
|
||
<div class="session-drilldown-icon"><i data-lucide="bar-chart-3"></i></div>
|
||
<div class="session-drilldown-copy">
|
||
<strong>热门话题聚合:${sessionEscapeHtml(selectedTopic)}</strong>
|
||
<span>${sessionEscapeHtml(currentCompanyName)} · ${formatSessionDateLabel(selectedDate)} · 当前结果 ${topicSessions.length} 条 · 不区分设备</span>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="session-filter-panel">
|
||
<div class="session-filter-main">
|
||
<div class="form-group">
|
||
<label class="form-label">日期</label>
|
||
<input type="date" class="form-control" id="sessionDateFilter" value="${sessionEscapeAttr(selectedDate)}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">热门话题</label>
|
||
<select class="form-control" id="sessionTopicFilter">
|
||
<option value="">全部话题</option>
|
||
${topicOptions.map(topic => `<option value="${sessionEscapeAttr(topic)}" ${topic === selectedTopic ? 'selected' : ''}>${sessionEscapeHtml(topic)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="session-filter-stats">
|
||
<div>
|
||
<span>设备</span>
|
||
<strong>${deviceCount}</strong>
|
||
</div>
|
||
<div>
|
||
<span>当日会话</span>
|
||
<strong>${selectedTopic ? topicSessions.length : daySessions.length}</strong>
|
||
</div>
|
||
<div>
|
||
<span>互动轮次</span>
|
||
<strong>${selectedTopic ? topicSessions.reduce((sum, session) => sum + session.turns, 0) : dayTurns}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${selectedTopic ? renderTopicAggregateView(topicSessions, selectedTopic, selectedDate, currentCompanyName) : `
|
||
<div class="session-device-layout">
|
||
<div class="chart-card">
|
||
<div class="card-header">
|
||
<div>
|
||
<div class="card-title">全部设备</div>
|
||
<div class="card-subtitle">${formatSessionDateLabel(selectedDate)} · 点击设备查看当天对话历史</div>
|
||
</div>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>设备</th>
|
||
${showCompanyColumn ? '<th>所属公司</th>' : ''}
|
||
<th>当日会话</th>
|
||
<th>轮次</th>
|
||
<th>涉及话题</th>
|
||
<th>最后对话</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${renderDeviceRows(devices, selectedDeviceId, showCompanyColumn)}</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card">
|
||
${renderDeviceSessionList(selectedDevice, selectedDate)}
|
||
</div>
|
||
</div>
|
||
`}
|
||
`;
|
||
|
||
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);
|
||
});
|
||
});
|
||
});
|