|
|
@@ -0,0 +1,3714 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>聚德策划大师-大健康AI文案撰写</title>
|
|
|
+ <meta name="description" content="聚德AI短视频脚本撰写器 - 专业高效的文案创作平台">
|
|
|
+ <meta name="theme-color" content="#6366f1" media="(prefers-color-scheme: light)">
|
|
|
+ <meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
|
|
+ <meta name="color-scheme" content="light dark">
|
|
|
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
|
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/modal.css') }}">
|
|
|
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/components/copy-button.css') }}">
|
|
|
+ <link rel="icon" href="{{ url_for('static', filename='logo.png') }}">
|
|
|
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/github.min.css') }}">
|
|
|
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/highlightjs.min.css') }}">
|
|
|
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
|
|
|
+
|
|
|
+ <!-- 思考过程样式 -->
|
|
|
+ <style>
|
|
|
+ /* 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;
|
|
|
+ white-space: normal;
|
|
|
+ /* 正常换行处理 */
|
|
|
+ word-wrap: break-word;
|
|
|
+ /* 长单词换行 */
|
|
|
+ line-height: 1.4;
|
|
|
+ /* 适中的行高 */
|
|
|
+ text-align: left;
|
|
|
+ /* 左对齐 */
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 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;
|
|
|
+ font-style: italic;
|
|
|
+ color: #7952b3;
|
|
|
+ background: none;
|
|
|
+ border-bottom: none;
|
|
|
+ padding-bottom: 0;
|
|
|
+ line-height: 1.6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thought-container p:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 对话项样式优化 */
|
|
|
+ .conversation-meta {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 0.75rem;
|
|
|
+ color: #666;
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-count {
|
|
|
+ font-size: 0.7rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .last-time {
|
|
|
+ font-size: 0.7rem;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.active .conversation-meta {
|
|
|
+ color: rgba(99, 102, 241, 0.8);
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.active .last-time {
|
|
|
+ color: rgba(99, 102, 241, 0.6);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 对话状态指示器样式 */
|
|
|
+ .conversation-item.streaming .conversation-title {
|
|
|
+ color: #10b981;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.paused .conversation-title {
|
|
|
+ color: #f59e0b;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.streaming .conversation-meta {
|
|
|
+ color: #10b981;
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.paused .conversation-meta {
|
|
|
+ color: #f59e0b;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 当前活跃对话指示器 */
|
|
|
+ .conversation-item.active {
|
|
|
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(99, 102, 241, 0.05));
|
|
|
+ border-left: 3px solid #6366f1;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /* 对话切换动画 */
|
|
|
+ .conversation-item {
|
|
|
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item:hover {
|
|
|
+ background: rgba(99, 102, 241, 0.05);
|
|
|
+ transform: translateX(2px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.active:hover {
|
|
|
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(99, 102, 241, 0.08));
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 对话状态点指示器 */
|
|
|
+ .conversation-status-dot {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ right: 12px;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.streaming .conversation-status-dot {
|
|
|
+ background: #10b981;
|
|
|
+ opacity: 1;
|
|
|
+ animation: pulse-dot 2s infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.paused .conversation-status-dot {
|
|
|
+ background: #f59e0b;
|
|
|
+ opacity: 1;
|
|
|
+ animation: pulse-dot 2s infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .conversation-item.active .conversation-status-dot {
|
|
|
+ background: #6366f1;
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes pulse-dot {
|
|
|
+ 0% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ 50% {
|
|
|
+ opacity: 0.5;
|
|
|
+ transform: scale(1.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 编辑按钮样式 - 与复制按钮保持一致 */
|
|
|
+ .edit-button {
|
|
|
+ position: absolute;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ background: var(--primary-indigo);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ z-index: 1001;
|
|
|
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
|
|
+ transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
|
|
+ opacity: 0;
|
|
|
+ bottom: 8px;
|
|
|
+ right: 48px;
|
|
|
+ /* 位于复制按钮左侧 */
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 悬停时显示编辑按钮 */
|
|
|
+ .message.user:hover .edit-button {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-button:hover {
|
|
|
+ transform: scale(1.1);
|
|
|
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.25);
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-button svg {
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 内联编辑样式 */
|
|
|
+ .message-editing {
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-form {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 16px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border: 2px solid #6366f1;
|
|
|
+ border-radius: 12px;
|
|
|
+ margin: 8px 0;
|
|
|
+ width: 600px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-textarea {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 80px;
|
|
|
+ padding: 12px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+ resize: vertical;
|
|
|
+ font-family: inherit;
|
|
|
+ background: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-textarea:focus {
|
|
|
+ outline: none;
|
|
|
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-cancel-btn {
|
|
|
+ padding: 4px 10px;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 6px;
|
|
|
+ color: #666;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-cancel-btn:hover {
|
|
|
+ background: #f5f5f5;
|
|
|
+ border-color: #ccc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-send-btn {
|
|
|
+ padding: 4px 10px;
|
|
|
+ background: #6366f1;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ color: white;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-send-btn:hover {
|
|
|
+ background: #5b5cf6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-send-btn:disabled {
|
|
|
+ background: #ccc;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+
|
|
|
+</head>
|
|
|
+
|
|
|
+<body>
|
|
|
+ <div class="glass-particles"></div>
|
|
|
+
|
|
|
+ <!-- 移动端侧边栏切换按钮 -->
|
|
|
+ <button class="sidebar-toggle" id="sidebar-toggle" style="display: none;">
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M3 12h18M3 6h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 移动端侧边栏遮罩层 -->
|
|
|
+ <div class="sidebar-overlay" id="sidebar-overlay"></div>
|
|
|
+
|
|
|
+ <div class="chat-container">
|
|
|
+ <!-- 历史记录侧边栏 -->
|
|
|
+ <div class="history-sidebar" id="history-sidebar">
|
|
|
+ <div class="sidebar-header">
|
|
|
+ <button class="new-chat-btn" id="new-chat-btn">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ 开启新对话
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="history-content">
|
|
|
+ <!-- 历史记录将通过JavaScript动态加载 -->
|
|
|
+ <div class="history-loading" id="history-loading"
|
|
|
+ style="display: flex; align-items: center; justify-content: center; padding: 20px; color: #666;">
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
|
|
+ style="margin-right: 8px; animation: spin 1s linear infinite;">
|
|
|
+ <path d="M21 12a9 9 0 11-6.219-8.56" stroke="currentColor" stroke-width="2"
|
|
|
+ stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ 正在加载历史记录...
|
|
|
+ </div>
|
|
|
+ <div class="history-empty" id="history-empty"
|
|
|
+ style="display: none; text-align: center; padding: 40px 20px; color: #999;">
|
|
|
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
|
|
+ style="margin-bottom: 16px; opacity: 0.5;">
|
|
|
+ <path d="M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z"
|
|
|
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ <div style="font-size: 16px; margin-bottom: 8px;">暂无历史记录</div>
|
|
|
+ <div style="font-size: 14px; opacity: 0.7;">开始新的对话吧!</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主聊天区域 -->
|
|
|
+ <div class="main-chat-area">
|
|
|
+ <header class="glass-header">
|
|
|
+ <div class="logo-title-container">
|
|
|
+ <img src="{{ url_for('static', filename='logo.png') }}" alt="聚德AI短视频脚本撰写" class="header-logo">
|
|
|
+ <h1>聚德策划大师-大健康AI文案撰写<span class="version-tag">v 1.6.3</span></h1>
|
|
|
+ <a class="logo-title-link" href="https://wj.qq.com/s2/23610733/1a16/" target="_blank"
|
|
|
+ class="jude-link">问题反馈</a>
|
|
|
+ </div>
|
|
|
+ <div class="buttons-container">
|
|
|
+ <!-- 智能体选择(仅智能策划大师) -->
|
|
|
+ <div id="agent-dropdown" class="glass-btn" style="position: relative;">
|
|
|
+ <img id="agent-trigger-icon" src="{{ url_for('static', filename='img/planning.png') }}"
|
|
|
+ alt="平台图标" width="16" height="16" onerror="this.style.display='none'">
|
|
|
+ <span id="agent-trigger-text">智能策划大师</span>
|
|
|
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ <div id="agent-menu" class="glass-panel"
|
|
|
+ style="display:none; position: absolute; top: calc(100% + 6px); left: 0; z-index: 1001; min-width: 220px;">
|
|
|
+ <ul style="list-style: none; margin: 0; padding: 6px;">
|
|
|
+ <li class="agent-option" data-agent="planning"
|
|
|
+ style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
|
|
|
+ onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
|
|
|
+ onmouseout="this.style.background=''">
|
|
|
+ <img src="{{ url_for('static', filename='img/planning.png') }}" alt="智能策划大师"
|
|
|
+ width="16" height="16" onerror="this.style.display='none'">
|
|
|
+ <div style="display:flex; flex-direction:column;">
|
|
|
+ <span style="font-weight: 500;">智能策划大师</span>
|
|
|
+ </div>
|
|
|
+ </li>
|
|
|
+ <li class="agent-option" data-agent="videoaccount"
|
|
|
+ style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
|
|
|
+ onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
|
|
|
+ onmouseout="this.style.background=''">
|
|
|
+ <img src="{{ url_for('static', filename='img/videoaccount.svg') }}" alt="病理分析"
|
|
|
+ width="16" height="16" onerror="this.style.display='none'">
|
|
|
+ <div style="display:flex; flex-direction:column;">
|
|
|
+ <span style="font-weight: 500;">病理分析</span>
|
|
|
+ </div>
|
|
|
+ </li>
|
|
|
+
|
|
|
+ <!-- 投放诊断 -->
|
|
|
+ <li class="agent-option" data-agent="diagnosis"
|
|
|
+ style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
|
|
|
+ onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
|
|
|
+ onmouseout="this.style.background=''">
|
|
|
+ <img src="{{ url_for('static', filename='img/diagnosis.svg') }}" alt="投放诊断"
|
|
|
+ width="16" height="16" onerror="this.style.display='none'">
|
|
|
+ <div style="display:flex; flex-direction:column;">
|
|
|
+ <span style="font-weight: 500;">投放诊断</span>
|
|
|
+ </div>
|
|
|
+ </li>
|
|
|
+
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button id="reset-btn" class="btn glass-btn">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path
|
|
|
+ d="M4 4V9H4.58152M19.9381 11C19.446 7.05369 16.0796 4 12 4C8.64262 4 5.76829 6.06817 4.58152 9M4.58152 9H9M20 20V15H19.4185M19.4185 15C18.2317 17.9318 15.3574 20 12 20C7.92038 20 4.55399 16.9463 4.06189 13M19.4185 15H15"
|
|
|
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ 重置对话
|
|
|
+ </button>
|
|
|
+ <!-- 用户信息 -->
|
|
|
+ <div class="user-info" id="user-info" style="display: none;">
|
|
|
+ <div class="user-details">
|
|
|
+ <div class="user-avatar" id="user-avatar">
|
|
|
+ <img id="avatar-img" src="" alt="用户头像" onerror="this.style.display='none'">
|
|
|
+ <div id="avatar-fallback" class="avatar-fallback"></div>
|
|
|
+ </div>
|
|
|
+ <div class="user-text">
|
|
|
+ <div class="user-info-row">
|
|
|
+ <span class="user-name" id="user-name"></span>
|
|
|
+ <span class="user-type" id="user-type"></span>
|
|
|
+ </div>
|
|
|
+ <div class="user-info-row">
|
|
|
+ <span class="user-title" id="user-title" style="display: none;"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="user-dropdown" id="user-dropdown" style="display: none;">
|
|
|
+ <div class="user-dropdown-content">
|
|
|
+ <button id="logout-btn" class="btn glass-btn logout-btn">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
|
+ xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 3l4 4-4 4M21 7H9"
|
|
|
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ 退出登录
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+
|
|
|
+ <!-- 移动端菜单切换按钮 -->
|
|
|
+ <div class="chat-main-content">
|
|
|
+ <div id="chat-messages" class="chat-messages glass-panel">
|
|
|
+ <div class="message bot">
|
|
|
+ <div class="message-content">请发送你的投放数据。 如果是视频诊断,目前仅能诊断文案,请去聚德神转转为文案(最好是带时间戳的字幕-聚德智能视频处理工具可导出)。
|
|
|
+ <ul>
|
|
|
+ <!-- <li>请输入想要分析的疾病或者产品名称</li> -->
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chat-input-container glass-panel">
|
|
|
+ <textarea id="user-input" placeholder="请输入需要撰写的品类名和要求..." rows="1"></textarea>
|
|
|
+ <button id="send-btn" class="glass-btn-primary" aria-label="发送消息">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2"
|
|
|
+ stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 核心JS库 -->
|
|
|
+ <script src="{{ url_for('static', filename='js/highlight.min.js') }}"></script>
|
|
|
+ <script src="{{ url_for('static', filename='js/marked.min.js') }}"></script>
|
|
|
+ <script src="{{ url_for('static', filename='js/showdown.min.js') }}"></script>
|
|
|
+ <script src="{{ url_for('static', filename='js/copy-buttons.js') }}"></script>
|
|
|
+ <script src="{{ url_for('static', filename='js/chat.js') }}"></script>
|
|
|
+
|
|
|
+ <!-- 认证和用户信息管理 -->
|
|
|
+ <!-- 自定义弹框 -->
|
|
|
+ <!-- 删除确认弹框 -->
|
|
|
+ <div id="delete-modal" class="custom-modal">
|
|
|
+ <div class="modal-overlay"></div>
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h3 class="modal-title">删除对话</h3>
|
|
|
+ <button class="modal-close" onclick="closeModal('delete-modal')">
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="modal-body">
|
|
|
+ <div class="modal-icon delete-icon">
|
|
|
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"
|
|
|
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ <path d="M10 11v6M14 11v6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <p class="modal-message">确定要删除这个对话吗?删除后将无法恢复。</p>
|
|
|
+ </div>
|
|
|
+ <div class="modal-footer">
|
|
|
+ <button class="btn btn-secondary" onclick="closeModal('delete-modal')">取消</button>
|
|
|
+ <button class="btn btn-danger" id="confirm-delete-btn">删除</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 重命名弹框 -->
|
|
|
+ <div id="rename-modal" class="custom-modal">
|
|
|
+ <div class="modal-overlay"></div>
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h3 class="modal-title">重命名对话</h3>
|
|
|
+ <button class="modal-close" onclick="closeModal('rename-modal')">
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="modal-body">
|
|
|
+ <div class="form-group">
|
|
|
+ <input type="text" id="rename-input" class="form-input" placeholder="请输入新的对话标题" maxlength="50">
|
|
|
+ <div class="form-hint">最多50个字符</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="modal-footer">
|
|
|
+ <button class="btn btn-secondary" onclick="closeModal('rename-modal')">取消</button>
|
|
|
+ <button class="btn btn-primary" id="confirm-rename-btn">保存</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 消息提示弹框 -->
|
|
|
+ <div id="message-modal" class="custom-modal">
|
|
|
+ <div class="modal-overlay"></div>
|
|
|
+ <div class="modal-content message-modal-content">
|
|
|
+ <div class="modal-body">
|
|
|
+ <div class="modal-icon message-icon">
|
|
|
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M9 12l2 2 4-4M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2"
|
|
|
+ stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <p class="modal-message" id="message-text">操作成功</p>
|
|
|
+ </div>
|
|
|
+ <div class="modal-footer">
|
|
|
+ <button class="btn btn-primary" onclick="closeModal('message-modal')">确定</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+
|
|
|
+ <script>
|
|
|
+ // 弹框控制函数
|
|
|
+ function showModal(modalId) {
|
|
|
+ const modal = document.getElementById(modalId);
|
|
|
+ if (modal) {
|
|
|
+ modal.classList.add('show');
|
|
|
+ document.body.style.overflow = 'hidden'; // 防止背景滚动
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function closeModal(modalId) {
|
|
|
+ const modal = document.getElementById(modalId);
|
|
|
+ if (modal) {
|
|
|
+ modal.classList.remove('show');
|
|
|
+ document.body.style.overflow = ''; // 恢复滚动
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function showMessage(message, type = 'success') {
|
|
|
+ const messageText = document.getElementById('message-text');
|
|
|
+ const messageIcon = document.querySelector('#message-modal .modal-icon');
|
|
|
+
|
|
|
+ if (messageText) {
|
|
|
+ messageText.textContent = message;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (messageIcon) {
|
|
|
+ // 根据类型设置图标颜色
|
|
|
+ messageIcon.className = 'modal-icon message-icon';
|
|
|
+ if (type === 'error') {
|
|
|
+ messageIcon.style.color = '#ef4444';
|
|
|
+ } else if (type === 'warning') {
|
|
|
+ messageIcon.style.color = '#f59e0b';
|
|
|
+ } else {
|
|
|
+ messageIcon.style.color = '#10b981';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showModal('message-modal');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 点击遮罩层关闭弹框
|
|
|
+ document.addEventListener('click', function (e) {
|
|
|
+ if (e.target.classList.contains('modal-overlay')) {
|
|
|
+ const modal = e.target.closest('.custom-modal');
|
|
|
+ if (modal) {
|
|
|
+ closeModal(modal.id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // ESC键关闭弹框
|
|
|
+ document.addEventListener('keydown', function (e) {
|
|
|
+ if (e.key === 'Escape') {
|
|
|
+ const openModal = document.querySelector('.custom-modal.show');
|
|
|
+ if (openModal) {
|
|
|
+ closeModal(openModal.id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 防重复重定向标志
|
|
|
+ let isRedirecting = false;
|
|
|
+
|
|
|
+ // 页面加载时检查认证状态
|
|
|
+ document.addEventListener('DOMContentLoaded', function () {
|
|
|
+ checkAuthStatus();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 思考过程折叠/展开函数
|
|
|
+ function toggleThought(button) {
|
|
|
+ const container = button.closest('.thought-container');
|
|
|
+ const content = container.querySelector('.thought-content');
|
|
|
+ const icon = button.querySelector('svg path');
|
|
|
+
|
|
|
+ if (container.dataset.collapsed === 'true') {
|
|
|
+ // 展开
|
|
|
+ container.dataset.collapsed = 'false';
|
|
|
+ content.style.maxHeight = '500px';
|
|
|
+ content.style.padding = '10px 0';
|
|
|
+ icon.setAttribute('d', 'M6 9l6 6 6-6');
|
|
|
+ } else {
|
|
|
+ // 折叠
|
|
|
+ container.dataset.collapsed = 'true';
|
|
|
+ content.style.maxHeight = '0';
|
|
|
+ content.style.padding = '0';
|
|
|
+ icon.setAttribute('d', 'M18 15l-6-6-6 6');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function checkAuthStatus() {
|
|
|
+ // 检查URL参数中是否有登录成功信息
|
|
|
+ const urlParams = new URLSearchParams(window.location.search);
|
|
|
+ const loginSuccess = urlParams.get('login_success');
|
|
|
+ const token = urlParams.get('token');
|
|
|
+ const userInfoJson = urlParams.get('user_info');
|
|
|
+
|
|
|
+ if (loginSuccess && token && userInfoJson) {
|
|
|
+ // 登录成功,处理用户信息
|
|
|
+ try {
|
|
|
+ const userInfo = JSON.parse(decodeURIComponent(userInfoJson));
|
|
|
+
|
|
|
+ // 保存到localStorage
|
|
|
+ localStorage.setItem('auth_token', token);
|
|
|
+ localStorage.setItem('user_info', JSON.stringify(userInfo));
|
|
|
+
|
|
|
+ // 显示用户信息
|
|
|
+ displayUserInfo(userInfo);
|
|
|
+ showUserSection();
|
|
|
+ addAuthHeader(token);
|
|
|
+
|
|
|
+ // 清理URL参数
|
|
|
+ window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
+
|
|
|
+ // 初始化历史记录侧边栏
|
|
|
+ initializeHistorySidebar();
|
|
|
+
|
|
|
+ // 启动对话状态更新器
|
|
|
+ if (typeof startConversationStatusUpdater === 'function') {
|
|
|
+ startConversationStatusUpdater();
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (e) {
|
|
|
+ redirectToLogin();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 正常检查localStorage中的认证信息
|
|
|
+ const storedToken = localStorage.getItem('auth_token');
|
|
|
+ const storedUserInfo = localStorage.getItem('user_info');
|
|
|
+
|
|
|
+ if (storedToken && storedUserInfo) {
|
|
|
+ try {
|
|
|
+ const user = JSON.parse(storedUserInfo);
|
|
|
+
|
|
|
+ // 验证用户信息的基本字段
|
|
|
+ if (!user.name && !user.nickname) {
|
|
|
+ user.name = '钉钉用户';
|
|
|
+ user.nickname = '钉钉用户';
|
|
|
+ }
|
|
|
+
|
|
|
+ displayUserInfo(user);
|
|
|
+ showUserSection();
|
|
|
+ addAuthHeader(storedToken);
|
|
|
+
|
|
|
+ // 初始化历史记录侧边栏
|
|
|
+ initializeHistorySidebar();
|
|
|
+
|
|
|
+ // 启动对话状态更新器
|
|
|
+ if (typeof startConversationStatusUpdater === 'function') {
|
|
|
+ startConversationStatusUpdater();
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (e) {
|
|
|
+ redirectToLogin();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ redirectToLogin();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function displayUserInfo(user) {
|
|
|
+ // 显示用户头像
|
|
|
+ const avatarImg = document.getElementById('avatar-img');
|
|
|
+ const avatarFallback = document.getElementById('avatar-fallback');
|
|
|
+
|
|
|
+ if (user.avatar && user.avatar.trim() !== '') {
|
|
|
+ avatarImg.src = user.avatar;
|
|
|
+ avatarImg.style.display = 'block';
|
|
|
+ avatarFallback.style.display = 'none';
|
|
|
+ } else {
|
|
|
+ avatarImg.style.display = 'none';
|
|
|
+ avatarFallback.style.display = 'block';
|
|
|
+ // 获取用户名的首字母作为头像
|
|
|
+ const displayName = user.nickname || user.name || 'U';
|
|
|
+ avatarFallback.textContent = displayName.charAt(0).toUpperCase();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示用户信息
|
|
|
+ const userName = document.getElementById('user-name');
|
|
|
+ if (userName) {
|
|
|
+ const displayName = user.nickname || user.name || '钉钉用户';
|
|
|
+ userName.textContent = displayName;
|
|
|
+ }
|
|
|
+
|
|
|
+ const userType = document.getElementById('user-type');
|
|
|
+ if (userType) {
|
|
|
+ // 根据登录类型显示不同的标签样式
|
|
|
+ if (user.login_type === 'dingtalk') {
|
|
|
+ userType.textContent = '钉钉';
|
|
|
+ userType.style.background = 'rgba(24, 144, 255, 0.2)';
|
|
|
+ userType.style.color = '#1890ff';
|
|
|
+ userType.style.border = '1px solid rgba(24, 144, 255, 0.3)';
|
|
|
+ } else {
|
|
|
+ userType.textContent = user.login_type || '钉钉';
|
|
|
+ userType.style.background = 'rgba(255, 255, 255, 0.3)';
|
|
|
+ userType.style.color = '#666';
|
|
|
+ userType.style.border = '1px solid rgba(255, 255, 255, 0.2)';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const userTitle = document.getElementById('user-title');
|
|
|
+ if (userTitle && user.title) {
|
|
|
+ userTitle.textContent = user.title;
|
|
|
+ userTitle.style.display = 'block';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示其他用户信息
|
|
|
+ if (user.email) {
|
|
|
+ const emailItem = document.getElementById('user-email');
|
|
|
+ if (emailItem) {
|
|
|
+ emailItem.querySelector('.info-value').textContent = user.email;
|
|
|
+ emailItem.style.display = 'block';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (user.mobile) {
|
|
|
+ const mobileItem = document.getElementById('user-mobile');
|
|
|
+ if (mobileItem) {
|
|
|
+ mobileItem.querySelector('.info-value').textContent = user.mobile;
|
|
|
+ mobileItem.style.display = 'block';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (user.job_number) {
|
|
|
+ const jobItem = document.getElementById('user-job');
|
|
|
+ if (jobItem) {
|
|
|
+ jobItem.querySelector('.info-value').textContent = user.job_number;
|
|
|
+ jobItem.style.display = 'block';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (user.dept_name) {
|
|
|
+ const deptItem = document.getElementById('user-dept');
|
|
|
+ if (deptItem) {
|
|
|
+ deptItem.querySelector('.info-value').textContent = user.dept_name;
|
|
|
+ deptItem.style.display = 'block';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function showUserSection() {
|
|
|
+ const userInfo = document.getElementById('user-info');
|
|
|
+ if (userInfo) userInfo.style.display = 'flex';
|
|
|
+ }
|
|
|
+
|
|
|
+ function addAuthHeader(token) {
|
|
|
+ // 注意:addAuthHeader函数由chat.js提供,这里不需要重复定义
|
|
|
+ // 如果需要设置认证头,直接调用chat.js中的函数
|
|
|
+ if (typeof window.addAuthHeader === 'function') {
|
|
|
+ window.addAuthHeader(token);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function redirectToLogin() {
|
|
|
+ if (isRedirecting) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ isRedirecting = true;
|
|
|
+ localStorage.removeItem('auth_token');
|
|
|
+ localStorage.removeItem('user_info');
|
|
|
+ // 添加延迟,避免频繁重定向
|
|
|
+ setTimeout(() => {
|
|
|
+ window.location.href = '/login';
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 退出登录
|
|
|
+ document.getElementById('logout-btn').addEventListener('click', function () {
|
|
|
+ fetch('/api/auth/logout', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ }).then(() => {
|
|
|
+ redirectToLogin();
|
|
|
+ }).catch(() => {
|
|
|
+ redirectToLogin();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 用户信息下拉菜单(修复:添加鼠标进入/离开事件,防止移向退出登录按钮时菜单消失)
|
|
|
+ function initializeUserDropdown() {
|
|
|
+ const userInfo = document.getElementById('user-info');
|
|
|
+ const dropdown = document.getElementById('user-dropdown');
|
|
|
+
|
|
|
+ if (!userInfo || !dropdown) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化时确保下拉菜单是隐藏状态(覆盖CSS的:hover效果)
|
|
|
+ dropdown.style.display = 'none';
|
|
|
+ dropdown.style.opacity = '0';
|
|
|
+ dropdown.style.visibility = 'hidden';
|
|
|
+ dropdown.style.transform = 'translateY(-15px) scale(0.95)';
|
|
|
+
|
|
|
+ let isHovering = false;
|
|
|
+ let hideTimer = null;
|
|
|
+
|
|
|
+ function showDropdown() {
|
|
|
+ if (hideTimer) {
|
|
|
+ clearTimeout(hideTimer);
|
|
|
+ hideTimer = null;
|
|
|
+ }
|
|
|
+ dropdown.style.display = 'block';
|
|
|
+ dropdown.style.opacity = '1';
|
|
|
+ dropdown.style.visibility = 'visible';
|
|
|
+ dropdown.style.transform = 'translateY(0) scale(1)';
|
|
|
+ }
|
|
|
+
|
|
|
+ function scheduleHide() {
|
|
|
+ if (hideTimer) {
|
|
|
+ clearTimeout(hideTimer);
|
|
|
+ }
|
|
|
+ hideTimer = setTimeout(() => {
|
|
|
+ if (!isHovering) {
|
|
|
+ dropdown.style.display = 'none';
|
|
|
+ dropdown.style.opacity = '0';
|
|
|
+ dropdown.style.visibility = 'hidden';
|
|
|
+ dropdown.style.transform = 'translateY(-15px) scale(0.95)';
|
|
|
+ }
|
|
|
+ }, 150); // 150ms延迟,给用户足够时间移动到按钮
|
|
|
+ }
|
|
|
+
|
|
|
+ // 鼠标进入用户信息区域
|
|
|
+ userInfo.addEventListener('mouseenter', function () {
|
|
|
+ isHovering = true;
|
|
|
+ showDropdown();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 鼠标离开用户信息区域
|
|
|
+ userInfo.addEventListener('mouseleave', function () {
|
|
|
+ isHovering = false;
|
|
|
+ scheduleHide();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 鼠标进入下拉菜单区域(重要:防止鼠标移动到按钮时菜单消失)
|
|
|
+ dropdown.addEventListener('mouseenter', function () {
|
|
|
+ isHovering = true;
|
|
|
+ showDropdown();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 鼠标离开下拉菜单区域
|
|
|
+ dropdown.addEventListener('mouseleave', function () {
|
|
|
+ isHovering = false;
|
|
|
+ scheduleHide();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保留点击事件用于移动端
|
|
|
+ document.getElementById('user-avatar').addEventListener('click', function (e) {
|
|
|
+ // 在桌面端,如果已经在悬停显示状态,点击时不处理(避免冲突)
|
|
|
+ if (window.innerWidth > 768 && dropdown.style.display === 'block') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 移动端切换显示/隐藏
|
|
|
+ if (dropdown.style.display === 'none' || dropdown.style.display === '') {
|
|
|
+ showDropdown();
|
|
|
+ } else {
|
|
|
+ dropdown.style.display = 'none';
|
|
|
+ dropdown.style.opacity = '0';
|
|
|
+ dropdown.style.visibility = 'hidden';
|
|
|
+ dropdown.style.transform = 'translateY(-15px) scale(0.95)';
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化用户下拉菜单
|
|
|
+ document.addEventListener('DOMContentLoaded', function () {
|
|
|
+ setTimeout(() => {
|
|
|
+ initializeUserDropdown();
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 智能体选择(悬停展开)
|
|
|
+ document.addEventListener('DOMContentLoaded', function () {
|
|
|
+ setTimeout(() => {
|
|
|
+ initializeAgentHoverDropdown();
|
|
|
+ }, 100);
|
|
|
+
|
|
|
+ // 监听窗口大小变化,重新初始化智能体菜单
|
|
|
+ let resizeTimer;
|
|
|
+ window.addEventListener('resize', function () {
|
|
|
+ clearTimeout(resizeTimer);
|
|
|
+ resizeTimer = setTimeout(() => {
|
|
|
+ initializeAgentHoverDropdown();
|
|
|
+ }, 250);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ function initializeAgentHoverDropdown() {
|
|
|
+ const dropdown = document.getElementById('agent-dropdown');
|
|
|
+ const menu = document.getElementById('agent-menu');
|
|
|
+ const triggerText = document.getElementById('agent-trigger-text');
|
|
|
+ const triggerIcon = document.getElementById('agent-trigger-icon');
|
|
|
+ if (!dropdown || !menu) return;
|
|
|
+
|
|
|
+ const agentMeta = {
|
|
|
+ 'planning': {
|
|
|
+ name: '智能策划大师',
|
|
|
+ icon: "{{ url_for('static', filename='img/planning.png') }}"
|
|
|
+ },
|
|
|
+ 'videoaccount': {
|
|
|
+ name: '病理分析',
|
|
|
+ icon: "{{ url_for('static', filename='img/videoaccount.svg') }}"
|
|
|
+ },
|
|
|
+ 'diagnosis': {
|
|
|
+ name: '投放诊断',
|
|
|
+ icon: "{{ url_for('static', filename='img/diagnosis.svg') }}"
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // 兼容历史存储:如果之前存的是已删除的智能体,强制切回智能策划大师
|
|
|
+ const storedAgent = localStorage.getItem('current_agent');
|
|
|
+ let currentAgent = 'planning';
|
|
|
+ if (!storedAgent || !agentMeta[storedAgent]) {
|
|
|
+ // 没有存过,或存的是已不存在的智能体 -> 统一重置为智能策划大师
|
|
|
+ localStorage.setItem('current_agent', 'planning');
|
|
|
+ } else {
|
|
|
+ currentAgent = storedAgent;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化触发器显示
|
|
|
+ triggerText.textContent = agentMeta[currentAgent].name;
|
|
|
+ if (triggerIcon) {
|
|
|
+ triggerIcon.src = agentMeta[currentAgent].icon;
|
|
|
+ triggerIcon.style.display = 'block';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 高亮选中项(仅影响下拉菜单项,不改全局样式)
|
|
|
+ function clearActiveStyles() {
|
|
|
+ menu.querySelectorAll('.agent-option').forEach(item => {
|
|
|
+ item.removeAttribute('data-active');
|
|
|
+ item.style.background = '';
|
|
|
+ item.style.border = '';
|
|
|
+ item.style.opacity = '';
|
|
|
+ item.style.pointerEvents = '';
|
|
|
+ });
|
|
|
+ }
|
|
|
+ function setActiveStyles(agentType) {
|
|
|
+ clearActiveStyles();
|
|
|
+ const activeEl = menu.querySelector(`.agent-option[data-agent="${agentType}"]`);
|
|
|
+ if (activeEl) {
|
|
|
+ activeEl.setAttribute('data-active', 'true');
|
|
|
+ activeEl.style.background = 'rgba(99, 102, 241, 0.12)';
|
|
|
+ activeEl.style.border = '1px solid rgba(99, 102, 241, 0.35)';
|
|
|
+ activeEl.style.opacity = '1';
|
|
|
+ // 禁止重复点击当前激活项
|
|
|
+ activeEl.style.pointerEvents = 'none';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setActiveStyles(currentAgent);
|
|
|
+
|
|
|
+ // 检测是否为移动设备
|
|
|
+ const isMobile = window.innerWidth <= 768;
|
|
|
+
|
|
|
+ if (isMobile) {
|
|
|
+ // 移动端:点击显示/隐藏菜单
|
|
|
+ let isMenuOpen = false;
|
|
|
+
|
|
|
+ function showMenu() {
|
|
|
+ menu.style.display = 'block';
|
|
|
+ isMenuOpen = true;
|
|
|
+ // 添加动画类
|
|
|
+ setTimeout(() => {
|
|
|
+ menu.classList.add('show');
|
|
|
+ }, 10);
|
|
|
+ }
|
|
|
+
|
|
|
+ function hideMenu() {
|
|
|
+ menu.classList.remove('show');
|
|
|
+ setTimeout(() => {
|
|
|
+ menu.style.display = 'none';
|
|
|
+ }, 300);
|
|
|
+ isMenuOpen = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 点击触发器显示菜单
|
|
|
+ dropdown.addEventListener('click', function (e) {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ if (isMenuOpen) {
|
|
|
+ hideMenu();
|
|
|
+ } else {
|
|
|
+ showMenu();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 移除遮罩层点击事件
|
|
|
+
|
|
|
+ // 点击菜单外部隐藏菜单
|
|
|
+ document.addEventListener('click', function (e) {
|
|
|
+ if (!dropdown.contains(e.target) && !menu.contains(e.target)) {
|
|
|
+ hideMenu();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ } else {
|
|
|
+ // 桌面端:悬停显示/隐藏(避免从触发区移动到菜单区时闪烁隐藏)
|
|
|
+ let isHovering = false;
|
|
|
+ let hideTimer = null;
|
|
|
+
|
|
|
+ function showMenu() {
|
|
|
+ if (hideTimer) {
|
|
|
+ clearTimeout(hideTimer);
|
|
|
+ hideTimer = null;
|
|
|
+ }
|
|
|
+ menu.style.display = 'block';
|
|
|
+ }
|
|
|
+
|
|
|
+ function scheduleHide() {
|
|
|
+ if (hideTimer) {
|
|
|
+ clearTimeout(hideTimer);
|
|
|
+ }
|
|
|
+ hideTimer = setTimeout(() => {
|
|
|
+ if (!isHovering) {
|
|
|
+ menu.style.display = 'none';
|
|
|
+ }
|
|
|
+ }, 120);
|
|
|
+ }
|
|
|
+
|
|
|
+ dropdown.addEventListener('mouseenter', function () {
|
|
|
+ isHovering = true;
|
|
|
+ showMenu();
|
|
|
+ });
|
|
|
+ dropdown.addEventListener('mouseleave', function () {
|
|
|
+ isHovering = false;
|
|
|
+ scheduleHide();
|
|
|
+ });
|
|
|
+
|
|
|
+ menu.addEventListener('mouseenter', function () {
|
|
|
+ isHovering = true;
|
|
|
+ showMenu();
|
|
|
+ });
|
|
|
+ menu.addEventListener('mouseleave', function () {
|
|
|
+ isHovering = false;
|
|
|
+ scheduleHide();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ menu.querySelectorAll('.agent-option').forEach((item, index) => {
|
|
|
+
|
|
|
+
|
|
|
+ item.addEventListener('click', function (e) {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+
|
|
|
+ const agentType = this.getAttribute('data-agent');
|
|
|
+ const current = localStorage.getItem('current_agent') || 'planning';
|
|
|
+
|
|
|
+ // 已是当前激活则不处理
|
|
|
+ if (agentType === current) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已有对话(不是新对话)且有实际消息内容
|
|
|
+ const conversationId = window.currentConversationId;
|
|
|
+ const hasExistingConversation = conversationId &&
|
|
|
+ conversationId !== 'new' &&
|
|
|
+ conversationId !== null &&
|
|
|
+ conversationId !== undefined;
|
|
|
+
|
|
|
+ // 检查当前对话是否有实际的消息内容(不是空的新对话)
|
|
|
+ const hasActualMessages = checkConversationHasMessages(conversationId);
|
|
|
+
|
|
|
+ if (hasExistingConversation && hasActualMessages) {
|
|
|
+ // 先关闭菜单,避免遮挡对话框
|
|
|
+ if (isMobile) {
|
|
|
+ hideMenu();
|
|
|
+ } else {
|
|
|
+ isHovering = false;
|
|
|
+ scheduleHide();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 延迟一下再弹出对话框,确保菜单已关闭
|
|
|
+ setTimeout(() => {
|
|
|
+ // 弹出确认对话框
|
|
|
+ showAgentSwitchConfirmDialog(agentType, current, function (confirmed) {
|
|
|
+ if (confirmed) {
|
|
|
+ // 用户确认,创建新对话并切换智能体
|
|
|
+ switchAgentWithNewConversation(agentType);
|
|
|
+ setActiveStyles(agentType);
|
|
|
+ // 更新触发器
|
|
|
+ triggerText.textContent = agentMeta[agentType].name;
|
|
|
+ if (triggerIcon) {
|
|
|
+ triggerIcon.src = agentMeta[agentType].icon;
|
|
|
+ triggerIcon.style.display = 'block';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 用户取消,不切换智能体,保持当前智能体
|
|
|
+ // 不需要做任何操作
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }, 100);
|
|
|
+ } else {
|
|
|
+ // 新对话或空对话(没有实际消息),直接切换智能体,无需确认
|
|
|
+ switchAgent(agentType);
|
|
|
+ setActiveStyles(agentType);
|
|
|
+ // 更新触发器
|
|
|
+ triggerText.textContent = agentMeta[agentType].name;
|
|
|
+ if (triggerIcon) {
|
|
|
+ triggerIcon.src = agentMeta[agentType].icon;
|
|
|
+ triggerIcon.style.display = 'block';
|
|
|
+ }
|
|
|
+ // 移动端点击后隐藏菜单
|
|
|
+ if (isMobile) {
|
|
|
+ hideMenu();
|
|
|
+ } else {
|
|
|
+ isHovering = false;
|
|
|
+ scheduleHide();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加触摸事件支持(移动端)
|
|
|
+ item.addEventListener('touchend', function (e) {
|
|
|
+ e.preventDefault();
|
|
|
+ this.click(); // 触发点击事件
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查对话是否有实际的消息内容
|
|
|
+ function checkConversationHasMessages(conversationId) {
|
|
|
+ if (!conversationId || conversationId === 'new') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方法1: 检查聊天消息区域中是否有用户消息
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages) {
|
|
|
+ const userMessages = chatMessages.querySelectorAll('.message.user');
|
|
|
+ // 如果有用户消息,说明对话有实际内容
|
|
|
+ if (userMessages.length > 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方法2: 检查侧边栏中当前对话的消息数量
|
|
|
+ const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
|
|
|
+ if (conversationItem) {
|
|
|
+ const messageCountElement = conversationItem.querySelector('.message-count');
|
|
|
+ if (messageCountElement) {
|
|
|
+ const messageCount = parseInt(messageCountElement.textContent) || 0;
|
|
|
+ // 如果消息数量大于0,说明对话有实际内容
|
|
|
+ if (messageCount > 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方法3: 检查是否有正在进行的流式响应(说明用户正在发送消息)
|
|
|
+ if (window.conversationStates && window.conversationStates.has(conversationId)) {
|
|
|
+ const state = window.conversationStates.get(conversationId);
|
|
|
+ if (state && (state.isStreaming || (state.userMessages && state.userMessages.length > 0))) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果以上都没有,说明是空的新对话
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function switchAgent(agentType) {
|
|
|
+ const current = localStorage.getItem('current_agent') || 'planning';
|
|
|
+ if (current === agentType) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 保存到本地存储
|
|
|
+ localStorage.setItem('current_agent', agentType);
|
|
|
+
|
|
|
+ // 更新状态提示(如有)
|
|
|
+ updateStatusIndicator(agentType);
|
|
|
+
|
|
|
+ // 显示切换提示
|
|
|
+ showAgentSwitchNotification(agentType);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 切换智能体并创建新对话
|
|
|
+ function switchAgentWithNewConversation(agentType) {
|
|
|
+ const current = localStorage.getItem('current_agent') || 'planning';
|
|
|
+ if (current === agentType) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存到本地存储
|
|
|
+ localStorage.setItem('current_agent', agentType);
|
|
|
+
|
|
|
+ // 更新状态提示(如有)
|
|
|
+ updateStatusIndicator(agentType);
|
|
|
+
|
|
|
+ // 创建新对话
|
|
|
+ if (typeof createNewConversation === 'function') {
|
|
|
+ createNewConversation();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示切换提示
|
|
|
+ showAgentSwitchNotification(agentType);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示智能体切换确认对话框
|
|
|
+ function showAgentSwitchConfirmDialog(newAgentType, currentAgentType, callback) {
|
|
|
+ // 防止重复弹出对话框
|
|
|
+ if (document.querySelector('.agent-switch-dialog-overlay')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const agentNames = {
|
|
|
+ 'planning': '智能策划大师',
|
|
|
+ 'videoaccount': '病理分析',
|
|
|
+ 'diagnosis': '投放诊断',
|
|
|
+ 'kuaishou': '快手',
|
|
|
+ 'toutiao': '头条'
|
|
|
+ };
|
|
|
+
|
|
|
+ const newAgentName = agentNames[newAgentType] || '新智能体';
|
|
|
+ const currentAgentName = agentNames[currentAgentType] || '当前智能体';
|
|
|
+
|
|
|
+ // 创建对话框遮罩层
|
|
|
+ const overlay = document.createElement('div');
|
|
|
+ overlay.className = 'agent-switch-dialog-overlay';
|
|
|
+ overlay.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 10000;
|
|
|
+ animation: fadeIn 0.2s ease;
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 创建对话框
|
|
|
+ const dialog = document.createElement('div');
|
|
|
+ dialog.className = 'agent-switch-dialog';
|
|
|
+ dialog.style.cssText = `
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 24px;
|
|
|
+ max-width: 400px;
|
|
|
+ width: 90%;
|
|
|
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
|
|
+ animation: slideUp 0.3s ease;
|
|
|
+ `;
|
|
|
+
|
|
|
+ dialog.innerHTML = `
|
|
|
+ <div style="margin-bottom: 20px;">
|
|
|
+ <div style="font-size: 18px; font-weight: 600; color: #333; margin-bottom: 12px;">
|
|
|
+ 切换智能体
|
|
|
+ </div>
|
|
|
+ <div style="font-size: 14px; color: #666; line-height: 1.6;">
|
|
|
+ 切换将新建对话,是否继续?
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; gap: 12px; justify-content: flex-end;">
|
|
|
+ <button class="dialog-btn-cancel" style="
|
|
|
+ padding: 10px 20px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: white;
|
|
|
+ color: #666;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ ">否</button>
|
|
|
+ <button class="dialog-btn-confirm" style="
|
|
|
+ padding: 10px 20px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: #007bff;
|
|
|
+ color: white;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ ">继续</button>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 添加动画样式
|
|
|
+ if (!document.getElementById('agent-switch-dialog-styles')) {
|
|
|
+ const style = document.createElement('style');
|
|
|
+ style.id = 'agent-switch-dialog-styles';
|
|
|
+ style.textContent = `
|
|
|
+ @keyframes fadeIn {
|
|
|
+ from { opacity: 0; }
|
|
|
+ to { opacity: 1; }
|
|
|
+ }
|
|
|
+ @keyframes fadeOut {
|
|
|
+ from { opacity: 1; }
|
|
|
+ to { opacity: 0; }
|
|
|
+ }
|
|
|
+ @keyframes slideUp {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(20px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ @keyframes slideDown {
|
|
|
+ from {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(20px);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .agent-switch-dialog-overlay {
|
|
|
+ backdrop-filter: blur(2px);
|
|
|
+ }
|
|
|
+ .dialog-btn-cancel:hover {
|
|
|
+ background: #f5f5f5 !important;
|
|
|
+ border-color: #bbb !important;
|
|
|
+ }
|
|
|
+ .dialog-btn-confirm:hover {
|
|
|
+ background: #0056b3 !important;
|
|
|
+ }
|
|
|
+ .dialog-btn-cancel:active,
|
|
|
+ .dialog-btn-confirm:active {
|
|
|
+ transform: scale(0.98);
|
|
|
+ }
|
|
|
+ `;
|
|
|
+ document.head.appendChild(style);
|
|
|
+ }
|
|
|
+
|
|
|
+ overlay.appendChild(dialog);
|
|
|
+ document.body.appendChild(overlay);
|
|
|
+
|
|
|
+ // 绑定按钮事件
|
|
|
+ const cancelBtn = dialog.querySelector('.dialog-btn-cancel');
|
|
|
+ const confirmBtn = dialog.querySelector('.dialog-btn-confirm');
|
|
|
+
|
|
|
+ const closeDialog = (result) => {
|
|
|
+ overlay.style.animation = 'fadeOut 0.2s ease';
|
|
|
+ dialog.style.animation = 'slideDown 0.2s ease';
|
|
|
+ setTimeout(() => {
|
|
|
+ overlay.remove();
|
|
|
+ callback(result);
|
|
|
+ }, 200);
|
|
|
+ };
|
|
|
+
|
|
|
+ cancelBtn.addEventListener('click', () => closeDialog(false));
|
|
|
+ confirmBtn.addEventListener('click', () => closeDialog(true));
|
|
|
+
|
|
|
+ // 点击遮罩层关闭(取消)
|
|
|
+ overlay.addEventListener('click', (e) => {
|
|
|
+ if (e.target === overlay) {
|
|
|
+ closeDialog(false);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // ESC键关闭(取消)
|
|
|
+ const handleEsc = (e) => {
|
|
|
+ if (e.key === 'Escape') {
|
|
|
+ closeDialog(false);
|
|
|
+ document.removeEventListener('keydown', handleEsc);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ document.addEventListener('keydown', handleEsc);
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateStatusIndicator(agentType) {
|
|
|
+ const agentNames = {
|
|
|
+ 'planning': '智能策划大师',
|
|
|
+ 'videoaccount': '病理分析',
|
|
|
+ 'diagnosis': '投放诊断',
|
|
|
+ 'kuaishou': '快手',
|
|
|
+ 'toutiao': '头条'
|
|
|
+ };
|
|
|
+
|
|
|
+ const currentPlatformName = document.getElementById('current-platform-name');
|
|
|
+ if (currentPlatformName) {
|
|
|
+ currentPlatformName.textContent = agentNames[agentType] || '智能策划大师';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function showAgentSwitchNotification(agentType) {
|
|
|
+ const agentNames = {
|
|
|
+ 'planning': '智能策划大师',
|
|
|
+ 'videoaccount': '病理分析',
|
|
|
+ 'diagnosis': '投放诊断',
|
|
|
+ 'kuaishou': '快手',
|
|
|
+ 'toutiao': '头条'
|
|
|
+ };
|
|
|
+
|
|
|
+ const agentName = agentNames[agentType] || '智能策划大师';
|
|
|
+
|
|
|
+ // 创建通知元素
|
|
|
+ const notification = document.createElement('div');
|
|
|
+ notification.className = 'agent-switch-notification';
|
|
|
+ notification.setAttribute('data-platform', agentType);
|
|
|
+ notification.innerHTML = `
|
|
|
+ <div class="notification-content">
|
|
|
+ <span class="notification-icon">✨</span>
|
|
|
+ <span class="notification-text">已切换到 ${agentName}</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 添加到页面
|
|
|
+ document.body.appendChild(notification);
|
|
|
+
|
|
|
+ // 显示动画
|
|
|
+ setTimeout(() => {
|
|
|
+ notification.classList.add('show');
|
|
|
+ }, 100);
|
|
|
+
|
|
|
+ // 自动隐藏
|
|
|
+ setTimeout(() => {
|
|
|
+ notification.classList.remove('show');
|
|
|
+ setTimeout(() => {
|
|
|
+ if (notification.parentNode) {
|
|
|
+ notification.parentNode.removeChild(notification);
|
|
|
+ }
|
|
|
+ }, 300);
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取当前平台类型(供其他功能使用)
|
|
|
+ function getCurrentAgent() {
|
|
|
+ return localStorage.getItem('current_agent') || 'planning';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 动态调整聊天消息区域高度
|
|
|
+ function adjustChatMessagesHeight() {
|
|
|
+ const userInput = document.getElementById('user-input');
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+
|
|
|
+ if (userInput && chatMessages) {
|
|
|
+ // 获取输入框的实际高度
|
|
|
+ const inputHeight = userInput.scrollHeight;
|
|
|
+ const minHeight = 44; // 最小高度
|
|
|
+ const maxHeight = 200; // 最大高度
|
|
|
+ const actualHeight = Math.max(minHeight, Math.min(inputHeight, maxHeight));
|
|
|
+
|
|
|
+ // 计算聊天消息区域应该的高度
|
|
|
+ const containerHeight = document.querySelector('.chat-container').offsetHeight;
|
|
|
+ const headerHeight = document.querySelector('header').offsetHeight;
|
|
|
+ const inputContainerPadding = 40; // 输入框容器的padding
|
|
|
+ const availableHeight = containerHeight - headerHeight - actualHeight - inputContainerPadding;
|
|
|
+
|
|
|
+ // 设置最小高度为100px,确保有足够的显示空间
|
|
|
+ const finalHeight = Math.max(availableHeight, 100);
|
|
|
+
|
|
|
+ chatMessages.style.height = `${finalHeight}px`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 滚动到聊天消息区域底部
|
|
|
+ function scrollToBottom() {
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages) {
|
|
|
+ chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 平滑滚动到聊天消息区域底部
|
|
|
+ function smoothScrollToBottom() {
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages) {
|
|
|
+ chatMessages.scrollTo({
|
|
|
+ top: chatMessages.scrollHeight,
|
|
|
+ behavior: 'smooth'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听输入框内容变化
|
|
|
+ document.addEventListener('DOMContentLoaded', function () {
|
|
|
+ const userInput = document.getElementById('user-input');
|
|
|
+ if (userInput) {
|
|
|
+ // 监听输入事件
|
|
|
+ userInput.addEventListener('input', adjustChatMessagesHeight);
|
|
|
+
|
|
|
+ // 监听键盘事件(回车等)
|
|
|
+ userInput.addEventListener('keydown', function (e) {
|
|
|
+ if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
+ // 如果是回车键且没有按Shift,延迟调整高度
|
|
|
+ setTimeout(adjustChatMessagesHeight, 100);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', adjustChatMessagesHeight);
|
|
|
+
|
|
|
+ // 初始化时调整一次高度
|
|
|
+ setTimeout(adjustChatMessagesHeight, 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听聊天消息区域的内容变化,自动滚动到底部
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages) {
|
|
|
+ // 创建观察器监听内容变化
|
|
|
+ const observer = new MutationObserver(function (mutations) {
|
|
|
+ // 如果正在编辑消息,不自动滚动
|
|
|
+ if (window.isEditingMessage) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const distanceToBottom = chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight;
|
|
|
+ const threshold = 50;
|
|
|
+ if (distanceToBottom > threshold) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 延迟滚动,确保内容完全渲染
|
|
|
+ setTimeout(() => {
|
|
|
+ scrollToBottom();
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 监听子节点变化和属性变化
|
|
|
+ observer.observe(chatMessages, {
|
|
|
+ childList: true,
|
|
|
+ subtree: true,
|
|
|
+ attributes: false
|
|
|
+ });
|
|
|
+
|
|
|
+ // 初始化时滚动到底部
|
|
|
+ setTimeout(scrollToBottom, 100);
|
|
|
+
|
|
|
+ // 添加双击快速滚动到底部的功能
|
|
|
+ let clickCount = 0;
|
|
|
+ let clickTimer = null;
|
|
|
+
|
|
|
+ chatMessages.addEventListener('click', function () {
|
|
|
+ clickCount++;
|
|
|
+
|
|
|
+ if (clickCount === 1) {
|
|
|
+ clickTimer = setTimeout(() => {
|
|
|
+ clickCount = 0;
|
|
|
+ }, 300);
|
|
|
+ } else if (clickCount === 2) {
|
|
|
+ clearTimeout(clickTimer);
|
|
|
+ clickCount = 0;
|
|
|
+ smoothScrollToBottom();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加键盘快捷键:Ctrl+End 或 Cmd+End 快速滚动到底部
|
|
|
+ document.addEventListener('keydown', function (e) {
|
|
|
+ if ((e.ctrlKey || e.metaKey) && e.key === 'End') {
|
|
|
+ e.preventDefault();
|
|
|
+ smoothScrollToBottom();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 历史记录侧边栏功能 - 移除自动初始化,由认证成功后调用
|
|
|
+
|
|
|
+ function initializeHistorySidebar() {
|
|
|
+ const sidebar = document.getElementById('history-sidebar');
|
|
|
+ const sidebarToggle = document.getElementById('sidebar-toggle');
|
|
|
+ const sidebarOverlay = document.getElementById('sidebar-overlay');
|
|
|
+ const newChatBtn = document.getElementById('new-chat-btn');
|
|
|
+ const conversationItems = document.querySelectorAll('.conversation-item');
|
|
|
+ const conversationMenus = document.querySelectorAll('.conversation-menu');
|
|
|
+
|
|
|
+ // 检测是否为移动设备
|
|
|
+ const isMobile = window.innerWidth <= 768;
|
|
|
+
|
|
|
+ if (isMobile) {
|
|
|
+ // 移动端显示切换按钮
|
|
|
+ if (sidebarToggle) {
|
|
|
+ sidebarToggle.style.display = 'flex';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移动端侧边栏切换功能
|
|
|
+ if (sidebarToggle && sidebar && sidebarOverlay) {
|
|
|
+ sidebarToggle.addEventListener('click', function () {
|
|
|
+ sidebar.classList.add('show');
|
|
|
+ sidebarOverlay.classList.add('show');
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
+ });
|
|
|
+
|
|
|
+ sidebarOverlay.addEventListener('click', function () {
|
|
|
+ sidebar.classList.remove('show');
|
|
|
+ sidebarOverlay.classList.remove('show');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 桌面端隐藏切换按钮
|
|
|
+ if (sidebarToggle) {
|
|
|
+ sidebarToggle.style.display = 'none';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', function () {
|
|
|
+ const isMobileNow = window.innerWidth <= 768;
|
|
|
+
|
|
|
+ if (isMobileNow && !isMobile) {
|
|
|
+ // 切换到移动端
|
|
|
+ if (sidebarToggle) {
|
|
|
+ sidebarToggle.style.display = 'flex';
|
|
|
+ }
|
|
|
+ if (sidebar && sidebarOverlay) {
|
|
|
+ sidebar.classList.remove('show');
|
|
|
+ sidebarOverlay.classList.remove('show');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ }
|
|
|
+ } else if (!isMobileNow && isMobile) {
|
|
|
+ // 切换到桌面端
|
|
|
+ if (sidebarToggle) {
|
|
|
+ sidebarToggle.style.display = 'none';
|
|
|
+ }
|
|
|
+ if (sidebar && sidebarOverlay) {
|
|
|
+ sidebar.classList.remove('show');
|
|
|
+ sidebarOverlay.classList.remove('show');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 新对话按钮功能
|
|
|
+ if (newChatBtn) {
|
|
|
+ newChatBtn.addEventListener('click', function () {
|
|
|
+ // 立即创建新对话
|
|
|
+ createNewConversation();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对话项点击功能
|
|
|
+ conversationItems.forEach(item => {
|
|
|
+ item.addEventListener('click', function (e) {
|
|
|
+ // 如果点击的是菜单按钮,不处理
|
|
|
+ if (e.target.closest('.conversation-menu')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const conversationId = this.getAttribute('data-conversation-id');
|
|
|
+
|
|
|
+ // 设置激活的对话
|
|
|
+ if (typeof setActiveConversation === 'function') {
|
|
|
+ setActiveConversation(conversationId);
|
|
|
+ }
|
|
|
+
|
|
|
+ loadConversation(conversationId);
|
|
|
+ updateConversationSelection(conversationId);
|
|
|
+
|
|
|
+ // 移动端关闭侧边栏
|
|
|
+ if (isMobile && sidebar && sidebarOverlay) {
|
|
|
+ sidebar.classList.remove('show');
|
|
|
+ sidebarOverlay.classList.remove('show');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 对话菜单功能
|
|
|
+ conversationMenus.forEach(menu => {
|
|
|
+ menu.addEventListener('click', function (e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ const conversationId = this.getAttribute('data-conversation-id');
|
|
|
+ showConversationMenu(conversationId, this);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 同步侧边栏用户信息
|
|
|
+ syncSidebarUserInfo();
|
|
|
+
|
|
|
+ // 加载历史记录
|
|
|
+ loadConversationHistory();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找现有的空"新对话"
|
|
|
+ function findExistingEmptyConversation() {
|
|
|
+ const conversationItems = document.querySelectorAll('.conversation-item');
|
|
|
+ for (let item of conversationItems) {
|
|
|
+ const messageCountElement = item.querySelector('.message-count');
|
|
|
+ if (messageCountElement) {
|
|
|
+ const messageCount = parseInt(messageCountElement.textContent);
|
|
|
+ if (messageCount === 0) {
|
|
|
+ return {
|
|
|
+ conversationId: item.getAttribute('data-conversation-id'),
|
|
|
+ sessionId: item.getAttribute('data-session-id'),
|
|
|
+ element: item
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 切换到现有的空"新对话"
|
|
|
+ function switchToExistingEmptyConversation(conversation) {
|
|
|
+ // 更新当前对话ID和session_id
|
|
|
+ window.currentConversationId = conversation.conversationId;
|
|
|
+ window.currentSessionId = conversation.sessionId;
|
|
|
+
|
|
|
+ // 清除聊天区域并显示提示语
|
|
|
+ clearCurrentConversation();
|
|
|
+
|
|
|
+ // 更新侧边栏选中状态
|
|
|
+ updateConversationSelection(conversation.conversationId);
|
|
|
+
|
|
|
+ // 移动端关闭侧边栏
|
|
|
+ const isMobile = window.innerWidth <= 768;
|
|
|
+ if (isMobile) {
|
|
|
+ const sidebar = document.getElementById('history-sidebar');
|
|
|
+ const sidebarOverlay = document.getElementById('sidebar-overlay');
|
|
|
+ if (sidebar && sidebarOverlay) {
|
|
|
+ sidebar.classList.remove('show');
|
|
|
+ sidebarOverlay.classList.remove('show');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保输入框状态正确
|
|
|
+ const userInput = document.getElementById('user-input');
|
|
|
+ const sendButton = document.getElementById('send-btn');
|
|
|
+ if (userInput && sendButton) {
|
|
|
+ userInput.disabled = false;
|
|
|
+ userInput.placeholder = '请输入需要撰写的品类名和要求...';
|
|
|
+ userInput.classList.remove('input-loading');
|
|
|
+ sendButton.disabled = false;
|
|
|
+ sendButton.classList.remove('stopping');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function createNewConversation() {
|
|
|
+ // 检查认证状态
|
|
|
+ const token = localStorage.getItem('auth_token');
|
|
|
+ if (!token) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先检查是否已经存在一个空的"新对话"
|
|
|
+ const existingEmptyConversation = findExistingEmptyConversation();
|
|
|
+ if (existingEmptyConversation) {
|
|
|
+ // 如果存在空的"新对话",直接切换到该对话
|
|
|
+ switchToExistingEmptyConversation(existingEmptyConversation);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示加载状态
|
|
|
+ const newChatBtn = document.getElementById('new-chat-btn');
|
|
|
+ if (newChatBtn) {
|
|
|
+ const originalText = newChatBtn.innerHTML;
|
|
|
+ newChatBtn.innerHTML = `
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="animation: spin 1s linear infinite;">
|
|
|
+ <path d="M21 12a9 9 0 11-6.219-8.56" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ 创建中...
|
|
|
+ `;
|
|
|
+ newChatBtn.disabled = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用后端API创建新对话
|
|
|
+ fetch('/api/conversations/new', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ title: '新对话'
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ if (response.status === 401) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return response.json();
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ if (data && data.success) {
|
|
|
+ // 更新当前对话ID和session_id
|
|
|
+ window.currentConversationId = data.conversation.conversation_id;
|
|
|
+ window.currentSessionId = data.conversation.session_id;
|
|
|
+
|
|
|
+ // 清除聊天区域并显示提示语
|
|
|
+ clearCurrentConversation();
|
|
|
+
|
|
|
+
|
|
|
+ // 更新侧边栏选中状态
|
|
|
+ updateConversationSelection(data.conversation.conversation_id);
|
|
|
+
|
|
|
+ // 重新加载历史记录以显示新对话,但不自动加载对话内容
|
|
|
+ loadConversationHistory(false);
|
|
|
+
|
|
|
+ // 确保输入框状态正确
|
|
|
+ const userInput = document.getElementById('user-input');
|
|
|
+ const sendButton = document.getElementById('send-btn');
|
|
|
+ if (userInput && sendButton) {
|
|
|
+ userInput.disabled = false;
|
|
|
+ userInput.placeholder = '请输入需要撰写的品类名和要求...';
|
|
|
+ userInput.classList.remove('input-loading');
|
|
|
+ sendButton.disabled = false;
|
|
|
+ sendButton.classList.remove('stopping');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移动端关闭侧边栏
|
|
|
+ const isMobile = window.innerWidth <= 768;
|
|
|
+ if (isMobile) {
|
|
|
+ const sidebar = document.getElementById('history-sidebar');
|
|
|
+ const sidebarOverlay = document.getElementById('sidebar-overlay');
|
|
|
+ if (sidebar && sidebarOverlay) {
|
|
|
+ sidebar.classList.remove('show');
|
|
|
+ sidebarOverlay.classList.remove('show');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ showMessage('创建新对话失败: ' + (data ? data.error : '未知错误'), 'error');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ showMessage('创建新对话时发生错误: ' + error.message, 'error');
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ // 恢复按钮状态
|
|
|
+ if (newChatBtn) {
|
|
|
+ newChatBtn.innerHTML = `
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ 开启新对话
|
|
|
+ `;
|
|
|
+ newChatBtn.disabled = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function clearCurrentConversation() {
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages) {
|
|
|
+ chatMessages.innerHTML = `
|
|
|
+ <div class="message bot">
|
|
|
+ <div class="message-content">请发送你的投放数据。如果是视频诊断,目前仅能诊断文案,请去聚德神转转为文案(最好是带时间戳的字幕-聚德智能视频处理工具可导出)。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除全局状态管理中的当前激活对话
|
|
|
+ if (typeof currentActiveConversationId !== 'undefined') {
|
|
|
+ currentActiveConversationId = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除强制创建新对话的标志(因为已经创建了新对话)
|
|
|
+ window.forceNewConversation = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadConversation(conversationId) {
|
|
|
+ // 检查认证状态
|
|
|
+ const token = localStorage.getItem('auth_token');
|
|
|
+ if (!token) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置激活的对话
|
|
|
+ if (typeof setActiveConversation === 'function') {
|
|
|
+ setActiveConversation(conversationId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先切换到指定对话,然后加载内容
|
|
|
+ switchToConversationAndLoad(conversationId, token);
|
|
|
+ }
|
|
|
+
|
|
|
+ function switchToConversationAndLoad(conversationId, token) {
|
|
|
+ // 调用后端API切换到指定对话
|
|
|
+ fetch(`/api/conversations/${conversationId}/switch`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ }).then(response => {
|
|
|
+ if (response.status === 401) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return response.json();
|
|
|
+ }).then(data => {
|
|
|
+ if (data && data.success) {
|
|
|
+ // 更新当前对话ID和session_id
|
|
|
+ window.currentConversationId = data.conversation.conversation_id;
|
|
|
+ window.currentSessionId = data.conversation.session_id;
|
|
|
+
|
|
|
+ // 加载对话内容
|
|
|
+ loadConversationSimple(conversationId, token);
|
|
|
+ } else {
|
|
|
+ showMessage('切换对话失败: ' + (data ? data.error : '未知错误'), 'error');
|
|
|
+ }
|
|
|
+ }).catch(error => {
|
|
|
+ showMessage('切换对话时发生错误: ' + error.message, 'error');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadConversationSimple(conversationId, token) {
|
|
|
+ // 检查是否有正在进行的流式响应
|
|
|
+ const hasActiveStream = window.conversationStates && window.conversationStates.has(conversationId);
|
|
|
+ const streamState = hasActiveStream ? window.conversationStates.get(conversationId) : null;
|
|
|
+ // 检查是否真正在流式响应(只要 isStreaming 为 true,无论是否有 messageElement)
|
|
|
+ const isActuallyStreaming = streamState && streamState.isStreaming;
|
|
|
+ // 检查是否有可用的消息元素(用于判断是否可以通过 updateExistingMessages 处理)
|
|
|
+ const hasMessageElement = streamState && streamState.messageElement && streamState.messageElement.parentNode;
|
|
|
+
|
|
|
+ // 返回 Promise
|
|
|
+ return fetch(`/api/conversations/${conversationId}`, {
|
|
|
+ method: 'GET',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ if (response.status === 401) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return response.json();
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ if (data && data.success) {
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+
|
|
|
+
|
|
|
+ // 如果有正在进行的流式响应且有可用的消息元素,保留流式响应元素
|
|
|
+ if (isActuallyStreaming && hasMessageElement) {
|
|
|
+ // 保留流式响应的消息元素,只更新历史消息
|
|
|
+ updateExistingMessages(data.messages, streamState.messageElement);
|
|
|
+ } else {
|
|
|
+ // 清空聊天区域并重新渲染所有消息
|
|
|
+ chatMessages.innerHTML = '';
|
|
|
+
|
|
|
+ // 如果没有消息,显示提示语
|
|
|
+ if (!data.messages || data.messages.length === 0) {
|
|
|
+ chatMessages.innerHTML = `
|
|
|
+ <div class="message bot">
|
|
|
+ <div class="message-content">请发送你的投放数据。如果是视频诊断,目前仅能诊断文案,请去聚德神转转为文案(最好是带时间戳的字幕-聚德智能视频处理工具可导出)。</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ renderAllMessages(data.messages, chatMessages);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保用户消息在切换对话后仍然可见
|
|
|
+ // 检查是否有未保存的用户消息
|
|
|
+ if (streamState && streamState.userMessages) {
|
|
|
+ streamState.userMessages.forEach(userMsg => {
|
|
|
+ // 检查用户消息是否已经在DOM中
|
|
|
+ const existingUserMsg = Array.from(chatMessages.children).find(msg =>
|
|
|
+ msg.classList.contains('user') &&
|
|
|
+ msg.querySelector('.message-content')?.textContent === userMsg.content
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!existingUserMsg) {
|
|
|
+ // 如果用户消息不在DOM中,重新添加
|
|
|
+ chatMessages.appendChild(userMsg.element);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 恢复流式响应内容(无论是正在进行还是已完成但未保存到数据库) =====
|
|
|
+ // 检查数据库中是否已经包含了流式响应的内容
|
|
|
+ let shouldRestoreStreamContent = false;
|
|
|
+
|
|
|
+ // 如果流式响应正在进行,必须恢复显示
|
|
|
+ if (streamState && isActuallyStreaming) {
|
|
|
+ shouldRestoreStreamContent = true;
|
|
|
+ }
|
|
|
+ // 如果流式响应已完成,检查是否需要恢复(内容未保存到数据库)
|
|
|
+ else if (streamState && !isActuallyStreaming && (streamState.currentMessage || (streamState.currentThoughts && streamState.currentThoughts.length > 0))) {
|
|
|
+ // 检查数据库返回的最后一条 assistant 消息是否与流式响应的内容匹配
|
|
|
+ const dbMessages = data.messages || [];
|
|
|
+ const lastAssistantMsg = dbMessages.filter(msg => msg.role === 'assistant').pop();
|
|
|
+
|
|
|
+ if (lastAssistantMsg) {
|
|
|
+ // 如果最后一条 assistant 消息的内容与流式响应的内容匹配,说明内容已经保存到数据库
|
|
|
+ // 使用更精确的匹配:检查消息内容是否相同或包含流式响应的主要内容
|
|
|
+ let contentMatches = false;
|
|
|
+ if (streamState.currentMessage) {
|
|
|
+ const streamContent = streamState.currentMessage.trim();
|
|
|
+ const dbContent = lastAssistantMsg.content.trim();
|
|
|
+ // 精确匹配或检查前100个字符是否匹配(处理可能的截断)
|
|
|
+ contentMatches = (dbContent === streamContent) ||
|
|
|
+ (dbContent.length > 50 && streamContent.length > 50 &&
|
|
|
+ dbContent.substring(0, 100) === streamContent.substring(0, 100));
|
|
|
+ } else {
|
|
|
+ // 如果没有流式响应内容,认为匹配
|
|
|
+ contentMatches = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ let thoughtsMatch = false;
|
|
|
+ if (streamState.currentThoughts && streamState.currentThoughts.length > 0) {
|
|
|
+ const streamThoughts = streamState.currentThoughts.join('').trim();
|
|
|
+ const dbThoughts = (lastAssistantMsg.thinking_content || '').trim();
|
|
|
+ thoughtsMatch = (dbThoughts === streamThoughts) ||
|
|
|
+ (dbThoughts.length > 50 && streamThoughts.length > 50 &&
|
|
|
+ dbThoughts.substring(0, 100) === streamThoughts.substring(0, 100));
|
|
|
+ } else {
|
|
|
+ // 如果流式响应没有思考过程,检查数据库是否有思考过程
|
|
|
+ thoughtsMatch = !lastAssistantMsg.thinking_content || lastAssistantMsg.thinking_content.trim() === '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只有当内容不匹配时,才需要恢复流式响应的内容
|
|
|
+ shouldRestoreStreamContent = !contentMatches || !thoughtsMatch;
|
|
|
+ } else {
|
|
|
+ // 如果数据库中没有 assistant 消息,说明确实需要恢复流式响应的内容
|
|
|
+ shouldRestoreStreamContent = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果内容已保存在数据库中,清除流式响应状态以避免下次重复检查
|
|
|
+ if (!shouldRestoreStreamContent) {
|
|
|
+ streamState.currentMessage = '';
|
|
|
+ streamState.currentThoughts = [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (shouldRestoreStreamContent && streamState) {
|
|
|
+ // 如果流式响应正在进行或已完成,但消息元素不在DOM中,需要重新创建或恢复
|
|
|
+ let restoredMessageElement = streamState.messageElement;
|
|
|
+
|
|
|
+ // 如果流式响应正在进行,确保消息元素存在
|
|
|
+ const isStreamingNow = streamState.isStreaming;
|
|
|
+
|
|
|
+ // 如果消息元素不存在或不在DOM中
|
|
|
+ if (!restoredMessageElement) {
|
|
|
+ // 消息元素不存在,需要创建
|
|
|
+ restoredMessageElement = document.createElement('div');
|
|
|
+ restoredMessageElement.className = 'message bot';
|
|
|
+
|
|
|
+ // 创建思考过程容器(如果有)
|
|
|
+ if (streamState.currentThoughts && streamState.currentThoughts.length > 0) {
|
|
|
+ const thoughtContainer = document.createElement('div');
|
|
|
+ thoughtContainer.className = 'thought-container';
|
|
|
+
|
|
|
+ const thoughtToggleBtn = document.createElement('button');
|
|
|
+ thoughtToggleBtn.className = 'thought-toggle-btn';
|
|
|
+ thoughtToggleBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
|
+ thoughtToggleBtn.title = '收起/展开思考过程';
|
|
|
+ thoughtToggleBtn.onclick = function (e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ thoughtContainer.classList.toggle('collapsed');
|
|
|
+ };
|
|
|
+
|
|
|
+ const thoughtContent = document.createElement('div');
|
|
|
+ thoughtContent.className = 'thought-content';
|
|
|
+ if (typeof window.renderThoughts === 'function') {
|
|
|
+ thoughtContent.innerHTML = window.renderThoughts(streamState.currentThoughts.join(''));
|
|
|
+ } else {
|
|
|
+ thoughtContent.textContent = streamState.currentThoughts.join('');
|
|
|
+ }
|
|
|
+
|
|
|
+ thoughtContainer.appendChild(thoughtToggleBtn);
|
|
|
+ thoughtContainer.appendChild(thoughtContent);
|
|
|
+ restoredMessageElement.appendChild(thoughtContainer);
|
|
|
+
|
|
|
+ // 更新状态中的引用
|
|
|
+ streamState.thoughtContainer = thoughtContainer;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建消息内容容器
|
|
|
+ const contentContainer = document.createElement('div');
|
|
|
+ // 如果流式响应正在进行,添加 typing 类名
|
|
|
+ contentContainer.className = isStreamingNow ? 'message-content typing' : 'message-content';
|
|
|
+ if (streamState.currentMessage) {
|
|
|
+ if (typeof window.renderMarkdown === 'function') {
|
|
|
+ contentContainer.innerHTML = window.renderMarkdown(streamState.currentMessage);
|
|
|
+ if (typeof window.applyCodeHighlighting === 'function') {
|
|
|
+ window.applyCodeHighlighting(contentContainer);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ contentContainer.textContent = streamState.currentMessage;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ restoredMessageElement.appendChild(contentContainer);
|
|
|
+
|
|
|
+ // 保存 contentContainer 引用到状态中,以便流式响应继续更新
|
|
|
+ if (isStreamingNow && contentContainer) {
|
|
|
+ // 确保状态中的消息元素引用正确
|
|
|
+ streamState.messageElement = restoredMessageElement;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加到聊天区域
|
|
|
+ chatMessages.appendChild(restoredMessageElement);
|
|
|
+
|
|
|
+ // 添加对话ID标识,便于后续管理
|
|
|
+ restoredMessageElement.setAttribute('data-conversation-id', conversationId);
|
|
|
+
|
|
|
+ // 更新状态中的引用(确保流式响应能够继续更新)
|
|
|
+ streamState.messageElement = restoredMessageElement;
|
|
|
+
|
|
|
+ // 如果流式响应正在进行,确保恢复后能够继续更新
|
|
|
+ if (isStreamingNow) {
|
|
|
+ // 确保消息元素可见
|
|
|
+ restoredMessageElement.style.opacity = '1';
|
|
|
+ restoredMessageElement.style.transform = 'translateY(0)';
|
|
|
+
|
|
|
+ // 滚动到底部以看到实时更新
|
|
|
+ setTimeout(() => {
|
|
|
+ chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+ } else if (!restoredMessageElement.parentNode) {
|
|
|
+ // 消息元素存在但不在DOM中,需要重新添加到DOM
|
|
|
+ chatMessages.appendChild(restoredMessageElement);
|
|
|
+
|
|
|
+ // 确保消息元素可见
|
|
|
+ restoredMessageElement.style.opacity = '1';
|
|
|
+ restoredMessageElement.style.transform = 'translateY(0)';
|
|
|
+
|
|
|
+ // 如果流式响应正在进行,滚动到底部
|
|
|
+ if (isStreamingNow) {
|
|
|
+ setTimeout(() => {
|
|
|
+ chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果消息元素还在DOM中,只需要确保内容是最新的
|
|
|
+ const contentContainer = restoredMessageElement.querySelector('.message-content');
|
|
|
+ if (contentContainer) {
|
|
|
+ // 如果流式响应正在进行,确保有 typing 类名
|
|
|
+ if (isStreamingNow && !contentContainer.classList.contains('typing')) {
|
|
|
+ contentContainer.classList.add('typing');
|
|
|
+ } else if (!isStreamingNow && contentContainer.classList.contains('typing')) {
|
|
|
+ contentContainer.classList.remove('typing');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新内容
|
|
|
+ if (streamState.currentMessage) {
|
|
|
+ if (typeof window.renderMarkdown === 'function') {
|
|
|
+ contentContainer.innerHTML = window.renderMarkdown(streamState.currentMessage);
|
|
|
+ if (typeof window.applyCodeHighlighting === 'function') {
|
|
|
+ window.applyCodeHighlighting(contentContainer);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ contentContainer.textContent = streamState.currentMessage;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 恢复思考过程
|
|
|
+ if (streamState.thoughtContainer && streamState.currentThoughts && streamState.currentThoughts.length > 0) {
|
|
|
+ const thoughtContent = streamState.thoughtContainer.querySelector('.thought-content');
|
|
|
+ if (thoughtContent) {
|
|
|
+ if (typeof window.renderThoughts === 'function') {
|
|
|
+ thoughtContent.innerHTML = window.renderThoughts(streamState.currentThoughts.join(''));
|
|
|
+ } else {
|
|
|
+ thoughtContent.textContent = streamState.currentThoughts.join('');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保消息元素可见
|
|
|
+ restoredMessageElement.style.opacity = '1';
|
|
|
+ restoredMessageElement.style.transform = 'translateY(0)';
|
|
|
+
|
|
|
+ // 为新恢复的消息添加复制按钮
|
|
|
+ if (typeof window.setupCopyButtons === 'function') {
|
|
|
+ window.setupCopyButtons();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新当前对话ID和session_id
|
|
|
+ window.currentConversationId = conversationId;
|
|
|
+ if (data.conversation && data.conversation.session_id) {
|
|
|
+ window.currentSessionId = data.conversation.session_id;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新输入框状态 - 确保切换对话时输入框状态正确
|
|
|
+ if (typeof updateInputUIForConversation === 'function') {
|
|
|
+ updateInputUIForConversation(conversationId);
|
|
|
+ } else if (typeof resetUIState === 'function') {
|
|
|
+ resetUIState();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 滚动到底部
|
|
|
+ setTimeout(() => {
|
|
|
+ chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
+ }, 100);
|
|
|
+ } else {
|
|
|
+ showMessage('加载对话失败: ' + (data ? data.error : '未知错误'), 'error');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ showMessage('加载对话时发生错误: ' + error.message, 'error');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateExistingMessages(messages, streamElement) {
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ const existingMessages = Array.from(chatMessages.children);
|
|
|
+
|
|
|
+ // 找到流式响应元素的位置
|
|
|
+ const streamIndex = existingMessages.indexOf(streamElement);
|
|
|
+
|
|
|
+ // 移除流式响应元素之前的所有消息(这些是历史消息)
|
|
|
+ for (let i = 0; i < streamIndex; i++) {
|
|
|
+ if (existingMessages[i] && existingMessages[i].parentNode) {
|
|
|
+ existingMessages[i].remove();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 在流式响应元素之前插入所有历史消息
|
|
|
+ messages.forEach((msg, index) => {
|
|
|
+ // 显示所有历史消息,包括用户消息和AI回复
|
|
|
+ const messageDiv = createMessageElement(msg);
|
|
|
+ chatMessages.insertBefore(messageDiv, streamElement);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderAllMessages(messages, chatMessages) {
|
|
|
+ messages.forEach((msg, index) => {
|
|
|
+ const messageDiv = createMessageElement(msg);
|
|
|
+ chatMessages.appendChild(messageDiv);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function createMessageElement(msg) {
|
|
|
+ const messageDiv = document.createElement('div');
|
|
|
+ messageDiv.className = `message ${msg.role}`;
|
|
|
+
|
|
|
+ let content = msg.content;
|
|
|
+
|
|
|
+ // 如果是AI消息且有思考过程,添加思考容器
|
|
|
+ if (msg.role === 'assistant' && msg.thinking_content) {
|
|
|
+ // 使用思考过程专用的渲染函数
|
|
|
+ let thoughtHtml = '';
|
|
|
+ if (typeof window.renderThoughts === 'function') {
|
|
|
+ thoughtHtml = window.renderThoughts(msg.thinking_content);
|
|
|
+ } else if (typeof window.renderMarkdown === 'function') {
|
|
|
+ thoughtHtml = window.renderMarkdown(msg.thinking_content);
|
|
|
+ } else if (typeof showdown !== 'undefined') {
|
|
|
+ const converter = new showdown.Converter({
|
|
|
+ strikethrough: true,
|
|
|
+ tables: true,
|
|
|
+ tasklists: true,
|
|
|
+ ghCodeBlocks: true,
|
|
|
+ openLinksInNewWindow: true
|
|
|
+ });
|
|
|
+ thoughtHtml = converter.makeHtml(msg.thinking_content);
|
|
|
+ } else {
|
|
|
+ thoughtHtml = msg.thinking_content;
|
|
|
+ }
|
|
|
+
|
|
|
+ const thoughtContainerHtml = `
|
|
|
+ <div class="thought-container" data-collapsed="false">
|
|
|
+ <button class="thought-toggle-btn" onclick="toggleThought(this)">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <div class="thought-content">
|
|
|
+ ${thoughtHtml}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ content = thoughtContainerHtml + content;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用统一的Markdown渲染函数渲染消息内容
|
|
|
+ let renderedContent = content;
|
|
|
+ if (msg.role === 'assistant') {
|
|
|
+ if (typeof window.renderMarkdown === 'function') {
|
|
|
+ // 只渲染消息内容部分,思考过程已经渲染过了
|
|
|
+ const messageContentOnly = msg.content;
|
|
|
+ renderedContent = msg.thinking_content ?
|
|
|
+ content.replace(msg.content, window.renderMarkdown(messageContentOnly)) :
|
|
|
+ window.renderMarkdown(messageContentOnly);
|
|
|
+ } else if (typeof showdown !== 'undefined') {
|
|
|
+ const converter = new showdown.Converter({
|
|
|
+ strikethrough: true,
|
|
|
+ tables: true,
|
|
|
+ tasklists: true,
|
|
|
+ ghCodeBlocks: true,
|
|
|
+ openLinksInNewWindow: true
|
|
|
+ });
|
|
|
+ // 只渲染消息内容部分,思考过程已经渲染过了
|
|
|
+ const messageContentOnly = msg.content;
|
|
|
+ renderedContent = msg.thinking_content ?
|
|
|
+ content.replace(msg.content, converter.makeHtml(messageContentOnly)) :
|
|
|
+ converter.makeHtml(messageContentOnly);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 为用户消息添加编辑按钮
|
|
|
+ let messageContentHtml = `<div class="message-content">${renderedContent}</div>`;
|
|
|
+ if (msg.role === 'user' && msg.message_id) {
|
|
|
+ // 转义特殊字符,防止XSS攻击
|
|
|
+ const escapedContent = msg.content.replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
|
|
+ messageContentHtml = `
|
|
|
+ <div class="message-content">${renderedContent}</div>
|
|
|
+ <button class="edit-button" onclick="editMessage(${msg.message_id}, '${escapedContent}')" title="编辑消息">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ messageDiv.innerHTML = messageContentHtml;
|
|
|
+
|
|
|
+ // 为用户消息添加data-message-id属性
|
|
|
+ if (msg.role === 'user' && msg.message_id) {
|
|
|
+ messageDiv.setAttribute('data-message-id', msg.message_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保消息立即可见
|
|
|
+ messageDiv.style.opacity = '1';
|
|
|
+ messageDiv.style.transform = 'translateY(0)';
|
|
|
+
|
|
|
+ // 应用代码高亮和表格处理
|
|
|
+ if (typeof applyCodeHighlighting === 'function') {
|
|
|
+ applyCodeHighlighting(messageDiv);
|
|
|
+ }
|
|
|
+ if (typeof processAuditResultTables === 'function') {
|
|
|
+ processAuditResultTables();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 为历史消息添加复制按钮
|
|
|
+ if (typeof window.setupCopyButtons === 'function') {
|
|
|
+ window.setupCopyButtons();
|
|
|
+ }
|
|
|
+
|
|
|
+ return messageDiv;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ function updateConversationSelection(conversationId) {
|
|
|
+ const conversationItems = document.querySelectorAll('.conversation-item');
|
|
|
+ conversationItems.forEach(item => {
|
|
|
+ const itemId = item.getAttribute('data-conversation-id');
|
|
|
+ if (itemId === conversationId) {
|
|
|
+ item.classList.add('active');
|
|
|
+ // 确保状态点显示
|
|
|
+ const statusDot = item.querySelector('.conversation-status-dot');
|
|
|
+ if (statusDot) {
|
|
|
+ statusDot.style.opacity = '1';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ item.classList.remove('active');
|
|
|
+ // 移除非活跃对话的状态类
|
|
|
+ item.classList.remove('streaming', 'paused');
|
|
|
+ const statusDot = item.querySelector('.conversation-status-dot');
|
|
|
+ if (statusDot) {
|
|
|
+ statusDot.style.opacity = '0';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateConversationStatus(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';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function showConversationMenu(conversationId, button) {
|
|
|
+ // 创建菜单
|
|
|
+ const menu = document.createElement('div');
|
|
|
+ menu.className = 'conversation-context-menu';
|
|
|
+ menu.innerHTML = `
|
|
|
+ <div class="menu-item" data-action="rename">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>
|
|
|
+ 重命名
|
|
|
+ </div>
|
|
|
+ <div class="menu-item" data-action="delete">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>
|
|
|
+ 删除
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 添加样式
|
|
|
+ menu.style.cssText = `
|
|
|
+ position: absolute;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ backdrop-filter: blur(20px);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.3);
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
|
|
+ padding: 8px 0;
|
|
|
+ min-width: 120px;
|
|
|
+ z-index: 1000;
|
|
|
+ opacity: 0;
|
|
|
+ transform: scale(0.9);
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 定位菜单
|
|
|
+ const rect = button.getBoundingClientRect();
|
|
|
+ menu.style.left = (rect.left - 100) + 'px';
|
|
|
+ menu.style.top = (rect.bottom + 5) + 'px';
|
|
|
+
|
|
|
+ // 添加到页面
|
|
|
+ document.body.appendChild(menu);
|
|
|
+
|
|
|
+ // 显示动画
|
|
|
+ setTimeout(() => {
|
|
|
+ menu.style.opacity = '1';
|
|
|
+ menu.style.transform = 'scale(1)';
|
|
|
+ }, 10);
|
|
|
+
|
|
|
+ // 菜单项点击事件
|
|
|
+ menu.querySelectorAll('.menu-item').forEach(item => {
|
|
|
+ item.addEventListener('click', function () {
|
|
|
+ const action = this.getAttribute('data-action');
|
|
|
+ handleConversationAction(action, conversationId);
|
|
|
+ closeMenu();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 点击外部关闭菜单
|
|
|
+ function closeMenu() {
|
|
|
+ menu.style.opacity = '0';
|
|
|
+ menu.style.transform = 'scale(0.9)';
|
|
|
+ setTimeout(() => {
|
|
|
+ if (menu.parentNode) {
|
|
|
+ menu.parentNode.removeChild(menu);
|
|
|
+ }
|
|
|
+ }, 200);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 点击外部关闭
|
|
|
+ document.addEventListener('click', function closeOnOutsideClick(e) {
|
|
|
+ if (!menu.contains(e.target) && e.target !== button) {
|
|
|
+ closeMenu();
|
|
|
+ document.removeEventListener('click', closeOnOutsideClick);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleConversationAction(action, conversationId) {
|
|
|
+ switch (action) {
|
|
|
+ case 'rename':
|
|
|
+ renameConversation(conversationId);
|
|
|
+ break;
|
|
|
+ case 'delete':
|
|
|
+ deleteConversation(conversationId);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function renameConversation(conversationId) {
|
|
|
+ // 检查认证状态
|
|
|
+ const token = localStorage.getItem('auth_token');
|
|
|
+ if (!token) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取当前对话标题
|
|
|
+ const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
|
|
|
+ const currentTitle = conversationItem ? conversationItem.querySelector('.conversation-title').textContent : '';
|
|
|
+
|
|
|
+ // 设置输入框的值
|
|
|
+ const renameInput = document.getElementById('rename-input');
|
|
|
+ if (renameInput) {
|
|
|
+ renameInput.value = currentTitle;
|
|
|
+ renameInput.focus();
|
|
|
+ renameInput.select();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示重命名弹框
|
|
|
+ showModal('rename-modal');
|
|
|
+
|
|
|
+ // 绑定确认按钮事件
|
|
|
+ const confirmBtn = document.getElementById('confirm-rename-btn');
|
|
|
+ if (confirmBtn) {
|
|
|
+ confirmBtn.onclick = function () {
|
|
|
+ const newTitle = renameInput.value.trim();
|
|
|
+ if (newTitle && newTitle !== currentTitle) {
|
|
|
+ performRename(conversationId, newTitle, token);
|
|
|
+ } else if (!newTitle) {
|
|
|
+ showMessage('请输入新的对话标题', 'warning');
|
|
|
+ } else {
|
|
|
+ closeModal('rename-modal');
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绑定回车键事件
|
|
|
+ if (renameInput) {
|
|
|
+ renameInput.onkeydown = function (e) {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+ confirmBtn.click();
|
|
|
+ } else if (e.key === 'Escape') {
|
|
|
+ closeModal('rename-modal');
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function performRename(conversationId, newTitle, token) {
|
|
|
+ fetch(`/api/conversations/${conversationId}/rename`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ title: newTitle
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ if (response.status === 401) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return response.json();
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ if (data && data.success) {
|
|
|
+ const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
|
|
|
+ if (conversationItem) {
|
|
|
+ const titleElement = conversationItem.querySelector('.conversation-title');
|
|
|
+ if (titleElement) {
|
|
|
+ titleElement.textContent = newTitle;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ closeModal('rename-modal');
|
|
|
+ showMessage('对话标题已更新');
|
|
|
+ } else {
|
|
|
+ showMessage('重命名失败: ' + (data ? data.error : '未知错误'), 'error');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ showMessage('重命名对话时发生错误: ' + error.message, 'error');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function deleteConversation(conversationId) {
|
|
|
+ // 检查认证状态
|
|
|
+ const token = localStorage.getItem('auth_token');
|
|
|
+ if (!token) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示删除确认弹框
|
|
|
+ showModal('delete-modal');
|
|
|
+
|
|
|
+ // 绑定确认删除按钮事件
|
|
|
+ const confirmBtn = document.getElementById('confirm-delete-btn');
|
|
|
+ if (confirmBtn) {
|
|
|
+ confirmBtn.onclick = function () {
|
|
|
+ performDelete(conversationId, token);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function performDelete(conversationId, token) {
|
|
|
+ fetch(`/api/conversations/${conversationId}`, {
|
|
|
+ method: 'DELETE',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ if (response.status === 401) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return response.json();
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ if (data && data.success) {
|
|
|
+ const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
|
|
|
+ if (conversationItem) {
|
|
|
+ conversationItem.remove();
|
|
|
+ }
|
|
|
+ closeModal('delete-modal');
|
|
|
+ showMessage('对话已删除');
|
|
|
+
|
|
|
+ // 如果删除的是当前对话,清除当前对话ID
|
|
|
+ if (window.currentConversationId === conversationId) {
|
|
|
+ window.currentConversationId = null;
|
|
|
+ // 清空聊天区域
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages) {
|
|
|
+ chatMessages.innerHTML = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ showMessage('删除失败: ' + (data ? data.error : '未知错误'), 'error');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ showMessage('删除对话时发生错误: ' + error.message, 'error');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadConversationHistory(autoSelectLatest = false) {
|
|
|
+ // 检查认证状态
|
|
|
+ const token = localStorage.getItem('auth_token');
|
|
|
+ if (!token) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从服务器加载历史记录
|
|
|
+ fetch('/api/conversations', {
|
|
|
+ method: 'GET',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ if (response.status === 401) {
|
|
|
+ // 认证失败,清除本地存储并重定向到登录页面
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return response.json();
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ if (data && data.success) {
|
|
|
+ updateHistorySidebar(data.conversations, autoSelectLatest);
|
|
|
+ } else {
|
|
|
+ // 显示空状态
|
|
|
+ const historyLoading = document.getElementById('history-loading');
|
|
|
+ const historyEmpty = document.getElementById('history-empty');
|
|
|
+ if (historyLoading) historyLoading.style.display = 'none';
|
|
|
+ if (historyEmpty) historyEmpty.style.display = 'block';
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ // 显示空状态
|
|
|
+ const historyLoading = document.getElementById('history-loading');
|
|
|
+ const historyEmpty = document.getElementById('history-empty');
|
|
|
+ if (historyLoading) historyLoading.style.display = 'none';
|
|
|
+ if (historyEmpty) historyEmpty.style.display = 'block';
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将loadConversationHistory函数暴露到全局作用域,供chat.js调用
|
|
|
+ window.loadConversationHistory = loadConversationHistory;
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ function updateHistorySidebar(conversations, autoSelectLatest = false) {
|
|
|
+ // 更新侧边栏的历史记录
|
|
|
+ const historyContent = document.querySelector('.history-content');
|
|
|
+ const historyLoading = document.getElementById('history-loading');
|
|
|
+ const historyEmpty = document.getElementById('history-empty');
|
|
|
+
|
|
|
+ if (!historyContent) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 隐藏加载状态
|
|
|
+ if (historyLoading) {
|
|
|
+ historyLoading.style.display = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清空现有内容
|
|
|
+ historyContent.innerHTML = '';
|
|
|
+
|
|
|
+ // 检查是否有任何对话
|
|
|
+ const hasAnyConversations = Object.values(conversations).some(group => group && group.length > 0);
|
|
|
+
|
|
|
+ if (!hasAnyConversations) {
|
|
|
+ // 显示空状态
|
|
|
+ if (historyEmpty) {
|
|
|
+ historyEmpty.style.display = 'block';
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 隐藏空状态
|
|
|
+ if (historyEmpty) {
|
|
|
+ historyEmpty.style.display = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按时间分组渲染
|
|
|
+ const timeGroups = [
|
|
|
+ { key: 'today', label: '今天' },
|
|
|
+ { key: 'yesterday', label: '昨天' },
|
|
|
+ { key: 'week', label: '7天内' },
|
|
|
+ { key: 'month', label: '30天内' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ timeGroups.forEach(group => {
|
|
|
+ const groupConversations = conversations[group.key] || [];
|
|
|
+ if (groupConversations.length === 0) return;
|
|
|
+
|
|
|
+ // 创建分组容器
|
|
|
+ const groupDiv = document.createElement('div');
|
|
|
+ groupDiv.className = 'history-group';
|
|
|
+
|
|
|
+ // 分组标题
|
|
|
+ const headerDiv = document.createElement('div');
|
|
|
+ headerDiv.className = 'group-header';
|
|
|
+ headerDiv.textContent = group.label;
|
|
|
+
|
|
|
+ // 对话列表
|
|
|
+ const listDiv = document.createElement('div');
|
|
|
+ listDiv.className = 'conversation-list';
|
|
|
+
|
|
|
+ groupConversations.forEach(conv => {
|
|
|
+ const itemDiv = document.createElement('div');
|
|
|
+ itemDiv.className = 'conversation-item';
|
|
|
+ itemDiv.setAttribute('data-conversation-id', conv.conversation_id);
|
|
|
+ itemDiv.setAttribute('data-session-id', conv.session_id || '');
|
|
|
+
|
|
|
+ // 格式化时间显示
|
|
|
+ const lastMessageTime = conv.last_message_at ? new Date(conv.last_message_at).toLocaleTimeString('zh-CN', {
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit'
|
|
|
+ }) : '';
|
|
|
+
|
|
|
+ itemDiv.innerHTML = `
|
|
|
+ <div class="conversation-content">
|
|
|
+ <div class="conversation-title">${conv.title}</div>
|
|
|
+ <div class="conversation-meta">
|
|
|
+ <span class="message-count">${conv.message_count}条消息</span>
|
|
|
+ ${lastMessageTime ? `<span class="last-time">${lastMessageTime}</span>` : ''}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="conversation-status-dot"></div>
|
|
|
+ <button class="conversation-menu" data-conversation-id="${conv.conversation_id}">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <circle cx="12" cy="12" r="1" fill="currentColor"/>
|
|
|
+ <circle cx="19" cy="12" r="1" fill="currentColor"/>
|
|
|
+ <circle cx="5" cy="12" r="1" fill="currentColor"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ `;
|
|
|
+
|
|
|
+ listDiv.appendChild(itemDiv);
|
|
|
+ });
|
|
|
+
|
|
|
+ groupDiv.appendChild(headerDiv);
|
|
|
+ groupDiv.appendChild(listDiv);
|
|
|
+ historyContent.appendChild(groupDiv);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 重新绑定事件
|
|
|
+ bindConversationEvents();
|
|
|
+
|
|
|
+ // 自动选中对话
|
|
|
+ const firstConversationItem = document.querySelector('.conversation-item');
|
|
|
+ if (firstConversationItem) {
|
|
|
+ const conversationId = firstConversationItem.getAttribute('data-conversation-id');
|
|
|
+
|
|
|
+ if (autoSelectLatest) {
|
|
|
+ // 新对话完成后,选中最新的对话,但不加载内容(保持提示语显示)
|
|
|
+ updateConversationSelection(conversationId);
|
|
|
+ // 不调用 loadConversation,保持提示语显示
|
|
|
+ } else if (!window.currentConversationId) {
|
|
|
+ // 页面初始加载时,选中第一个对话
|
|
|
+ updateConversationSelection(conversationId);
|
|
|
+ loadConversation(conversationId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function bindConversationEvents() {
|
|
|
+ // 重新绑定对话项点击事件
|
|
|
+ const conversationItems = document.querySelectorAll('.conversation-item');
|
|
|
+ conversationItems.forEach(item => {
|
|
|
+ item.addEventListener('click', function (e) {
|
|
|
+ if (e.target.closest('.conversation-menu')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const conversationId = this.getAttribute('data-conversation-id');
|
|
|
+ const sessionId = this.getAttribute('data-session-id');
|
|
|
+
|
|
|
+ // 设置当前对话的session_id
|
|
|
+ if (sessionId) {
|
|
|
+ window.currentSessionId = sessionId;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置激活的对话
|
|
|
+ if (typeof setActiveConversation === 'function') {
|
|
|
+ setActiveConversation(conversationId);
|
|
|
+ }
|
|
|
+
|
|
|
+ loadConversation(conversationId);
|
|
|
+ updateConversationSelection(conversationId);
|
|
|
+
|
|
|
+ // 移动端关闭侧边栏
|
|
|
+ const isMobile = window.innerWidth <= 768;
|
|
|
+ if (isMobile) {
|
|
|
+ const sidebar = document.getElementById('history-sidebar');
|
|
|
+ const sidebarOverlay = document.getElementById('sidebar-overlay');
|
|
|
+ if (sidebar && sidebarOverlay) {
|
|
|
+ sidebar.classList.remove('show');
|
|
|
+ sidebarOverlay.classList.remove('show');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 重新绑定对话菜单事件
|
|
|
+ const conversationMenus = document.querySelectorAll('.conversation-menu');
|
|
|
+ conversationMenus.forEach(menu => {
|
|
|
+ menu.addEventListener('click', function (e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ const conversationId = this.getAttribute('data-conversation-id');
|
|
|
+ showConversationMenu(conversationId, this);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function syncSidebarUserInfo() {
|
|
|
+ const userInfo = localStorage.getItem('user_info');
|
|
|
+ if (userInfo) {
|
|
|
+ try {
|
|
|
+ const user = JSON.parse(userInfo);
|
|
|
+ const sidebarUserName = document.getElementById('sidebar-user-name');
|
|
|
+ const sidebarAvatarImg = document.getElementById('sidebar-avatar-img');
|
|
|
+ const sidebarAvatarFallback = document.getElementById('sidebar-avatar-fallback');
|
|
|
+
|
|
|
+ if (sidebarUserName) {
|
|
|
+ const displayName = user.nickname || user.name || '用户';
|
|
|
+ sidebarUserName.textContent = displayName;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (sidebarAvatarImg && user.avatar && user.avatar.trim() !== '') {
|
|
|
+ sidebarAvatarImg.src = user.avatar;
|
|
|
+ sidebarAvatarImg.style.display = 'block';
|
|
|
+ sidebarAvatarFallback.style.display = 'none';
|
|
|
+ } else if (sidebarAvatarFallback) {
|
|
|
+ sidebarAvatarImg.style.display = 'none';
|
|
|
+ sidebarAvatarFallback.style.display = 'flex';
|
|
|
+ const displayName = user.nickname || user.name || 'U';
|
|
|
+ sidebarAvatarFallback.textContent = displayName.charAt(0).toUpperCase();
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 编辑消息功能
|
|
|
+ let currentEditMessageId = null;
|
|
|
+ let currentEditMessageElement = null;
|
|
|
+ window.isEditingMessage = false; // 设置为全局变量
|
|
|
+
|
|
|
+ window.editMessage = function (messageId, originalContent) {
|
|
|
+ // 如果已经在编辑其他消息,先取消
|
|
|
+ if (currentEditMessageElement) {
|
|
|
+ cancelEdit();
|
|
|
+ }
|
|
|
+
|
|
|
+ currentEditMessageId = messageId;
|
|
|
+
|
|
|
+ // 找到对应的消息元素
|
|
|
+ const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
|
|
|
+ if (!messageElement) {
|
|
|
+ console.error('找不到消息元素');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ currentEditMessageElement = messageElement;
|
|
|
+ window.isEditingMessage = true; // 设置编辑状态
|
|
|
+
|
|
|
+ // 保存当前滚动位置
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ const currentScrollTop = chatMessages.scrollTop;
|
|
|
+
|
|
|
+ // 解码HTML实体
|
|
|
+ const decodedContent = originalContent
|
|
|
+ .replace(/"/g, '"')
|
|
|
+ .replace(/'/g, "'")
|
|
|
+ .replace(/</g, '<')
|
|
|
+ .replace(/>/g, '>')
|
|
|
+ .replace(/&/g, '&');
|
|
|
+
|
|
|
+ // 创建编辑表单
|
|
|
+ const editForm = document.createElement('div');
|
|
|
+ editForm.className = 'edit-form';
|
|
|
+ editForm.innerHTML = `
|
|
|
+ <textarea class="edit-textarea" placeholder="请输入消息内容...">${decodedContent}</textarea>
|
|
|
+ <div class="edit-actions">
|
|
|
+ <button class="edit-cancel-btn" onclick="cancelEdit()">取消</button>
|
|
|
+ <button class="edit-send-btn" onclick="confirmEdit()">发送</button>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 隐藏原始消息内容,显示编辑表单
|
|
|
+ const messageContent = messageElement.querySelector('.message-content');
|
|
|
+ if (messageContent) {
|
|
|
+ messageContent.style.display = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 隐藏编辑按钮
|
|
|
+ const editButton = messageElement.querySelector('.edit-button');
|
|
|
+ if (editButton) {
|
|
|
+ editButton.style.display = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加编辑表单到消息元素
|
|
|
+ messageElement.appendChild(editForm);
|
|
|
+ messageElement.classList.add('message-editing');
|
|
|
+
|
|
|
+ // 恢复滚动位置
|
|
|
+ chatMessages.scrollTop = currentScrollTop;
|
|
|
+
|
|
|
+ // 聚焦到文本域
|
|
|
+ const textarea = editForm.querySelector('.edit-textarea');
|
|
|
+ if (textarea) {
|
|
|
+ textarea.focus();
|
|
|
+ textarea.select();
|
|
|
+
|
|
|
+ // 绑定键盘事件:Enter 发送(Shift+Enter 换行),Esc 取消
|
|
|
+ textarea.onkeydown = function (e) {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+ if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) {
|
|
|
+ return; // 保留换行/组合键
|
|
|
+ }
|
|
|
+ e.preventDefault();
|
|
|
+ confirmEdit();
|
|
|
+ } else if (e.key === 'Escape') {
|
|
|
+ cancelEdit();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function cancelEdit() {
|
|
|
+ if (currentEditMessageElement) {
|
|
|
+ // 保存当前滚动位置
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ const currentScrollTop = chatMessages.scrollTop;
|
|
|
+
|
|
|
+ // 恢复原始消息内容
|
|
|
+ const messageContent = currentEditMessageElement.querySelector('.message-content');
|
|
|
+ if (messageContent) {
|
|
|
+ messageContent.style.display = 'block';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示编辑按钮
|
|
|
+ const editButton = currentEditMessageElement.querySelector('.edit-button');
|
|
|
+ if (editButton) {
|
|
|
+ editButton.style.display = 'flex';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除编辑表单
|
|
|
+ const editForm = currentEditMessageElement.querySelector('.edit-form');
|
|
|
+ if (editForm) {
|
|
|
+ editForm.remove();
|
|
|
+ }
|
|
|
+
|
|
|
+ currentEditMessageElement.classList.remove('message-editing');
|
|
|
+
|
|
|
+ // 先恢复滚动位置,再清除编辑状态
|
|
|
+ chatMessages.scrollTop = currentScrollTop;
|
|
|
+
|
|
|
+ // 延迟清除编辑状态,确保DOM变化不会触发滚动
|
|
|
+ setTimeout(() => {
|
|
|
+ currentEditMessageElement = null;
|
|
|
+ currentEditMessageId = null;
|
|
|
+ window.isEditingMessage = false;
|
|
|
+ }, 150);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function confirmEdit() {
|
|
|
+ if (!currentEditMessageElement || !currentEditMessageId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const editForm = currentEditMessageElement.querySelector('.edit-form');
|
|
|
+ const textarea = editForm.querySelector('.edit-textarea');
|
|
|
+ const sendBtn = editForm.querySelector('.edit-send-btn');
|
|
|
+
|
|
|
+ if (!textarea || !sendBtn) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const newContent = textarea.value.trim();
|
|
|
+ if (!newContent) {
|
|
|
+ showMessage('消息内容不能为空', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示加载状态
|
|
|
+ const originalText = sendBtn.textContent;
|
|
|
+ sendBtn.textContent = '发送中...';
|
|
|
+ sendBtn.disabled = true;
|
|
|
+
|
|
|
+ // 调用后端API编辑消息
|
|
|
+ performEditMessage(currentEditMessageId, newContent);
|
|
|
+ }
|
|
|
+
|
|
|
+ function performEditMessage(messageId, newContent) {
|
|
|
+ // 检查认证状态
|
|
|
+ const token = localStorage.getItem('auth_token');
|
|
|
+ if (!token) {
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置输入框为加载状态
|
|
|
+ const userInput = document.getElementById('user-input');
|
|
|
+ const sendButton = document.getElementById('send-btn');
|
|
|
+
|
|
|
+ if (userInput && sendButton) {
|
|
|
+ userInput.classList.add('input-loading');
|
|
|
+ userInput.placeholder = '正在重新生成回复...';
|
|
|
+ userInput.disabled = true;
|
|
|
+ sendButton.innerHTML = `
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <rect x="6" y="6" width="12" height="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>`;
|
|
|
+ sendButton.classList.add('stopping');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 隐藏编辑表单
|
|
|
+ if (currentEditMessageElement) {
|
|
|
+ const editForm = currentEditMessageElement.querySelector('.edit-form');
|
|
|
+ if (editForm) {
|
|
|
+ editForm.style.display = 'none';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 立即隐藏将被删除的消息(编辑的消息及其之后的所有消息) =====
|
|
|
+ const editMessageElement = currentEditMessageElement;
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ let messagesToHide = [];
|
|
|
+ if (editMessageElement && chatMessages) {
|
|
|
+ let foundEditMessage = false;
|
|
|
+ const allMessages = Array.from(chatMessages.querySelectorAll('.message'));
|
|
|
+ allMessages.forEach(msg => {
|
|
|
+ if (msg === editMessageElement) {
|
|
|
+ foundEditMessage = true;
|
|
|
+ messagesToHide.push(msg);
|
|
|
+ // 立即隐藏消息(但保留DOM元素,后续再删除)
|
|
|
+ msg.style.display = 'none';
|
|
|
+ } else if (foundEditMessage) {
|
|
|
+ messagesToHide.push(msg);
|
|
|
+ // 立即隐藏消息
|
|
|
+ msg.style.display = 'none';
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的用户消息显示(立即显示编辑后的内容)
|
|
|
+ function createNewUserMessageImmediately() {
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (!chatMessages) return null;
|
|
|
+
|
|
|
+ const newUserMessageElement = document.createElement('div');
|
|
|
+ newUserMessageElement.className = 'message user';
|
|
|
+
|
|
|
+ const contentElement = document.createElement('div');
|
|
|
+ contentElement.className = 'message-content';
|
|
|
+ contentElement.textContent = newContent;
|
|
|
+
|
|
|
+ newUserMessageElement.appendChild(contentElement);
|
|
|
+
|
|
|
+ // 重新对话添加编辑按钮
|
|
|
+ if (typeof messageId !== 'undefined' && messageId !== null) {
|
|
|
+ const editButton = document.createElement('button');
|
|
|
+ editButton.className = 'edit-button';
|
|
|
+ editButton.setAttribute('title', '编辑消息');
|
|
|
+ editButton.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
|
+ editButton.onclick = function () {
|
|
|
+ if (typeof window.editMessage === 'function') window.editMessage(messageId, newContent);
|
|
|
+ };
|
|
|
+ newUserMessageElement.appendChild(editButton);
|
|
|
+ newUserMessageElement.setAttribute('data-message-id', messageId);
|
|
|
+ }
|
|
|
+
|
|
|
+ chatMessages.appendChild(newUserMessageElement);
|
|
|
+
|
|
|
+ // 确保消息立即可见
|
|
|
+ newUserMessageElement.style.opacity = '1';
|
|
|
+ newUserMessageElement.style.transform = 'translateY(0)';
|
|
|
+
|
|
|
+ return newUserMessageElement;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 立即创建并显示新的用户消息
|
|
|
+ const newUserMessageElement = createNewUserMessageImmediately();
|
|
|
+
|
|
|
+ // ===== 初始化对话状态(参考正常发送消息的逻辑) =====
|
|
|
+ const conversationId = window.currentConversationId || 'new';
|
|
|
+
|
|
|
+ // 初始化对话状态(如果不存在)
|
|
|
+ if (typeof window.initializeConversationState === 'function') {
|
|
|
+ window.initializeConversationState(conversationId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取或创建对话状态
|
|
|
+ const conversationStates = window.conversationStates || new Map();
|
|
|
+ const state = conversationStates.get(conversationId) || {
|
|
|
+ isStreaming: false,
|
|
|
+ isPaused: false,
|
|
|
+ currentMessage: '',
|
|
|
+ currentThoughts: [],
|
|
|
+ messageElement: null,
|
|
|
+ thoughtContainer: null,
|
|
|
+ abortController: null,
|
|
|
+ lastUpdateTime: Date.now(),
|
|
|
+ isVisible: false,
|
|
|
+ userMessages: []
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重置流式响应状态
|
|
|
+ state.isStreaming = true;
|
|
|
+ state.isPaused = false;
|
|
|
+ state.currentMessage = '';
|
|
|
+ state.currentThoughts = [];
|
|
|
+ state.lastUpdateTime = Date.now();
|
|
|
+ state.isVisible = true;
|
|
|
+
|
|
|
+ // 保存用户消息到状态
|
|
|
+ if (!state.userMessages) {
|
|
|
+ state.userMessages = [];
|
|
|
+ }
|
|
|
+ state.userMessages.push({
|
|
|
+ content: newContent,
|
|
|
+ timestamp: new Date(),
|
|
|
+ element: newUserMessageElement,
|
|
|
+ messageId: messageId
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新状态映射
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+ window.conversationStates = conversationStates;
|
|
|
+
|
|
|
+ // 更新侧边栏状态
|
|
|
+ if (typeof window.updateConversationItemStatus === 'function') {
|
|
|
+ window.updateConversationItemStatus(conversationId, 'streaming');
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 创建AbortController用于取消请求(必须在fetch之前) =====
|
|
|
+ state.abortController = new AbortController();
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+
|
|
|
+ // 调用后端API编辑消息(流式响应)
|
|
|
+ fetch(`/api/messages/${messageId}/edit`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ content: newContent,
|
|
|
+ conversation_id: window.currentConversationId,
|
|
|
+ agent: localStorage.getItem('current_agent') || 'planning'
|
|
|
+ }),
|
|
|
+ signal: state.abortController.signal
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ if (response.status === 401) {
|
|
|
+ // 恢复UI状态
|
|
|
+ if (userInput && sendButton) {
|
|
|
+ userInput.classList.remove('input-loading');
|
|
|
+ userInput.placeholder = '请输入需要撰写的品类名和要求...';
|
|
|
+ userInput.disabled = false;
|
|
|
+ sendButton.innerHTML = `
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>`;
|
|
|
+ sendButton.classList.remove('stopping');
|
|
|
+ }
|
|
|
+ currentEditMessageElement = null;
|
|
|
+ currentEditMessageId = null;
|
|
|
+ window.isEditingMessage = false;
|
|
|
+ redirectToLogin();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`HTTP error! Status: ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用流式读取器处理SSE响应
|
|
|
+ const reader = response.body.getReader();
|
|
|
+ const decoder = new TextDecoder();
|
|
|
+ let buffer = '';
|
|
|
+
|
|
|
+ // 用于实时渲染的变量(保存到state中,便于切换对话时恢复)
|
|
|
+ let fullMessage = '';
|
|
|
+ let allThoughts = [];
|
|
|
+ let thoughtContainer = null;
|
|
|
+ let thoughtContent = null;
|
|
|
+ let contentContainer = null;
|
|
|
+ let messageDiv = null;
|
|
|
+ // newUserMessageElement 已经在前面创建了
|
|
|
+ let lastRenderTime = 0;
|
|
|
+ const RENDER_INTERVAL = 16;
|
|
|
+
|
|
|
+ // 创建AI回复消息容器
|
|
|
+ function createBotMessageContainer() {
|
|
|
+ if (messageDiv) return messageDiv;
|
|
|
+
|
|
|
+ messageDiv = document.createElement('div');
|
|
|
+ messageDiv.className = 'message bot';
|
|
|
+
|
|
|
+ // 添加对话ID标识,便于后续管理
|
|
|
+ messageDiv.setAttribute('data-conversation-id', conversationId);
|
|
|
+
|
|
|
+ // 创建思考过程容器
|
|
|
+ thoughtContainer = document.createElement('div');
|
|
|
+ thoughtContainer.className = 'thought-container';
|
|
|
+
|
|
|
+ const thoughtToggleBtn = document.createElement('button');
|
|
|
+ thoughtToggleBtn.className = 'thought-toggle-btn';
|
|
|
+ thoughtToggleBtn.innerHTML = '<span class="toggle-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 15L12 9L6 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>';
|
|
|
+ thoughtToggleBtn.title = '收起/展开思考过程';
|
|
|
+ thoughtToggleBtn.onclick = function (e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ thoughtContainer.classList.toggle('collapsed');
|
|
|
+ const icon = thoughtToggleBtn.querySelector('.toggle-icon');
|
|
|
+ if (thoughtContainer.classList.contains('collapsed')) {
|
|
|
+ icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
|
+ } else {
|
|
|
+ icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 15L12 9L6 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
|
+ }
|
|
|
+ };
|
|
|
+ thoughtContainer.appendChild(thoughtToggleBtn);
|
|
|
+
|
|
|
+ thoughtContent = document.createElement('div');
|
|
|
+ thoughtContent.className = 'thought-content';
|
|
|
+ thoughtContainer.appendChild(thoughtContent);
|
|
|
+
|
|
|
+ messageDiv.appendChild(thoughtContainer);
|
|
|
+
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages) {
|
|
|
+ chatMessages.appendChild(messageDiv);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 保存到对话状态(关键!) =====
|
|
|
+ state.messageElement = messageDiv;
|
|
|
+ state.thoughtContainer = thoughtContainer;
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+
|
|
|
+ // 确保消息元素可见
|
|
|
+ messageDiv.style.opacity = '1';
|
|
|
+ messageDiv.style.transform = 'translateY(0)';
|
|
|
+
|
|
|
+ return messageDiv;
|
|
|
+ }
|
|
|
+
|
|
|
+ function readStream() {
|
|
|
+ reader.read().then(({ done, value }) => {
|
|
|
+ if (done) {
|
|
|
+ // 流式响应完成
|
|
|
+ state.isStreaming = false;
|
|
|
+ if (typeof window.updateConversationItemStatus === 'function') {
|
|
|
+ window.updateConversationItemStatus(conversationId, 'stopped');
|
|
|
+ }
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+ updateUIOnComplete();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const chunk = decoder.decode(value, { stream: true });
|
|
|
+ buffer += chunk;
|
|
|
+
|
|
|
+ const lines = buffer.split('\n\n');
|
|
|
+ buffer = lines.pop() || '';
|
|
|
+
|
|
|
+ lines.forEach(line => {
|
|
|
+ if (line.startsWith('data: ')) {
|
|
|
+ try {
|
|
|
+ const eventData = JSON.parse(line.substring(6));
|
|
|
+ handleEditEventData(eventData);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析SSE数据失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 无论是否暂停,都继续读取流数据
|
|
|
+ readStream();
|
|
|
+ }).catch(error => {
|
|
|
+ if (!state.isPaused) {
|
|
|
+ console.error('读取流数据失败:', error);
|
|
|
+ showMessage('编辑消息时发生错误: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+ state.isStreaming = false;
|
|
|
+ if (typeof window.updateConversationItemStatus === 'function') {
|
|
|
+ window.updateConversationItemStatus(conversationId, 'stopped');
|
|
|
+ }
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+ updateUIOnError();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleEditEventData(data) {
|
|
|
+ // 更新最后更新时间
|
|
|
+ state.lastUpdateTime = Date.now();
|
|
|
+
|
|
|
+ if (!data.success) {
|
|
|
+ showMessage('编辑消息失败: ' + (data.error || '未知错误'), 'error');
|
|
|
+ state.isStreaming = false;
|
|
|
+ if (typeof window.updateConversationItemStatus === 'function') {
|
|
|
+ window.updateConversationItemStatus(conversationId, 'stopped');
|
|
|
+ }
|
|
|
+ updateUIOnError();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data.thought) {
|
|
|
+ // AI回复容器已存在或需要创建
|
|
|
+ allThoughts.push(data.thought);
|
|
|
+ // ===== 保存思考过程到状态 =====
|
|
|
+ state.currentThoughts = allThoughts;
|
|
|
+
|
|
|
+ createBotMessageContainer();
|
|
|
+ if (thoughtContent) {
|
|
|
+ const combinedThoughts = allThoughts.join('');
|
|
|
+ if (typeof window.renderThoughts === 'function') {
|
|
|
+ thoughtContent.innerHTML = window.renderThoughts(combinedThoughts);
|
|
|
+ } else {
|
|
|
+ thoughtContent.textContent = combinedThoughts;
|
|
|
+ }
|
|
|
+ // 更新状态
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+ forceScrollToBottom();
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data.chunk) {
|
|
|
+ // AI回复容器已存在或需要创建
|
|
|
+ fullMessage += data.chunk;
|
|
|
+ // ===== 保存消息内容到状态 =====
|
|
|
+ state.currentMessage = fullMessage;
|
|
|
+
|
|
|
+ createBotMessageContainer();
|
|
|
+
|
|
|
+ if (!contentContainer) {
|
|
|
+ contentContainer = document.createElement('div');
|
|
|
+ contentContainer.className = 'message-content typing';
|
|
|
+ messageDiv.appendChild(contentContainer);
|
|
|
+ }
|
|
|
+
|
|
|
+ const now = performance.now();
|
|
|
+ if (now - lastRenderTime >= RENDER_INTERVAL) {
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ if (contentContainer) {
|
|
|
+ if (typeof window.renderMarkdown === 'function') {
|
|
|
+ contentContainer.innerHTML = window.renderMarkdown(fullMessage);
|
|
|
+ if (typeof window.applyCodeHighlighting === 'function') {
|
|
|
+ window.applyCodeHighlighting(contentContainer);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ contentContainer.textContent = fullMessage;
|
|
|
+ }
|
|
|
+ // 更新状态
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+ if (isUserNearBottom()) {
|
|
|
+ forceScrollToBottom();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ lastRenderTime = now;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data.session_id) {
|
|
|
+ window.currentSessionId = data.session_id;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data.done) {
|
|
|
+ if (contentContainer) {
|
|
|
+ contentContainer.classList.remove('typing');
|
|
|
+ if (typeof window.renderMarkdown === 'function') {
|
|
|
+ contentContainer.innerHTML = window.renderMarkdown(fullMessage);
|
|
|
+ if (typeof window.applyCodeHighlighting === 'function') {
|
|
|
+ window.applyCodeHighlighting(contentContainer);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ contentContainer.textContent = fullMessage;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 标记流式响应完成,但保留状态 =====
|
|
|
+ state.isStreaming = false;
|
|
|
+ state.currentMessage = fullMessage;
|
|
|
+ state.currentThoughts = allThoughts;
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+
|
|
|
+ if (typeof window.updateConversationItemStatus === 'function') {
|
|
|
+ window.updateConversationItemStatus(conversationId, 'stopped');
|
|
|
+ }
|
|
|
+
|
|
|
+ updateUIOnComplete();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateUIOnComplete() {
|
|
|
+ // 删除旧的消息元素(被编辑的消息及其后的所有消息)
|
|
|
+ // 使用chat-messages容器来查找并删除所有相关消息
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages && editMessageElement) {
|
|
|
+ // 获取所有消息元素
|
|
|
+ const allMessages = Array.from(chatMessages.querySelectorAll('.message'));
|
|
|
+ let foundEditMessage = false;
|
|
|
+
|
|
|
+ allMessages.forEach(msg => {
|
|
|
+ // 找到被编辑的消息
|
|
|
+ if (msg === editMessageElement) {
|
|
|
+ foundEditMessage = true;
|
|
|
+ // 删除编辑表单
|
|
|
+ const editForm = msg.querySelector('.edit-form');
|
|
|
+ if (editForm) {
|
|
|
+ editForm.remove();
|
|
|
+ }
|
|
|
+ msg.classList.remove('message-editing');
|
|
|
+ // 删除整个消息元素(除了新创建的用户消息和AI回复)
|
|
|
+ if (msg && msg.parentNode && msg !== messageDiv && msg !== newUserMessageElement) {
|
|
|
+ msg.remove();
|
|
|
+ }
|
|
|
+ } else if (foundEditMessage) {
|
|
|
+ // 删除被编辑消息之后的所有消息(除了新创建的用户消息和AI回复)
|
|
|
+ if (msg && msg.parentNode && msg !== messageDiv && msg !== newUserMessageElement) {
|
|
|
+ msg.remove();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果editMessageElement还存在但没有被删除,手动删除它
|
|
|
+ if (editMessageElement && editMessageElement.parentNode) {
|
|
|
+ editMessageElement.remove();
|
|
|
+ }
|
|
|
+
|
|
|
+ currentEditMessageElement = null;
|
|
|
+ currentEditMessageId = null;
|
|
|
+ window.isEditingMessage = false;
|
|
|
+
|
|
|
+ if (userInput && sendButton) {
|
|
|
+ userInput.classList.remove('input-loading');
|
|
|
+ userInput.placeholder = '请输入需要撰写的品类名和要求...';
|
|
|
+ userInput.disabled = false;
|
|
|
+ sendButton.innerHTML = `
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>`;
|
|
|
+ sendButton.classList.remove('stopping');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只刷新侧边栏的对话列表,不重新加载消息(避免旧消息重新出现)
|
|
|
+ if (window.currentConversationId && typeof loadConversationHistory === 'function') {
|
|
|
+ setTimeout(() => {
|
|
|
+ loadConversationHistory(false); // 只更新侧边栏,不重新加载消息
|
|
|
+ }, 300);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 为新创建的消息添加复制按钮
|
|
|
+ if (typeof window.setupCopyButtons === 'function') {
|
|
|
+ window.setupCopyButtons();
|
|
|
+ }
|
|
|
+
|
|
|
+ forceScrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateUIOnError() {
|
|
|
+ if (currentEditMessageElement) {
|
|
|
+ const sendBtn = currentEditMessageElement.querySelector('.edit-send-btn');
|
|
|
+ if (sendBtn) {
|
|
|
+ sendBtn.textContent = '发送';
|
|
|
+ sendBtn.disabled = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (userInput && sendButton) {
|
|
|
+ userInput.classList.remove('input-loading');
|
|
|
+ userInput.placeholder = '请输入需要撰写的品类名和要求...';
|
|
|
+ userInput.disabled = false;
|
|
|
+ sendButton.innerHTML = `
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>`;
|
|
|
+ sendButton.classList.remove('stopping');
|
|
|
+ }
|
|
|
+
|
|
|
+ currentEditMessageElement = null;
|
|
|
+ currentEditMessageId = null;
|
|
|
+ window.isEditingMessage = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function forceScrollToBottom() {
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (chatMessages) {
|
|
|
+ chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function isUserNearBottom() {
|
|
|
+ const chatMessages = document.getElementById('chat-messages');
|
|
|
+ if (!chatMessages) return true;
|
|
|
+ const scrollThreshold = 50;
|
|
|
+ return chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight < scrollThreshold;
|
|
|
+ }
|
|
|
+
|
|
|
+ readStream();
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ // 确保在错误时也更新状态
|
|
|
+ if (error.name !== 'AbortError') {
|
|
|
+ state.isStreaming = false;
|
|
|
+ if (typeof window.updateConversationItemStatus === 'function') {
|
|
|
+ window.updateConversationItemStatus(conversationId, 'stopped');
|
|
|
+ }
|
|
|
+ conversationStates.set(conversationId, state);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.error('编辑消息时发生错误:', error);
|
|
|
+ showMessage('编辑消息时发生错误: ' + error.message, 'error');
|
|
|
+
|
|
|
+ // 恢复UI状态
|
|
|
+ if (currentEditMessageElement) {
|
|
|
+ const sendBtn = currentEditMessageElement.querySelector('.edit-send-btn');
|
|
|
+ if (sendBtn) {
|
|
|
+ sendBtn.textContent = '发送';
|
|
|
+ sendBtn.disabled = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (userInput && sendButton) {
|
|
|
+ userInput.classList.remove('input-loading');
|
|
|
+ userInput.placeholder = '请输入需要撰写的品类名和要求...';
|
|
|
+ userInput.disabled = false;
|
|
|
+ sendButton.innerHTML = `
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>`;
|
|
|
+ sendButton.classList.remove('stopping');
|
|
|
+ }
|
|
|
+
|
|
|
+ currentEditMessageElement = null;
|
|
|
+ currentEditMessageId = null;
|
|
|
+ window.isEditingMessage = false;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+
|
|
|
+</html>
|