Browse Source

ChatSDK可在对话历史中选定继续会话

master
wanghanlin 17 hours ago
parent
commit
5714d01cbb
  1. 103
      client/dist/chatbot-sdk.js
  2. 2
      client/dist/chatbot-sdk.js.map
  3. 2
      client/dist/chatbot-sdk.min.js
  4. 2
      client/dist/chatbot-sdk.min.js.map
  5. 76
      client/src/chat.ts
  6. 23
      client/src/dom.ts
  7. 7
      client/src/styles.ts
  8. 103
      src/main/resources/static/sdk/chatbot-sdk.js
  9. 2
      src/main/resources/static/sdk/chatbot-sdk.js.map
  10. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js
  11. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js.map
  12. 28
      src/main/resources/static/sdk/test.html

103
client/dist/chatbot-sdk.js

@ -1275,6 +1275,13 @@ var ChatbotSDK = (function () {
.csk-history-item:hover { .csk-history-item:hover {
background: #F3F4F6; background: #F3F4F6;
} }
.csk-history-item--active {
background: #EEF2FF;
border-left: 3px solid var(--csk-primary);
}
.csk-history-item--active:hover {
background: #E0E7FF;
}
.csk-history-item__info { .csk-history-item__info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -1810,7 +1817,7 @@ var ChatbotSDK = (function () {
} }
} }
/** 渲染会话列表 */ /** 渲染会话列表 */
function renderHistoryList(listEl, items, onExport, onDelete, emptyText) {
function renderHistoryList(listEl, items, onSelect, onExport, onDelete, activeChatId, emptyText) {
listEl.innerHTML = ''; listEl.innerHTML = '';
if (items.length === 0) { if (items.length === 0) {
const empty = document.createElement('div'); const empty = document.createElement('div');
@ -1825,11 +1832,24 @@ var ChatbotSDK = (function () {
for (const item of items) { for (const item of items) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'csk-history-item'; el.className = 'csk-history-item';
// 高亮当前活跃会话
const convId = item.chatId || item.id;
if (activeChatId && convId === activeChatId) {
el.classList.add('csk-history-item--active');
}
const info = document.createElement('div'); const info = document.createElement('div');
info.className = 'csk-history-item__info'; info.className = 'csk-history-item__info';
const idEl = document.createElement('div'); const idEl = document.createElement('div');
idEl.className = 'csk-history-item__id'; idEl.className = 'csk-history-item__id';
idEl.textContent = item.chatId || item.id;
// 显示最后一条消息预览,没有则显示 chatId
if (item.lastMessagePreview) {
idEl.textContent = item.lastMessagePreview.length > 60
? item.lastMessagePreview.substring(0, 60) + '...'
: item.lastMessagePreview;
}
else {
idEl.textContent = convId;
}
const metaEl = document.createElement('div'); const metaEl = document.createElement('div');
metaEl.className = 'csk-history-item__meta'; metaEl.className = 'csk-history-item__meta';
const metaParts = []; const metaParts = [];
@ -1866,6 +1886,10 @@ var ChatbotSDK = (function () {
actionsEl.appendChild(deleteBtn); actionsEl.appendChild(deleteBtn);
el.appendChild(info); el.appendChild(info);
el.appendChild(actionsEl); el.appendChild(actionsEl);
// 点击整行 → 切换到该会话
el.addEventListener('click', () => {
onSelect(convId);
});
listEl.appendChild(el); listEl.appendChild(el);
} }
} }
@ -2494,25 +2518,90 @@ var ChatbotSDK = (function () {
try { try {
const result = await fetchConversationList(1, 50, config$1.userId, config$1.integrateId); const result = await fetchConversationList(1, 50, config$1.userId, config$1.integrateId);
const items = result.list.map(c => ({ const items = result.list.map(c => ({
id: c.conversationId || c.chatId || '',
chatId: c.conversationId || c.chatId || '',
id: c.conversationId || '',
chatId: c.conversationId || '', // conversationId 就是 chatId
messageCount: c.messageCount, messageCount: c.messageCount,
lastMessageTime: c.lastMessageTime, lastMessageTime: c.lastMessageTime,
lastMessagePreview: c.lastMessagePreview,
createdAt: c.firstMessageTime || c.createdAt, createdAt: c.firstMessageTime || c.createdAt,
})); }));
renderHistoryList(listEl, items, (id) => { window.open(getConversationExportUrl(id), '_blank'); }, async (id) => {
renderHistoryList(listEl, items,
// onSelect: 切换到选中的会话
(conversationId) => {
switchToConversation(conversationId);
},
// onExport
(id) => { window.open(getConversationExportUrl(id), '_blank'); },
// onDelete
async (id) => {
if (!confirm(t('history_delete_confirm'))) if (!confirm(t('history_delete_confirm')))
return; return;
const ok = await deleteConversation(id); const ok = await deleteConversation(id);
if (ok)
if (ok) {
// 如果删的是当前会话,清空聊天窗口
if (id === getChatId()) {
messages = [];
if (messagesContainer$1) {
const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading');
msgs.forEach(el => el.remove());
}
if (clearBtn$1)
clearBtn$1.style.display = 'none';
}
loadHistoryConversations(); loadHistoryConversations();
});
}
},
// 当前活跃 chatId,用于高亮
getChatId());
} }
catch (err) { catch (err) {
logger.error(t('history_load_error'), err); logger.error(t('history_load_error'), err);
listEl.innerHTML = `<div class="csk-history-panel__empty"><div class="csk-history-panel__empty-icon">⚠</div><div>${t('history_load_error')}</div></div>`; listEl.innerHTML = `<div class="csk-history-panel__empty"><div class="csk-history-panel__empty-icon">⚠</div><div>${t('history_load_error')}</div></div>`;
} }
} }
/**
* 切换到指定会话加载上下文并继续对话
* @param conversationId 会话 ID chatId
*/
async function switchToConversation(conversationId) {
if (!config$1 || !messagesContainer$1)
return;
logger.info(`切换到会话 conversationId=${conversationId}`);
// 1. 更新 chatId
updateChatId(conversationId);
saveCachedChatId(config$1.integrateId, config$1.userId, conversationId);
// 2. 关闭历史面板
if (historyPanel$1) {
historyPanel$1.classList.add('csk-history-panel--hidden');
}
// 3. 清空当前消息
messages = [];
const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading');
msgs.forEach(el => el.remove());
// 4. 从后端加载该会话的消息
try {
const result = await fetchConversationMessages(conversationId);
if (result.messages.length > 0) {
messages = result.messages.map((msg) => ({
id: uuid(),
role: msg.messageType === 'USER' ? 'user' : 'ai',
content: msg.content,
timestamp: new Date(msg.createTime).getTime(),
}));
renderHistory();
logger.info(`加载会话 ${conversationId}${messages.length} 条消息`);
// 同步到 localStorage
saveMessages(config$1.integrateId, messages);
}
}
catch (err) {
logger.warn(`加载会话消息失败 conversationId=${conversationId}`, err);
}
// 5. 显示清空按钮
if (clearBtn$1 && messages.length > 0) {
clearBtn$1.style.display = 'inline-flex';
}
}
// ==================== 单例状态 ==================== // ==================== 单例状态 ====================
let config = null; let config = null;

2
client/dist/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

76
client/src/chat.ts

@ -426,22 +426,42 @@ export async function loadHistoryConversations(): Promise<void> {
try { try {
const result = await fetchConversationList(1, 50, config.userId, config.integrateId); const result = await fetchConversationList(1, 50, config.userId, config.integrateId);
const items: HistoryItemData[] = result.list.map(c => ({ const items: HistoryItemData[] = result.list.map(c => ({
id: c.conversationId || c.chatId || '',
chatId: c.conversationId || c.chatId || '',
id: c.conversationId || '',
chatId: c.conversationId || '', // conversationId 就是 chatId
messageCount: c.messageCount, messageCount: c.messageCount,
lastMessageTime: c.lastMessageTime, lastMessageTime: c.lastMessageTime,
lastMessagePreview: c.lastMessagePreview,
createdAt: c.firstMessageTime || c.createdAt, createdAt: c.firstMessageTime || c.createdAt,
})); }));
renderHistoryList( renderHistoryList(
listEl, listEl,
items, items,
// onSelect: 切换到选中的会话
(conversationId: string) => {
switchToConversation(conversationId);
},
// onExport
(id: string) => { window.open(getConversationExportUrl(id), '_blank'); }, (id: string) => { window.open(getConversationExportUrl(id), '_blank'); },
// onDelete
async (id: string) => { async (id: string) => {
if (!confirm(t('history_delete_confirm'))) return; if (!confirm(t('history_delete_confirm'))) return;
const ok = await deleteConversation(id); const ok = await deleteConversation(id);
if (ok) loadHistoryConversations();
if (ok) {
// 如果删的是当前会话,清空聊天窗口
if (id === getChatId()) {
messages = [];
if (messagesContainer) {
const msgs = messagesContainer.querySelectorAll('.csk-msg, .csk-loading');
msgs.forEach(el => el.remove());
} }
if (clearBtn) clearBtn.style.display = 'none';
}
loadHistoryConversations();
}
},
// 当前活跃 chatId,用于高亮
getChatId()
); );
} catch (err) { } catch (err) {
logger.error(t('history_load_error'), err); logger.error(t('history_load_error'), err);
@ -449,6 +469,56 @@ export async function loadHistoryConversations(): Promise<void> {
} }
} }
/**
*
* @param conversationId ID chatId
*/
export async function switchToConversation(conversationId: string): Promise<void> {
if (!config || !messagesContainer) return;
logger.info(`切换到会话 conversationId=${conversationId}`);
// 1. 更新 chatId
updateChatId(conversationId);
saveCachedChatId(config.integrateId, config.userId, conversationId);
// 2. 关闭历史面板
if (historyPanel) {
historyPanel.classList.add('csk-history-panel--hidden');
}
// 3. 清空当前消息
messages = [];
const msgs = messagesContainer.querySelectorAll('.csk-msg, .csk-loading');
msgs.forEach(el => el.remove());
// 4. 从后端加载该会话的消息
try {
const result = await fetchConversationMessages(conversationId);
if (result.messages.length > 0) {
messages = result.messages.map((msg) => ({
id: uuid(),
role: msg.messageType === 'USER' ? 'user' : 'ai' as const,
content: msg.content,
timestamp: new Date(msg.createTime).getTime(),
}));
renderHistory();
logger.info(`加载会话 ${conversationId}${messages.length} 条消息`);
// 同步到 localStorage
saveMessages(config.integrateId, messages);
}
} catch (err) {
logger.warn(`加载会话消息失败 conversationId=${conversationId}`, err);
}
// 5. 显示清空按钮
if (clearBtn && messages.length > 0) {
clearBtn.style.display = 'inline-flex';
}
}
/** 获取当前消息列表 */ /** 获取当前消息列表 */
export function getMessages(): ChatMessage[] { export function getMessages(): ChatMessage[] {
return messages; return messages;

23
client/src/dom.ts

@ -446,6 +446,7 @@ export interface HistoryItemData {
chatId?: string; chatId?: string;
messageCount?: number; messageCount?: number;
lastMessageTime?: string; lastMessageTime?: string;
lastMessagePreview?: string;
createdAt?: string; createdAt?: string;
} }
@ -453,8 +454,10 @@ export interface HistoryItemData {
export function renderHistoryList( export function renderHistoryList(
listEl: HTMLElement, listEl: HTMLElement,
items: HistoryItemData[], items: HistoryItemData[],
onSelect: (conversationId: string) => void,
onExport: (id: string) => void, onExport: (id: string) => void,
onDelete: (id: string) => void, onDelete: (id: string) => void,
activeChatId?: string,
emptyText?: string emptyText?: string
): void { ): void {
listEl.innerHTML = ''; listEl.innerHTML = '';
@ -473,13 +476,25 @@ export function renderHistoryList(
for (const item of items) { for (const item of items) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'csk-history-item'; el.className = 'csk-history-item';
// 高亮当前活跃会话
const convId = item.chatId || item.id;
if (activeChatId && convId === activeChatId) {
el.classList.add('csk-history-item--active');
}
const info = document.createElement('div'); const info = document.createElement('div');
info.className = 'csk-history-item__info'; info.className = 'csk-history-item__info';
const idEl = document.createElement('div'); const idEl = document.createElement('div');
idEl.className = 'csk-history-item__id'; idEl.className = 'csk-history-item__id';
idEl.textContent = item.chatId || item.id;
// 显示最后一条消息预览,没有则显示 chatId
if (item.lastMessagePreview) {
idEl.textContent = item.lastMessagePreview.length > 60
? item.lastMessagePreview.substring(0, 60) + '...'
: item.lastMessagePreview;
} else {
idEl.textContent = convId;
}
const metaEl = document.createElement('div'); const metaEl = document.createElement('div');
metaEl.className = 'csk-history-item__meta'; metaEl.className = 'csk-history-item__meta';
@ -520,6 +535,12 @@ export function renderHistoryList(
el.appendChild(info); el.appendChild(info);
el.appendChild(actionsEl); el.appendChild(actionsEl);
// 点击整行 → 切换到该会话
el.addEventListener('click', () => {
onSelect(convId);
});
listEl.appendChild(el); listEl.appendChild(el);
} }
} }

7
client/src/styles.ts

@ -570,6 +570,13 @@ function getStyles(config: ResolvedConfig): string {
.csk-history-item:hover { .csk-history-item:hover {
background: #F3F4F6; background: #F3F4F6;
} }
.csk-history-item--active {
background: #EEF2FF;
border-left: 3px solid var(--csk-primary);
}
.csk-history-item--active:hover {
background: #E0E7FF;
}
.csk-history-item__info { .csk-history-item__info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;

103
src/main/resources/static/sdk/chatbot-sdk.js

@ -1275,6 +1275,13 @@ var ChatbotSDK = (function () {
.csk-history-item:hover { .csk-history-item:hover {
background: #F3F4F6; background: #F3F4F6;
} }
.csk-history-item--active {
background: #EEF2FF;
border-left: 3px solid var(--csk-primary);
}
.csk-history-item--active:hover {
background: #E0E7FF;
}
.csk-history-item__info { .csk-history-item__info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -1810,7 +1817,7 @@ var ChatbotSDK = (function () {
} }
} }
/** 渲染会话列表 */ /** 渲染会话列表 */
function renderHistoryList(listEl, items, onExport, onDelete, emptyText) {
function renderHistoryList(listEl, items, onSelect, onExport, onDelete, activeChatId, emptyText) {
listEl.innerHTML = ''; listEl.innerHTML = '';
if (items.length === 0) { if (items.length === 0) {
const empty = document.createElement('div'); const empty = document.createElement('div');
@ -1825,11 +1832,24 @@ var ChatbotSDK = (function () {
for (const item of items) { for (const item of items) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'csk-history-item'; el.className = 'csk-history-item';
// 高亮当前活跃会话
const convId = item.chatId || item.id;
if (activeChatId && convId === activeChatId) {
el.classList.add('csk-history-item--active');
}
const info = document.createElement('div'); const info = document.createElement('div');
info.className = 'csk-history-item__info'; info.className = 'csk-history-item__info';
const idEl = document.createElement('div'); const idEl = document.createElement('div');
idEl.className = 'csk-history-item__id'; idEl.className = 'csk-history-item__id';
idEl.textContent = item.chatId || item.id;
// 显示最后一条消息预览,没有则显示 chatId
if (item.lastMessagePreview) {
idEl.textContent = item.lastMessagePreview.length > 60
? item.lastMessagePreview.substring(0, 60) + '...'
: item.lastMessagePreview;
}
else {
idEl.textContent = convId;
}
const metaEl = document.createElement('div'); const metaEl = document.createElement('div');
metaEl.className = 'csk-history-item__meta'; metaEl.className = 'csk-history-item__meta';
const metaParts = []; const metaParts = [];
@ -1866,6 +1886,10 @@ var ChatbotSDK = (function () {
actionsEl.appendChild(deleteBtn); actionsEl.appendChild(deleteBtn);
el.appendChild(info); el.appendChild(info);
el.appendChild(actionsEl); el.appendChild(actionsEl);
// 点击整行 → 切换到该会话
el.addEventListener('click', () => {
onSelect(convId);
});
listEl.appendChild(el); listEl.appendChild(el);
} }
} }
@ -2494,25 +2518,90 @@ var ChatbotSDK = (function () {
try { try {
const result = await fetchConversationList(1, 50, config$1.userId, config$1.integrateId); const result = await fetchConversationList(1, 50, config$1.userId, config$1.integrateId);
const items = result.list.map(c => ({ const items = result.list.map(c => ({
id: c.conversationId || c.chatId || '',
chatId: c.conversationId || c.chatId || '',
id: c.conversationId || '',
chatId: c.conversationId || '', // conversationId 就是 chatId
messageCount: c.messageCount, messageCount: c.messageCount,
lastMessageTime: c.lastMessageTime, lastMessageTime: c.lastMessageTime,
lastMessagePreview: c.lastMessagePreview,
createdAt: c.firstMessageTime || c.createdAt, createdAt: c.firstMessageTime || c.createdAt,
})); }));
renderHistoryList(listEl, items, (id) => { window.open(getConversationExportUrl(id), '_blank'); }, async (id) => {
renderHistoryList(listEl, items,
// onSelect: 切换到选中的会话
(conversationId) => {
switchToConversation(conversationId);
},
// onExport
(id) => { window.open(getConversationExportUrl(id), '_blank'); },
// onDelete
async (id) => {
if (!confirm(t('history_delete_confirm'))) if (!confirm(t('history_delete_confirm')))
return; return;
const ok = await deleteConversation(id); const ok = await deleteConversation(id);
if (ok)
if (ok) {
// 如果删的是当前会话,清空聊天窗口
if (id === getChatId()) {
messages = [];
if (messagesContainer$1) {
const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading');
msgs.forEach(el => el.remove());
}
if (clearBtn$1)
clearBtn$1.style.display = 'none';
}
loadHistoryConversations(); loadHistoryConversations();
});
}
},
// 当前活跃 chatId,用于高亮
getChatId());
} }
catch (err) { catch (err) {
logger.error(t('history_load_error'), err); logger.error(t('history_load_error'), err);
listEl.innerHTML = `<div class="csk-history-panel__empty"><div class="csk-history-panel__empty-icon">⚠</div><div>${t('history_load_error')}</div></div>`; listEl.innerHTML = `<div class="csk-history-panel__empty"><div class="csk-history-panel__empty-icon">⚠</div><div>${t('history_load_error')}</div></div>`;
} }
} }
/**
* 切换到指定会话加载上下文并继续对话
* @param conversationId 会话 ID chatId
*/
async function switchToConversation(conversationId) {
if (!config$1 || !messagesContainer$1)
return;
logger.info(`切换到会话 conversationId=${conversationId}`);
// 1. 更新 chatId
updateChatId(conversationId);
saveCachedChatId(config$1.integrateId, config$1.userId, conversationId);
// 2. 关闭历史面板
if (historyPanel$1) {
historyPanel$1.classList.add('csk-history-panel--hidden');
}
// 3. 清空当前消息
messages = [];
const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading');
msgs.forEach(el => el.remove());
// 4. 从后端加载该会话的消息
try {
const result = await fetchConversationMessages(conversationId);
if (result.messages.length > 0) {
messages = result.messages.map((msg) => ({
id: uuid(),
role: msg.messageType === 'USER' ? 'user' : 'ai',
content: msg.content,
timestamp: new Date(msg.createTime).getTime(),
}));
renderHistory();
logger.info(`加载会话 ${conversationId}${messages.length} 条消息`);
// 同步到 localStorage
saveMessages(config$1.integrateId, messages);
}
}
catch (err) {
logger.warn(`加载会话消息失败 conversationId=${conversationId}`, err);
}
// 5. 显示清空按钮
if (clearBtn$1 && messages.length > 0) {
clearBtn$1.style.display = 'inline-flex';
}
}
// ==================== 单例状态 ==================== // ==================== 单例状态 ====================
let config = null; let config = null;

2
src/main/resources/static/sdk/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

28
src/main/resources/static/sdk/test.html

@ -65,6 +65,21 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
.footer{padding:10px 20px;background:#fff;border-top:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;font-size:11px;color:#9CA3AF} .footer{padding:10px 20px;background:#fff;border-top:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;font-size:11px;color:#9CA3AF}
.mapping-note{background:#FFFBEB;border:1px solid #FDE68A;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:11px;color:#92400E;line-height:1.6} .mapping-note{background:#FFFBEB;border:1px solid #FDE68A;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:11px;color:#92400E;line-height:1.6}
.mapping-note b{color:#78350F} .mapping-note b{color:#78350F}
.code-section{margin-top:12px;border:1px solid #E5E7EB;border-radius:8px;overflow:hidden}
.code-section__header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#F9FAFB;border-bottom:1px solid #E5E7EB}
.code-section__title{font-size:12px;font-weight:600;color:#374151;display:flex;align-items:center;gap:4px}
.code-section__copy{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border:1px solid #D1D5DB;border-radius:4px;background:#fff;color:#374151;font-size:11px;cursor:pointer;transition:all .15s;font-family:inherit}
.code-section__copy:hover{background:#F3F4F6;border-color:#9CA3AF}
.code-section__copy--ok{background:#D1FAE5;border-color:#6EE7B7;color:#065F46}
.code-section__body{padding:0;max-height:260px;overflow-y:auto}
.code-section__body::-webkit-scrollbar{width:4px}
.code-section__body::-webkit-scrollbar-thumb{background:#D1D5DB;border-radius:2px}
.code-section pre{margin:0;padding:12px;font-size:11.5px;line-height:1.6;font-family:'SF Mono','Consolas','Menlo',monospace;white-space:pre;overflow-x:auto;color:#1F2937;tab-size:2}
.code-section .cm{color:#6B7280} /* 注释 */
.code-section .kw{color:#7C3AED} /* 关键字/key */
.code-section .st{color:#059669} /* 字符串 */
.code-section .nu{color:#2563EB} /* 数字 */
.code-section .bl{color:#DC2626} /* 必传高亮 */
</style> </style>
</head> </head>
<body> <body>
@ -166,6 +181,19 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
5. AI 回复支持 Markdown 渲染<br> 5. AI 回复支持 Markdown 渲染<br>
6. 开启 showCategorySwitch 可选择知识库分类 6. 开启 showCategorySwitch 可选择知识库分类
</div> </div>
<hr class="divider">
<!-- 生成接入代码 -->
<div class="code-section">
<div class="code-section__header">
<span class="code-section__title">📋 接入代码</span>
<button class="code-section__copy" id="btn-copy" onclick="copyCode()">📋 复制代码</button>
</div>
<div class="code-section__body">
<pre id="code-output"></pre>
</div>
</div>
</div> </div>
</div> </div>

Loading…
Cancel
Save