document.addEventListener('DOMContentLoaded', () => {
// 注意:认证状态检查由 index.html 中的 checkAuthStatus 函数处理
// 这里不再重复检查,避免冲突
// 防重复重定向标志
let isRedirecting = false;
// 主题检测和适配功能
function initThemeDetection () {
// 检测当前主题偏好
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// 为body添加主题类名,便于CSS选择器
if (prefersDark) {
document.body.classList.add('dark-theme');
document.body.classList.remove('light-theme');
} else {
document.body.classList.add('light-theme');
document.body.classList.remove('dark-theme');
}
// 监听主题变化
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
if (e.matches) {
// 切换到深色主题
document.body.classList.add('dark-theme');
document.body.classList.remove('light-theme');
} else {
// 切换到浅色主题
document.body.classList.add('light-theme');
document.body.classList.remove('dark-theme');
}
// 触发自定义事件,通知其他组件主题已变化
window.dispatchEvent(new CustomEvent('themeChanged', {
detail: { isDark: e.matches }
}));
});
// 添加平滑过渡效果
const style = document.createElement('style');
style.textContent = `
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease !important;
}
/* 确保过渡效果不会影响动画 */
.typing-indicator span,
.loading-spinner,
.ripple,
.message {
transition: none !important;
}
`;
document.head.appendChild(style);
}
// 初始化主题检测
initThemeDetection();
// 获取DOM元素
const chatMessages = document.getElementById('chat-messages');
const userInput = document.getElementById('user-input');
const sendButton = document.getElementById('send-btn');
const resetButton = document.getElementById('reset-btn');
const logoutButton = document.getElementById('logout-btn');
// 初始化复制按钮
if (typeof window.setupCopyButtons === 'function') {
window.setupCopyButtons();
}
// 绑定退出登录事件
if (logoutButton) {
logoutButton.addEventListener('click', handleLogout);
}
// 添加字符计数器样式
const style = document.createElement('style');
style.textContent = `
.char-counter {
position: absolute;
bottom: 8px;
right: 10px;
font-size: 12px;
color: #999;
opacity: 0.8;
transition: all 0.3s ease;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.9);
padding: 2px 6px;
border-radius: 4px;
pointer-events: none;
z-index: 1;
}
.char-counter.warning {
color: #ff9800;
font-weight: 500;
background: rgba(255, 152, 0, 0.1);
}
.char-counter.exceeded {
color: #f44336;
font-weight: 500;
background: rgba(244, 67, 54, 0.1);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.error-message {
background-color: #ff5252;
color: white;
padding: 10px 15px;
border-radius: 4px;
margin: 10px auto;
max-width: 80%;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
position: relative;
transition: opacity 0.3s ease, transform 0.3s ease;
}
/* AI思考过程样式 */
.thought-container {
background-color: #f3f0fa;
border-left: 3px solid #7952b3;
padding: 10px 15px;
margin: 10px 0 10px 0;
border-radius: 0 4px 4px 0;
font-style: italic;
color: #7952b3;
position: relative;
font-size: 0.98em;
}
.thought-container::before {
content: "思考过程";
display: block;
font-size: 0.75rem;
color: #7952b3;
font-weight: 600;
margin-bottom: 4px;
font-style: normal;
}
.thought-toggle-btn {
position: absolute;
top: 8px;
right: 12px;
background: none;
border: none;
color: #7952b3;
font-size: 1.1em;
cursor: pointer;
z-index: 2;
padding: 2px 6px;
border-radius: 3px;
transition: background 0.2s;
}
.thought-toggle-btn:hover {
background: #e5e1f6;
}
.thought-content {
overflow-y: auto;
max-height: 500px;
transition: max-height 0.35s cubic-bezier(.4,0,.2,1), padding 0.3s;
will-change: max-height;
/* 自定义滚动条样式 */
scrollbar-width: thin;
scrollbar-color: #d1c4e9 #f3f0fa;
}
/* Webkit浏览器的滚动条样式 */
.thought-content::-webkit-scrollbar {
width: 8px;
}
.thought-content::-webkit-scrollbar-track {
background: #f3f0fa;
border-radius: 4px;
}
.thought-content::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #d1c4e9, #b39ddb);
border-radius: 4px;
border: 1px solid #f3f0fa;
transition: background 0.2s ease;
}
.thought-content::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #b39ddb, #9575cd);
}
.thought-content::-webkit-scrollbar-thumb:active {
background: linear-gradient(135deg, #9575cd, #7e57c2);
}
.thought-container.collapsed .thought-content {
max-height: 0;
padding: 0 !important;
}
.thought-container.collapsed::before {
content: "思考过程(已收起)";
}
.thought-container p {
margin: 0 0 6px 0;
font-style: italic;
color: #7952b3;
background: none;
border-bottom: 1px dashed #d1c4e9;
padding-bottom: 2px;
}
.thought-container p:last-child {
border-bottom: none;
}
/* 打字机闪烁光标 */
.cursor-blink {
display: inline-block;
color: #666;
font-weight: bold;
margin-left: 1px;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
#send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: scale(0.98);
}
#send-btn:disabled:hover {
background-color: #007bff;
}
/* 输入框Loading样式 */
.input-loading {
background-image: linear-gradient(90deg, rgba(239, 242, 255, 0.5), rgba(255, 255, 255, 0.8), rgba(239, 242, 255, 0.5));
background-size: 200% 100%;
animation: input-loading 1.5s infinite;
border-color: #6366f1 !important;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15);
}
@keyframes input-loading {
0% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* 消息气泡动画效果 */
.message-content.typing {
position: relative;
overflow: hidden;
}
.message-content.typing::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: inherit;
border: 2px solid;
border-color: #6366f1;
box-shadow: 0 0 5px rgba(99, 102, 241, 0.5);
animation: border-pulse 0.8s ease-in-out infinite;
pointer-events: none;
}
@keyframes border-pulse {
0% { opacity: 0.4; border-color: #6366f1; }
50% { opacity: 1; border-color: #818cf8; }
100% { opacity: 0.4; border-color: #6366f1; }
}
/* 对话切换状态样式 */
.conversation-item.streaming {
position: relative;
}
.conversation-item.streaming::after {
content: '';
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
animation: pulse-dot 2s infinite;
}
.conversation-item.paused {
position: relative;
}
.conversation-item.paused::after {
content: '';
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background: #f59e0b;
border-radius: 50%;
animation: pulse-dot 2s infinite;
}
@keyframes pulse-dot {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
100% { opacity: 1; transform: scale(1); }
}
.paused-tip {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 4px;
padding: 4px 8px;
font-size: 0.85em;
color: #f59e0b;
margin-top: 8px;
font-style: italic;
text-align: center;
}
`;
document.head.appendChild(style);
// 状态变量 - 确保用户ID始终存在
let userId = localStorage.getItem('chat_user_id');
if (!userId) {
userId = generateUUID();
localStorage.setItem('chat_user_id', userId);
}
// 新增:用于流式输出中断的标志
let isStopped = false;
let isWaitingForResponse = false;
let currentBotMessageElement = null;
let currentBotMessageContainer = null; // 添加消息容器引用
let useStreamingMode = true; // 始终启用流式响应
let currentLoadingElement = null;
let markdownEnabled = true; // 启用 Markdown 渲染
// 统一的Markdown渲染函数
function renderMarkdown (text) {
if (!markdownEnabled || typeof showdown === 'undefined') {
return text;
}
const converter = new showdown.Converter({
strikethrough: true,
tables: true,
tasklists: true,
ghCodeBlocks: true,
openLinksInNewWindow: true,
emoji: true,
underline: true,
simplifiedAutoLink: true,
excludeTrailingPunctuationFromURLs: true,
literalMidWordUnderscores: true,
tablesHeaderId: true,
smoothLivePreview: true,
smartIndentationFix: true,
disableForced4SpacesIndentedSublists: true,
simpleLineBreaks: true,
requireSpaceBeforeHeadingText: true,
ghCompatibleHeaderId: true,
ghMentions: true,
ghMentionsLink: 'https://github.com/{mention}',
encodeEmails: true,
backslashEscapesHTMLTags: true,
omitExtraWLInCodeBlocks: true,
noHeaderId: false,
prefixHeaderId: false,
rawPrefixHeaderId: false,
rawHeaderId: false,
headerLevelStart: 1,
parseImgDimensions: true
});
return converter.makeHtml(text);
}
// 思考过程专用的渲染函数
function renderThoughts (text) {
if (!markdownEnabled || typeof showdown === 'undefined') {
return text;
}
const converter = new showdown.Converter({
strikethrough: true,
tables: true,
tasklists: true,
ghCodeBlocks: true,
openLinksInNewWindow: true,
emoji: true,
underline: true,
simplifiedAutoLink: true,
excludeTrailingPunctuationFromURLs: true,
literalMidWordUnderscores: true,
tablesHeaderId: true,
smoothLivePreview: true,
smartIndentationFix: true,
disableForced4SpacesIndentedSublists: true,
simpleLineBreaks: false, // 禁用简单换行,保持段落结构
requireSpaceBeforeHeadingText: true,
ghCompatibleHeaderId: true,
ghMentions: true,
ghMentionsLink: 'https://github.com/{mention}',
encodeEmails: true,
backslashEscapesHTMLTags: true,
omitExtraWLInCodeBlocks: true,
noHeaderId: false,
prefixHeaderId: false,
rawPrefixHeaderId: false,
rawHeaderId: false,
headerLevelStart: 1,
parseImgDimensions: true
});
return converter.makeHtml(text);
}
let renderDebounceTimer = null; // 用于防抖动渲染
let maxMessageLength = 20000; // 设置最大消息长度与后端POST限制一致
let eventSource = null; // 存储EventSource实例,用于停止请求
let abortController = null; // 用于中止fetch请求
// 全局对话状态管理 - 支持多对话并发流式响应
const conversationStates = new Map(); // 存储每个对话的状态
let currentActiveConversationId = null; // 当前激活的对话ID
let conversationStreams = new Map(); // 存储每个对话的流式连接
// 用于控制自动滚动的变量
let shouldAutoScroll = true;
let lastScrollTop = 0;
let scrollThreshold = 50; // 距离底部多少像素内触发自动滚动
// 对话状态管理函数
function initializeConversationState (conversationId) {
if (!conversationStates.has(conversationId)) {
conversationStates.set(conversationId, {
isStreaming: false,
isPaused: false,
currentMessage: '',
currentThoughts: [],
messageElement: null,
thoughtContainer: null,
abortController: null,
lastUpdateTime: Date.now(),
isVisible: false,
isUserStopped: false // 标记是否为用户主动停止
});
}
}
function setActiveConversation (conversationId) {
// 暂停当前激活的对话(如果存在)
if (currentActiveConversationId && currentActiveConversationId !== conversationId) {
pauseConversationStream(currentActiveConversationId);
}
// 激活新对话
currentActiveConversationId = conversationId;
initializeConversationState(conversationId);
// 切换对话时默认启用自动跟随底部
shouldAutoScroll = true;
const state = conversationStates.get(conversationId);
state.isVisible = true;
// 根据当前对话状态更新输入框UI
updateInputUIForConversation(conversationId);
// 如果该对话有正在进行的流式响应,恢复显示
if (state.isStreaming) {
if (state.isPaused) {
// 如果被暂停了,恢复显示
resumeConversationStream(conversationId);
} else {
// 如果正在流式响应但没有暂停,确保可见
state.isVisible = true;
updateConversationItemStatus(conversationId, 'streaming');
// 确保消息元素可见
if (state.messageElement) {
state.messageElement.style.opacity = '1';
state.messageElement.style.transform = 'translateY(0)';
// 滚动到消息位置
const chatMessages = document.getElementById('chat-messages');
if (chatMessages) {
state.messageElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
}
}
} else if (state.currentMessage || state.currentThoughts.length > 0) {
// 如果流式响应已完成,但还有内容,确保显示
if (state.messageElement) {
state.isVisible = true;
updateConversationItemStatus(conversationId, 'stopped');
// 确保消息元素可见
state.messageElement.style.opacity = '1';
state.messageElement.style.transform = 'translateY(0)';
// 滚动到消息位置
const chatMessages = document.getElementById('chat-messages');
if (chatMessages && state.messageElement) {
state.messageElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
}
}
// 更新全局状态:检查是否有任何对话正在流式响应
isWaitingForResponse = isAnyConversationStreaming();
}
// 根据对话状态更新输入框UI
function updateInputUIForConversation (conversationId) {
const state = conversationStates.get(conversationId);
const userInput = document.getElementById('user-input');
const sendButton = document.getElementById('send-btn');
if (!userInput || !sendButton) return;
// 检查当前对话是否正在流式响应
const isCurrentConversationStreaming = state && state.isStreaming && !state.isPaused;
if (isCurrentConversationStreaming) {
// 如果当前对话正在流式响应,设置输入框为回复中状态
userInput.classList.add('input-loading');
userInput.placeholder = '正在回复中...';
userInput.disabled = true;
// 更新发送按钮为停止按钮
sendButton.innerHTML = `
`;
sendButton.classList.add('stopping');
// 更新全局状态
isWaitingForResponse = true;
} else {
// 如果当前对话没有流式响应,恢复输入框为正常状态
userInput.classList.remove('input-loading');
userInput.placeholder = '请输入需要撰写的品类名和要求...';
userInput.disabled = false;
// 恢复发送按钮
sendButton.innerHTML = `
`;
sendButton.classList.remove('stopping');
// 更新全局状态
isWaitingForResponse = false;
}
}
// 新增:检查是否有任何对话正在流式响应
function isAnyConversationStreaming () {
for (const [conversationId, state] of conversationStates) {
if (state.isStreaming && !state.isPaused) {
return true;
}
}
return false;
}
// 新增:获取当前正在流式响应的对话ID
function getStreamingConversationId () {
for (const [conversationId, state] of conversationStates) {
if (state.isStreaming && !state.isPaused) {
return conversationId;
}
}
return null;
}
function pauseConversationStream (conversationId) {
const state = conversationStates.get(conversationId);
if (state && state.isStreaming) {
state.isPaused = true;
state.isVisible = false;
// 在思考过程区域显示"已暂停"提示
if (state.thoughtContainer && !state.thoughtContainer.querySelector('.paused-tip')) {
const pausedTip = document.createElement('div');
pausedTip.className = 'paused-tip';
pausedTip.textContent = '对话已切换,AI回复已暂停';
pausedTip.style.color = '#f59e0b';
pausedTip.style.marginTop = '8px';
pausedTip.style.fontSize = '0.9em';
pausedTip.style.fontStyle = 'italic';
state.thoughtContainer.appendChild(pausedTip);
}
// 更新侧边栏状态显示
updateConversationItemStatus(conversationId, 'paused');
}
}
function resumeConversationStream (conversationId) {
const state = conversationStates.get(conversationId);
if (state && state.isStreaming && state.isPaused) {
state.isPaused = false;
state.isVisible = true;
// 移除暂停提示
const pausedTip = state.thoughtContainer?.querySelector('.paused-tip');
if (pausedTip) {
pausedTip.remove();
}
// 更新侧边栏状态显示
updateConversationItemStatus(conversationId, 'streaming');
// 更新输入框状态
updateInputUIForConversation(conversationId);
// 恢复显示最新的思考过程
if (state.thoughtContainer && state.currentThoughts.length > 0) {
const thoughtContent = state.thoughtContainer.querySelector('.thought-content');
if (thoughtContent) {
const combinedThoughts = state.currentThoughts.join('');
// 使用思考过程专用的渲染函数
thoughtContent.innerHTML = renderThoughts(combinedThoughts);
}
}
// 恢复显示最新的消息内容
if (state.messageElement && state.currentMessage) {
const contentContainer = state.messageElement.querySelector('.message-content');
if (contentContainer) {
debouncedRenderMarkdown(contentContainer, state.currentMessage);
}
}
// 确保消息元素可见
if (state.messageElement) {
state.messageElement.style.opacity = '1';
state.messageElement.style.transform = 'translateY(0)';
// 滚动到当前对话的底部
const chatMessages = document.getElementById('chat-messages');
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
// 如果消息元素不在聊天区域中,重新添加到聊天区域
if (state.messageElement && !state.messageElement.parentNode) {
const chatMessages = document.getElementById('chat-messages');
if (chatMessages) {
chatMessages.appendChild(state.messageElement);
// 滚动到底部
setTimeout(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 100);
}
}
}
}
function stopConversationStream (conversationId) {
const state = conversationStates.get(conversationId);
if (state) {
// 标记为用户主动停止
state.isUserStopped = true;
state.isStreaming = false;
state.isPaused = false;
if (state.abortController) {
state.abortController.abort();
state.abortController = null;
}
// 移除暂停提示
const pausedTip = state.thoughtContainer?.querySelector('.paused-tip');
if (pausedTip) {
pausedTip.remove();
}
}
// 更新侧边栏状态显示
updateConversationItemStatus(conversationId, 'stopped');
// 更新全局状态:检查是否有任何对话正在流式响应
isWaitingForResponse = isAnyConversationStreaming();
// 如果停止的是当前激活的对话,更新输入框状态
if (conversationId === currentActiveConversationId) {
updateInputUIForConversation(conversationId);
}
}
function updateConversationItemStatus (conversationId, status) {
const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
if (conversationItem) {
const statusDot = conversationItem.querySelector('.conversation-status-dot');
// 移除所有状态类
conversationItem.classList.remove('streaming', 'paused');
// 添加新的状态类
if (status === 'streaming') {
conversationItem.classList.add('streaming');
if (statusDot) {
statusDot.style.opacity = '1';
statusDot.style.background = '#10b981';
}
} else if (status === 'paused') {
conversationItem.classList.add('paused');
if (statusDot) {
statusDot.style.opacity = '1';
statusDot.style.background = '#f59e0b';
}
} else if (status === 'stopped') {
if (statusDot) {
statusDot.style.opacity = '0';
}
}
}
}
// 创建字符计数器
const charCounter = document.createElement('div');
charCounter.className = 'char-counter';
charCounter.textContent = '0/' + maxMessageLength;
charCounter.id = 'char-counter';
// 将字符计数器添加到输入框的父元素中,以便定位在输入框内部
const inputContainer = document.querySelector('.chat-input-container');
if (inputContainer) {
inputContainer.style.position = 'relative';
inputContainer.appendChild(charCounter);
}
// 防抖动函数
function debounce (func, wait) {
return function () {
const context = this;
const args = arguments;
clearTimeout(renderDebounceTimer);
renderDebounceTimer = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 防抖动的Markdown渲染函数,确保光标跟随文字
const debouncedRenderMarkdown = debounce((element, text) => {
// 检查是否需要自动滚动
const wasAtBottom = shouldAutoScroll && isUserNearBottom();
// 使用 requestAnimationFrame 优化渲染性能
requestAnimationFrame(() => {
try {
// 移除现有光标
const existingCursor = element.querySelector('.cursor-blink');
if (existingCursor) existingCursor.remove();
// 使用统一的Markdown渲染函数
element.innerHTML = renderMarkdown(text);
// 添加代码高亮
applyCodeHighlighting(element);
// 处理审核结果表格
processAuditResultTables();
} catch (error) {
// 出错时使用简单文本显示
element.textContent = text;
}
// 根据情况滚动到底部
if (wasAtBottom) {
requestAnimationFrame(() => {
forceScrollToBottom();
});
}
});
}, 16); // 降低防抖时间到16ms,约等于60fps
// 改进 Markdown 解析器
function parseMarkdown (text) {
if (!markdownEnabled || !text) return text;
try {
// 检查marked是否存在
if (typeof marked === 'undefined') {
// 简单的备用实现:仅处理基本格式
const formattedText = text
.replace(/\n\n/g, '
') // 段落
.replace(/\n/g, '
') // 换行
.replace(/\*\*(.*?)\*\*/g, '$1') // 粗体
.replace(/\*(.*?)\*/g, '$1') // 斜体
.replace(/`(.*?)`/g, '$1'); // 行内代码
return `
`;
}
// 配置marked选项
marked.setOptions({
renderer: new marked.Renderer(),
highlight: function (code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
langPrefix: 'hljs language-', // 为代码块添加类名
pedantic: false,
gfm: true,
breaks: true,
sanitize: false,
smartypants: false,
xhtml: false
});
// 创建自定义渲染器
const renderer = new marked.Renderer();
// 定制表格渲染方法
renderer.table = function (header, body) {
// 检测表格中是否包含违规内容
const hasViolations = body.includes('violation-row');
// 添加audit-result类,确保能应用我们的CSS样式
return `\n\n${header}\n\n${body}\n
\n`;
};
// 定制表格行渲染方法
renderer.tablerow = function (content) {
// 检测行内是否包含违规关键词
const violationKeywords = ['违规', '违反', '不符合', '禁止', '风险', '投诉', '举报', '警告', '严重', '不通过'];
let rowClass = '';
if (violationKeywords.some(keyword => content.includes(keyword))) {
rowClass = 'violation-row';
} else if (content.includes('注意') || content.includes('警示') || content.includes('提醒')) {
rowClass = 'warning-row';
} else if (content.includes('正常') || content.includes('合规') || content.includes('通过')) {
rowClass = 'safe-row';
}
return `${content}
\n`;
};
// 定制段落渲染方法
renderer.paragraph = function (text) {
// 高亮审核结果关键词
if (text.includes('通过') && !text.includes('不通过')) {
text = text.replace(
/(.*?)(通过)(.*?)/g,
'$1$2$3'
);
}
if (text.includes('不通过')) {
text = text.replace(
/(.*?)(不通过)(.*?)/g,
'$1$2$3'
);
}
// 突出显示包含违规关键词的段落
const violationKeywords = ['违规', '违反', '不符合', '禁止', '风险', '投诉', '举报', '警告', '严重'];
violationKeywords.forEach(keyword => {
if (text.includes(keyword)) {
text = text.replace(
new RegExp(`(.*?)(${keyword})(.*?)`, 'g'),
'$1$2$3'
);
}
});
return `${text}
\n`;
};
// 使用定制的渲染器解析Markdown
marked.setOptions({ renderer: renderer });
// 解析Markdown文本
const parsed = marked.parse(text);
// 添加markdown-content类以应用样式
const result = `${parsed}
`;
return result;
} catch (error) {
// 发生错误时,回退到原始文本
return `${text}
`;
}
}
// 自动调整文本区域高度
function autoResizeTextarea () {
// 重置高度,以便重新计算
userInput.style.height = 'auto';
// 设置行高基准值
const lineHeight = 24; // 假设行高为24px
const minHeight = lineHeight + 12; // 最小高度,一行加上padding
const maxHeight = 200; // 最大高度
// 计算内容高度
const contentHeight = userInput.scrollHeight;
if (contentHeight <= minHeight) {
// 内容少于或等于一行
userInput.style.height = minHeight + 'px';
userInput.style.overflowY = 'hidden'; // 隐藏滚动条
} else if (contentHeight > maxHeight) {
// 内容超过最大高度
userInput.style.height = maxHeight + 'px';
userInput.style.overflowY = 'auto'; // 显示滚动条
} else {
// 内容在一行和最大高度之间
userInput.style.height = contentHeight + 'px';
userInput.style.overflowY = 'hidden'; // 隐藏滚动条
}
}
// 更新字符计数器
function updateCharCounter () {
const charCount = userInput.value.length;
const counterElement = document.getElementById('char-counter');
if (counterElement) {
counterElement.textContent = `${charCount}/${maxMessageLength}`;
// 当接近字符上限时改变样式提醒用户
counterElement.classList.remove('warning', 'exceeded');
if (charCount > maxMessageLength) {
counterElement.classList.add('exceeded');
} else if (charCount > maxMessageLength * 0.9) {
counterElement.classList.add('warning');
}
}
}
// 检查用户是否接近底部
function isUserNearBottom () {
return chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight < scrollThreshold;
}
// 强制滚动到底部
function forceScrollToBottom (force = false) {
// 如果正在编辑消息,不滚动
if (window.isEditingMessage) {
return;
}
if (!force && !shouldAutoScroll) {
return;
}
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// 添加用户消息
function addUserMessage (message, messageId = null) {
const messageElement = document.createElement('div');
messageElement.className = 'message user';
const contentElement = document.createElement('div');
contentElement.className = 'message-content';
contentElement.textContent = message;
messageElement.appendChild(contentElement);
// 如果有 messageId,添加编辑按钮
if (messageId) {
// 转义特殊字符,防止XSS攻击
const escapedContent = message
.replace(/\\/g, '\\\\')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(//g, '>')
.replace(/\n/g, '\\n');
const editButton = document.createElement('button');
editButton.className = 'edit-button';
editButton.setAttribute('title', '编辑消息');
editButton.innerHTML = `
`;
editButton.onclick = function () {
// 调用全局的 editMessage 函数(在 index.html 中定义)
if (typeof window.editMessage === 'function') {
window.editMessage(messageId, message);
}
};
messageElement.appendChild(editButton);
messageElement.setAttribute('data-message-id', messageId);
}
chatMessages.appendChild(messageElement);
// 确保消息立即可见
messageElement.style.opacity = '1';
messageElement.style.transform = 'translateY(0)';
// 滚动到底部
forceScrollToBottom(true);
return messageElement;
}
// 修改添加机器人消息函数
function addBotMessage (message, isMarkdown = true, thoughts = []) {
if (message.length <= 0 && (!thoughts || thoughts.length === 0)) {
return;
}
const botMessage = document.createElement('div');
botMessage.className = 'message bot-message';
// 先渲染思考过程(如有)
if (thoughts && Array.isArray(thoughts) && thoughts.length > 0) {
const thoughtContainer = document.createElement('div');
thoughtContainer.className = 'thought-container';
// 创建折叠按钮
const toggleBtn = document.createElement('button');
toggleBtn.className = 'thought-toggle-btn';
toggleBtn.innerHTML = '';
toggleBtn.title = '收起/展开思考过程';
toggleBtn.onclick = function (e) {
e.stopPropagation();
thoughtContainer.classList.toggle('collapsed');
const icon = toggleBtn.querySelector('svg path');
if (thoughtContainer.classList.contains('collapsed')) {
icon.setAttribute('d', 'M18 15L12 9L6 15');
} else {
icon.setAttribute('d', 'M6 9l6 6 6-6');
}
};
thoughtContainer.appendChild(toggleBtn);
const contentArea = document.createElement('div');
contentArea.className = 'thought-content';
// 处理思考过程,显示为连续的文本块
if (Array.isArray(thoughts)) {
// 如果是数组,合并后使用思考过程专用的渲染函数
const combinedThoughts = thoughts.join('');
contentArea.innerHTML = renderThoughts(combinedThoughts);
} else if (typeof thoughts === 'string') {
// 如果是字符串,使用思考过程专用的渲染函数
contentArea.innerHTML = renderThoughts(thoughts);
}
thoughtContainer.appendChild(contentArea);
botMessage.appendChild(thoughtContainer);
}
// 再渲染AI回复内容
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
if (isMarkdown) {
messageContent.innerHTML = renderMarkdown(message);
} else {
messageContent.innerText = message;
}
botMessage.appendChild(messageContent);
document.getElementById('chat-messages').appendChild(botMessage);
applyCodeHighlighting(botMessage);
processAuditResultTables();
// 为新添加的消息添加复制按钮
if (typeof window.setupCopyButtons === 'function') {
window.setupCopyButtons();
}
forceScrollToBottom();
return botMessage;
}
// 添加错误消息
function addErrorMessage (error, isAuthError = false) {
// 如果已经有打字动画,则移除
removeTypingAnimation();
const errorElement = document.createElement('div');
errorElement.className = 'error-message';
if (isAuthError) {
errorElement.innerHTML = `
正在尝试重新连接,如果失败将跳转到登录页面...
`;
} else {
errorElement.textContent = error;
}
chatMessages.appendChild(errorElement);
forceScrollToBottom();
// 认证错误显示更长时间,其他错误5秒后移除
const timeout = isAuthError ? 10000 : 5000;
setTimeout(() => {
if (errorElement.parentNode) {
errorElement.parentNode.removeChild(errorElement);
}
}, timeout);
}
// 生成UUID
function generateUUID () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 应用代码块高亮
function applyCodeHighlighting (element) {
// 检查hljs是否存在
if (typeof hljs === 'undefined') {
return;
}
try {
const codeBlocks = element.querySelectorAll('pre code');
for (const block of codeBlocks) {
hljs.highlightElement(block);
}
} catch (error) {
}
}
// 处理审核结果表格
function processAuditResultTables () {
const tables = document.querySelectorAll('table.audit-result');
tables.forEach(table => {
// 如果表格已经处理过,则跳过
if (table.dataset.processed) return;
// 标记表格已处理
table.dataset.processed = 'true';
// 处理表格内的长文本
const cells = table.querySelectorAll('td');
cells.forEach(cell => {
if (cell.textContent.length > 100) {
cell.classList.add('long-text');
}
});
});
}
// 修改发送消息函数
function sendMessage () {
const message = userInput.value.trim();
// 检查是否有任何对话正在流式响应
if (!message || isAnyConversationStreaming()) return;
if (message.length > maxMessageLength) {
alert(`消息长度不能超过${maxMessageLength}字符`);
return;
}
// 获取当前对话ID
const conversationId = window.currentConversationId || 'new';
// 初始化对话状态
initializeConversationState(conversationId);
setActiveConversation(conversationId);
// 确保当前对话可见
const state = conversationStates.get(conversationId);
if (state) {
state.isVisible = true;
}
// 展示用户消息(立即显示,不等待后端响应)
// 注意:此时还没有 message_id,后续会在收到后端响应后更新
const userMessageElement = addUserMessage(message);
// 将用户消息保存到对话状态中,确保切换对话时不会丢失
if (state) {
state.userMessages = state.userMessages || [];
state.userMessages.push({
content: message,
timestamp: new Date(),
element: userMessageElement,
messageId: null // 将在收到后端响应后设置
});
}
// 清空输入框
userInput.value = '';
autoResizeTextarea();
updateCharCounter();
// 更新按钮状态为停止按钮
sendButton.innerHTML = `
`;
sendButton.classList.add('stopping');
// 标记正在等待响应
isWaitingForResponse = true;
// 将输入框设置为loading状态
userInput.classList.add('input-loading');
userInput.placeholder = '正在回复中...';
userInput.disabled = true;
// 准备请求数据
const requestData = {
message: message,
user_id: userId,
agent: (localStorage.getItem('current_agent') || 'kuaishou'),
conversation_id: window.currentConversationId || null,
session_id: window.currentSessionId || null,
force_new: window.forceNewConversation || false
};
// 重置强制创建新对话的标志
window.forceNewConversation = false;
// 根据选择的模式,使用不同的请求方式
if (useStreamingMode) {
handleStreamResponse(message, conversationId);
} else {
handleStandardResponse(message, requestData);
}
isStopped = false; // 每次发送消息时重置
}
// 修改处理流式响应函数
function handleStreamResponse (message, conversationId) {
const state = conversationStates.get(conversationId);
if (!state) {
return;
}
// 重置对话状态
state.isStreaming = true;
state.isPaused = false;
state.isUserStopped = false; // 重置用户停止标志
state.currentMessage = '';
state.currentThoughts = [];
state.lastUpdateTime = Date.now();
// 更新侧边栏状态显示
updateConversationItemStatus(conversationId, 'streaming');
let fullMessage = '';
let lastProcessedMessage = '';
let lastRenderTime = 0;
const RENDER_INTERVAL = 16; // 16ms,约等于60fps
let allThoughts = [];
let thoughtContainer = null; // 新增思考过程容器
let thoughtContent = null; // 新增思考内容包裹层
let thoughtToggleBtn = null; // 折叠按钮
let contentContainer = null; // 结果内容容器,初始不创建
try {
// 创建AbortController用于取消请求
state.abortController = new AbortController();
// 创建机器人消息元素
const messageDiv = document.createElement('div');
messageDiv.className = 'message bot';
// 创建思考过程容器
thoughtContainer = document.createElement('div');
thoughtContainer.className = 'thought-container';
// 创建折叠按钮
thoughtToggleBtn = document.createElement('button');
thoughtToggleBtn.className = 'thought-toggle-btn';
// 展开时默认向上箭头
thoughtToggleBtn.innerHTML = ''
+ ''
+ '';
thoughtToggleBtn.title = '收起/展开思考过程';
thoughtToggleBtn.onclick = function (e) {
e.stopPropagation();
thoughtContainer.classList.toggle('collapsed');
// 切换SVG图标
const icon = thoughtToggleBtn.querySelector('.toggle-icon');
if (thoughtContainer.classList.contains('collapsed')) {
// 向下箭头
icon.innerHTML = '';
} else {
// 向上箭头
icon.innerHTML = '';
}
};
thoughtContainer.appendChild(thoughtToggleBtn);
// 创建思考内容包裹层
thoughtContent = document.createElement('div');
thoughtContent.className = 'thought-content';
thoughtContainer.appendChild(thoughtContent);
messageDiv.appendChild(thoughtContainer);
// 保存到对话状态
state.messageElement = messageDiv;
state.thoughtContainer = thoughtContainer;
// 添加到聊天区域
chatMessages.appendChild(state.messageElement);
// 确保消息元素可见
messageDiv.style.opacity = '1';
messageDiv.style.transform = 'translateY(0)';
// 添加对话ID标识,便于后续管理
messageDiv.setAttribute('data-conversation-id', conversationId);
// 滚动到底部
forceScrollToBottom();
// 始终使用POST方法发送所有消息
fetch('/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
user_id: userId,
agent: (localStorage.getItem('current_agent') || 'kuaishou'),
conversation_id: window.currentConversationId || null,
session_id: window.currentSessionId || null,
force_new: window.forceNewConversation || false
}),
signal: state.abortController.signal
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// 创建新的EventSource,包装响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function readStream () {
reader.read().then(({ done, value }) => {
if (done) {
// 流式响应完成
state.isStreaming = false;
updateConversationItemStatus(conversationId, 'stopped');
resetUIState();
return;
}
// 解码二进制数据为文本
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// 处理buffer中完整的SSE消息
const lines = buffer.split('\n\n');
buffer = lines.pop() || ''; // 最后一个可能不完整,保留到下次处理
lines.forEach(line => {
if (line.startsWith('data: ')) {
try {
const eventData = JSON.parse(line.substring(6));
processEventData(eventData, state);
} catch (error) {
}
}
});
// 无论是否暂停,都继续读取流数据
readStream(); // 继续读取下一块数据
}).catch(error => {
// 如果是用户主动停止,不显示错误提示
if (!state.isPaused && !state.isUserStopped && error.name !== 'AbortError') {
addErrorMessage('连接错误,请重试');
}
state.isStreaming = false;
state.isUserStopped = false; // 重置标志
resetUIState();
});
}
readStream(); // 开始读取流
})
.catch(error => {
// 如果是用户主动停止(AbortError),不显示错误提示
if (!state.isPaused && !state.isUserStopped && error.name !== 'AbortError') {
if (error.message.includes('401')) {
addErrorMessage('登录已过期,正在重新连接...', true);
// 尝试刷新token
tryRefreshToken().then(refreshed => {
if (!refreshed) {
clearAuthAndRedirect();
}
});
} else {
addErrorMessage('连接错误,请重试: ' + error.message);
}
}
state.isStreaming = false;
state.isUserStopped = false; // 重置标志
updateConversationItemStatus(conversationId, 'stopped');
resetUIState();
});
// 提取通用的事件数据处理逻辑为函数
function processEventData (data, conversationState) {
// 更新最后更新时间
conversationState.lastUpdateTime = Date.now();
// 处理用户消息ID(在初始响应中返回)
if (data.user_message_id) {
// 更新最后添加的用户消息,为其添加编辑按钮
if (conversationState.userMessages && conversationState.userMessages.length > 0) {
const lastUserMessage = conversationState.userMessages[conversationState.userMessages.length - 1];
if (lastUserMessage && !lastUserMessage.messageId) {
lastUserMessage.messageId = data.user_message_id;
// 更新消息元素,添加编辑按钮
if (lastUserMessage.element) {
const messageElement = lastUserMessage.element;
const messageId = data.user_message_id;
const messageContent = lastUserMessage.content;
// 转义特殊字符
const escapedContent = messageContent
.replace(/\\/g, '\\\\')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(//g, '>')
.replace(/\n/g, '\\n');
// 创建编辑按钮
const editButton = document.createElement('button');
editButton.className = 'edit-button';
editButton.setAttribute('title', '编辑消息');
editButton.innerHTML = `
`;
editButton.onclick = function () {
if (typeof window.editMessage === 'function') {
window.editMessage(messageId, messageContent);
}
};
messageElement.appendChild(editButton);
messageElement.setAttribute('data-message-id', messageId);
}
}
}
}
if (!data.success) {
if (data.error && data.error.includes('401')) {
addErrorMessage('登录已过期,正在重新连接...', true);
// 尝试刷新token
tryRefreshToken().then(refreshed => {
if (!refreshed) {
clearAuthAndRedirect();
}
});
} else {
addErrorMessage(data.error || '发生未知错误');
}
conversationState.isStreaming = false;
updateConversationItemStatus(conversationId, 'stopped');
resetUIState();
return;
}
// 处理思考过程
if (data.thought) {
allThoughts.push(data.thought);
conversationState.currentThoughts = allThoughts;
// 显示为连续的文本块,不添加换行符
if (thoughtContent) {
const combinedThoughts = allThoughts.join('');
// 使用思考过程专用的渲染函数
thoughtContent.innerHTML = renderThoughts(combinedThoughts);
// 只有在可见且未暂停时才滚动
if (conversationState.isVisible && !conversationState.isPaused) {
forceScrollToBottom();
}
}
return;
}
if (data.chunk) {
fullMessage += data.chunk;
conversationState.currentMessage = fullMessage;
// 第一次收到chunk时再创建contentContainer
if (!contentContainer) {
contentContainer = document.createElement('div');
contentContainer.className = 'message-content typing';
messageDiv.appendChild(contentContainer);
}
// 使用 requestAnimationFrame 优化渲染性能
const now = performance.now();
if (now - lastRenderTime >= RENDER_INTERVAL) {
requestAnimationFrame(() => {
if (fullMessage !== lastProcessedMessage && contentContainer) {
// 总是渲染内容,但只在可见且未暂停时滚动
debouncedRenderMarkdown(contentContainer, fullMessage);
lastProcessedMessage = fullMessage;
if (conversationState.isVisible && !conversationState.isPaused && isUserNearBottom()) {
requestAnimationFrame(() => {
forceScrollToBottom();
});
}
}
});
lastRenderTime = now;
}
}
if (data.session_id) {
// 更新当前对话的session_id
window.currentSessionId = data.session_id;
}
if (data.conversation_id) {
// 更新当前对话ID
window.currentConversationId = data.conversation_id;
// 更新对话状态中的对话ID
if (conversationId !== data.conversation_id) {
// 获取当前状态
const currentState = conversationStates.get(conversationId);
if (currentState) {
// 更新状态到新的对话ID
conversationStates.set(data.conversation_id, currentState);
conversationStates.delete(conversationId);
// 更新当前激活的对话ID
if (currentActiveConversationId === conversationId) {
currentActiveConversationId = data.conversation_id;
}
}
}
}
if (data.done) {
if (contentContainer) {
contentContainer.classList.remove('typing');
// 只在done时渲染最终结果
debouncedRenderMarkdown(contentContainer, fullMessage);
}
conversationState.isStreaming = false;
updateConversationItemStatus(conversationId, 'stopped');
resetUIState();
userInput.focus();
if (conversationState.isVisible) {
forceScrollToBottom();
}
// 为流式响应完成的消息添加复制按钮
if (typeof window.setupCopyButtons === 'function') {
window.setupCopyButtons();
}
// 对话完成后刷新历史记录
if (typeof loadConversationHistory === 'function') {
// 添加延迟确保数据库保存完成
setTimeout(() => {
loadConversationHistory(true); // 自动选中最新的对话
}, 500);
}
// 如果是新对话,更新侧边栏选中状态
if (data.conversation_id && data.conversation_id !== conversationId) {
if (typeof updateConversationSelection === 'function') {
updateConversationSelection(data.conversation_id);
}
}
// 更新对话内容缓存
if (typeof updateConversationCache === 'function') {
updateConversationCache(conversationId, fullMessage, allThoughts);
}
return;
}
}
} catch (error) {
addErrorMessage('连接错误,请重试');
state.isStreaming = false;
updateConversationItemStatus(conversationId, 'stopped');
resetUIState();
}
}
// 修改处理标准响应函数
function handleStandardResponse (message, requestData) {
abortController = new AbortController();
fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData),
signal: abortController.signal
})
.then(response => response.json())
.then(data => {
if (data.success) {
addBotMessage(data.message, true, data.thoughts);
// 更新当前对话ID和session_id
if (data.conversation_id) {
window.currentConversationId = data.conversation_id;
}
if (data.session_id) {
window.currentSessionId = data.session_id;
}
// 对话完成后刷新历史记录
if (typeof loadConversationHistory === 'function') {
// 添加延迟确保数据库保存完成
setTimeout(() => {
loadConversationHistory(true); // 自动选中最新的对话
}, 500);
}
// 如果是新对话,更新侧边栏选中状态
if (data.conversation_id) {
if (typeof updateConversationSelection === 'function') {
updateConversationSelection(data.conversation_id);
}
}
} else {
if (data.error && data.error.includes('401')) {
addErrorMessage('登录已过期,正在重新连接...', true);
// 尝试刷新token
tryRefreshToken().then(refreshed => {
if (!refreshed) {
clearAuthAndRedirect();
}
});
} else {
addErrorMessage(data.error || '发生未知错误');
}
}
})
.catch(error => {
if (error.name === 'AbortError') {
addErrorMessage('请求已取消');
} else {
addErrorMessage('连接错误,请重试');
}
})
.finally(() => {
document.querySelectorAll('.message-content.typing').forEach(el => {
el.classList.remove('typing');
});
resetUIState();
});
}
// 修改停止请求函数
function stopRequest () {
isStopped = true; // 新增:设置停止标志
// 停止当前激活对话的流式响应
if (currentActiveConversationId) {
stopConversationStream(currentActiveConversationId);
}
if (eventSource) {
closeEventSource();
}
if (abortController) {
abortController.abort();
abortController = null;
}
// 重置为发送按钮
resetUIState();
// 重置消息元素引用
currentBotMessageElement = null;
currentBotMessageContainer = null;
}
// 关闭EventSource连接
function closeEventSource () {
if (eventSource) {
eventSource.close();
eventSource = null;
}
}
// 修改重置UI状态函数
function resetUIState () {
// 更新全局状态:检查是否有任何对话正在流式响应
isWaitingForResponse = isAnyConversationStreaming();
// 根据当前激活的对话状态来重置UI
if (currentActiveConversationId) {
updateInputUIForConversation(currentActiveConversationId);
} else {
// 如果没有激活的对话,恢复默认状态
sendButton.innerHTML = `
`;
sendButton.classList.remove('stopping');
// 恢复输入框状态
userInput.classList.remove('input-loading');
userInput.placeholder = '请输入需要撰写的品类名和要求...';
userInput.disabled = false;
}
// 重置消息元素引用
currentBotMessageElement = null;
currentBotMessageContainer = null;
// 如果有加载动画,移除它
if (currentLoadingElement) {
currentLoadingElement.remove();
currentLoadingElement = null;
}
}
// 重置对话
function resetConversation () {
// 如果正在确认,则执行重置
if (resetButton.classList.contains('confirming')) {
// 发送重置请求
fetch('/api/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ user_id: userId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 清空聊天记录,仅保留欢迎消息
while (chatMessages.childNodes.length > 1) {
chatMessages.removeChild(chatMessages.lastChild);
}
// 清除当前对话ID
window.currentConversationId = null;
// 重置按钮状态
resetButton.classList.remove('confirming');
resetButton.textContent = '重置对话';
// 显示提示
addBotMessage('对话已重置,您可以开始新的对话了。');
} else {
addErrorMessage(data.error || '重置失败,请重试');
}
})
.catch(error => {
addErrorMessage('重置失败,请重试');
});
} else {
// 进入确认状态
resetButton.classList.add('confirming');
resetButton.textContent = '确认重置';
// 5秒后如果没有点击,恢复原状
setTimeout(() => {
if (resetButton.classList.contains('confirming')) {
resetButton.classList.remove('confirming');
resetButton.textContent = '重置对话';
}
}, 5000);
}
}
// 添加事件监听器
userInput.addEventListener('input', () => {
autoResizeTextarea();
updateCharCounter();
});
// 在输入框内按Enter发送消息(Shift+Enter换行),状态与发送按钮一致
userInput.addEventListener('keydown', (e) => {
if (e.key !== 'Enter') return;
if (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey || e.isComposing) return;
e.preventDefault(); // 阻止默认换行
if (isAnyConversationStreaming()) {
stopRequest(); // 和按钮行为保持一致:正在流式时改为停止
return;
}
if (userInput.disabled) return;
sendMessage();
});
// 按下Escape键时取消请求
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isAnyConversationStreaming()) {
e.preventDefault();
stopRequest();
}
});
// 点击发送按钮
sendButton.addEventListener('click', () => {
if (isAnyConversationStreaming()) {
stopRequest();
} else {
sendMessage();
}
});
// 重置按钮点击事件
resetButton.addEventListener('click', resetConversation);
// 监听滚动事件
chatMessages.addEventListener('scroll', () => {
// 记录当前滚动位置
const currentScrollTop = chatMessages.scrollTop;
const maxScrollTop = chatMessages.scrollHeight - chatMessages.clientHeight;
if (currentScrollTop < lastScrollTop - 2) {
shouldAutoScroll = false;
} else if (maxScrollTop - currentScrollTop <= scrollThreshold) {
shouldAutoScroll = true;
}
lastScrollTop = currentScrollTop;
});
// 初始化调整文本区域高度
autoResizeTextarea();
// 聚焦到输入框
userInput.focus();
// 初始化字符计数器
updateCharCounter();
// 向输入框添加水波纹效果
function createRipple (event) {
const button = event.currentTarget;
// 如果按钮被禁用,则不创建水波纹
if (button.disabled) return;
const ripple = document.createElement('span');
ripple.classList.add('ripple');
button.appendChild(ripple);
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = `${size}px`;
ripple.style.left = `${x}px`;
ripple.style.top = `${y}px`;
// 动画结束后移除水波纹元素
setTimeout(() => {
ripple.remove();
}, 600);
}
// 添加水波纹效果
sendButton.addEventListener('click', createRipple);
resetButton.addEventListener('click', createRipple);
// 移除打字动画
function removeTypingAnimation () {
document.querySelectorAll('.message-content.typing').forEach(el => {
el.classList.remove('typing');
});
}
// 认证相关函数 - 增强版本
async function checkAuthStatusInChat () {
try {
const token = localStorage.getItem('auth_token');
const userInfo = localStorage.getItem('user_info');
if (!token || !userInfo) {
window.location.href = '/login';
return;
}
// 检查token是否即将过期(提前1小时检查)
if (isTokenExpiringSoon(token)) {
const refreshed = await tryRefreshToken();
if (!refreshed) {
clearAuthAndRedirect();
return;
}
}
// 显示本地用户信息
try {
const user = JSON.parse(userInfo);
displayUserInfo(user);
showUserSection();
addAuthHeader(token);
} catch (e) {
clearAuthAndRedirect();
}
} catch (error) {
clearAuthAndRedirect();
}
}
// 检查token是否即将过期
function isTokenExpiringSoon (token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const exp = payload.exp * 1000; // 转换为毫秒
const now = Date.now();
const oneHour = 60 * 60 * 1000; // 1小时
return (exp - now) < oneHour;
} catch (e) {
return true; // 解析失败,认为已过期
}
}
// 尝试刷新token(单飞模式,避免并发刷新导致状态错乱)
let isRefreshing = false;
let refreshPromise = null;
async function tryRefreshToken () {
// 如果已有刷新任务在进行,直接复用该任务结果
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = (async () => {
if (isRefreshing) return false;
try {
isRefreshing = true;
const token = localStorage.getItem('auth_token');
if (!token) {
return false;
}
// 使用原生fetch,避免被addAuthHeader包装后递归进入刷新逻辑
const nativeFetch = window.__nativeFetch || window.fetch;
const response = await nativeFetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (response.ok && data.success && data.token) {
localStorage.setItem('auth_token', data.token);
return true;
}
if (response.status === 401 ||
data.code === 'TOKEN_EXPIRED_NO_SESSION' ||
data.code === 'MISSING_TOKEN') {
return false;
}
return false;
} catch (error) {
return false;
} finally {
isRefreshing = false;
refreshPromise = null;
}
})();
return refreshPromise;
}
function clearAuthAndRedirect () {
if (isRedirecting) {
return;
}
isRedirecting = true;
// 清除本地存储
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
// 清除session存储(如果有)
sessionStorage.removeItem('auth_token');
sessionStorage.removeItem('user_info');
// 立即跳转,不使用setTimeout
try {
window.location.replace('/login');
} catch (error) {
window.location.href = '/login';
}
}
function showUserSection () {
const userInfo = document.getElementById('user-info');
if (userInfo) userInfo.style.display = 'flex';
}
function addAuthHeader () {
// 避免重复包装fetch
if (!window.__nativeFetch) {
window.__nativeFetch = window.fetch;
}
if (window.__authFetchWrapped) {
return;
}
window.__authFetchWrapped = true;
const originalFetch = window.__nativeFetch;
window.fetch = async function (url, options = {}) {
if (options.headers === undefined) {
options.headers = {};
}
// 每次请求都读取最新token,避免闭包持有旧token导致长期运行后401
const latestToken = localStorage.getItem('auth_token');
if (latestToken) {
options.headers['Authorization'] = `Bearer ${latestToken}`;
}
try {
const response = await originalFetch(url, options);
// 检查401错误,但不要对refresh接口本身进行重试
if (response.status == 401) {
// 如果是refresh接口返回401,直接跳转登录,不要再次尝试刷新
const urlString = typeof url === 'string' ? url : url.toString();
if (urlString.includes('/api/auth/refresh')) {
clearAuthAndRedirect();
return response;
}
const refreshed = await tryRefreshToken();
if (refreshed) {
// 更新token后重试请求
const retryToken = localStorage.getItem('auth_token');
if (retryToken) {
options.headers['Authorization'] = `Bearer ${retryToken}`;
}
return originalFetch(url, options);
} else {
// 刷新失败,清除认证信息并跳转登录
clearAuthAndRedirect();
return response;
}
}
return response;
} catch (error) {
throw error;
}
};
}
// 将addAuthHeader函数暴露到全局作用域
window.addAuthHeader = addAuthHeader;
// 定期更新对话状态
function startConversationStatusUpdater () {
setInterval(() => {
updateAllConversationStatuses();
}, 2000); // 每2秒更新一次
}
function updateAllConversationStatuses () {
conversationStates.forEach((state, conversationId) => {
if (state.isStreaming) {
// 检查流式响应是否还在进行
const now = Date.now();
const timeSinceLastUpdate = now - state.lastUpdateTime;
// 如果超过30秒没有更新,认为流式响应已结束
if (timeSinceLastUpdate > 30000) {
state.isStreaming = false;
state.isPaused = false;
updateConversationItemStatus(conversationId, 'stopped');
}
}
});
}
// 将对话状态管理函数和变量暴露到全局作用域
window.conversationStates = conversationStates;
window.currentActiveConversationId = currentActiveConversationId;
window.setActiveConversation = setActiveConversation;
window.pauseConversationStream = pauseConversationStream;
window.resumeConversationStream = resumeConversationStream;
window.stopConversationStream = stopConversationStream;
window.initializeConversationState = initializeConversationState;
window.updateConversationItemStatus = updateConversationItemStatus;
window.startConversationStatusUpdater = startConversationStatusUpdater;
window.isAnyConversationStreaming = isAnyConversationStreaming;
window.getStreamingConversationId = getStreamingConversationId;
window.updateInputUIForConversation = updateInputUIForConversation;
window.renderMarkdown = renderMarkdown;
window.renderThoughts = renderThoughts;
function displayUserInfo (userInfo) {
const userInfoElement = document.getElementById('user-info');
const userNameElement = document.getElementById('user-name');
const userTypeElement = document.getElementById('user-type');
const userTitleElement = document.getElementById('user-title');
const avatarImg = document.getElementById('avatar-img');
const avatarFallback = document.getElementById('avatar-fallback');
const userDropdown = document.getElementById('user-dropdown');
if (!userInfoElement || !userNameElement) return;
// 显示用户名
userNameElement.textContent = userInfo.nickname || userInfo.name || '用户';
// 显示用户类型
if (userTypeElement) {
const typeMap = {
'dingtalk': '钉钉用户',
'wecom': '企业微信用户'
};
userTypeElement.textContent = typeMap[userInfo.login_type] || '用户';
}
// 显示用户职位
if (userTitleElement && userInfo.title) {
userTitleElement.textContent = userInfo.title;
userTitleElement.style.display = 'block';
}
// 显示用户头像
if (avatarImg && avatarFallback) {
if (userInfo.avatar) {
avatarImg.src = userInfo.avatar;
avatarImg.style.display = 'block';
avatarFallback.style.display = 'none';
// 添加头像加载失败处理
avatarImg.onerror = function () {
this.style.display = 'none';
avatarFallback.style.display = 'flex';
const firstChar = (userInfo.name || 'U').charAt(0).toUpperCase();
avatarFallback.textContent = firstChar;
};
} else {
avatarImg.style.display = 'none';
avatarFallback.style.display = 'flex';
const firstChar = (userInfo.name || 'U').charAt(0).toUpperCase();
avatarFallback.textContent = firstChar;
}
}
// 显示用户详细信息下拉框
if (userDropdown) {
displayUserDetails(userInfo);
}
userInfoElement.style.display = 'flex';
}
function displayUserDetails (userInfo) {
// 显示邮箱信息
const emailElement = document.getElementById('user-email');
if (emailElement && userInfo.email) {
emailElement.querySelector('.info-value').textContent = userInfo.email;
emailElement.style.display = 'flex';
}
// 显示手机信息
const mobileElement = document.getElementById('user-mobile');
if (mobileElement && userInfo.mobile) {
mobileElement.querySelector('.info-value').textContent = userInfo.mobile;
mobileElement.style.display = 'flex';
}
// 显示工号信息
const jobElement = document.getElementById('user-job');
if (jobElement && userInfo.job_number) {
jobElement.querySelector('.info-value').textContent = userInfo.job_number;
jobElement.style.display = 'flex';
}
// 显示部门信息
const deptElement = document.getElementById('user-dept');
if (deptElement && userInfo.dept_name) {
deptElement.querySelector('.info-value').textContent = userInfo.dept_name;
deptElement.style.display = 'flex';
}
// 如果有任何详细信息,显示下拉框
const userDropdown = document.getElementById('user-dropdown');
if (userDropdown && (userInfo.email || userInfo.mobile || userInfo.job_number || userInfo.dept_name)) {
userDropdown.style.display = 'block';
}
}
async function handleLogout () {
try {
const token = localStorage.getItem('auth_token');
// 调用退出登录API
const response = await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
// 清除本地存储
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
// 跳转到登录页
window.location.href = '/login';
} catch (error) {
// 即使API调用失败,也清除本地存储并跳转
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
window.location.href = '/login';
}
}
});