PageSourceSearch

https://www.ptglab.com/AbleDialog.html

html ptglab.com collected 2026-09-24 19:22:34 UTC 357,687 bytes, 6,921 lines download raw bytes

1
2
3
4
5
6
7
8
9
10
11
12
13
14<!DOCTYPE html>
15<html lang="zh-CN">
16<head>
17    <meta charset="UTF-8">
18    <meta name="viewport" content="width=device-width, initial-scale=1.0">
19    <title>Proteintech AI</title>
20    <!-- 可能存在失效保留一个 -->
21    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
22    <!-- <link href="/css/font-awesome.min.css" rel="stylesheet"> -->
23    <link rel="icon" sizes="16x16" href="/favicon.ico">
24    <link href="/css/tailwind.min.css" rel="stylesheet">
25    
25<script src="/scripts/markdown-it.js"></script>
25
26    
26<script src="/scripts/jquery-2.2.4.min-jzy.js"></script>
26
27
28    <!-- Google Tag Manager -->
29    
29<script>
vendor: 495 bytes, lines 29-37
29
30        (function (w, d, s, l, i) {
31            w[l] = w[l] || []; w[l].push({
32                'gtm.start':
33                    new Date().getTime(), event: 'gtm.js'
34            }); var f = d.getElementsByTagName(s)[0],
35                j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
36                    'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
37        })(window, document, 'script', 'dataLayer', '
37GTM-P6XSTD9D
vendor: 3 bytes, line 37
37');
37</script>
37
38    <!-- End Google Tag Manager -->
39
40
41    <style type="text/tailwindcss">
42        @layer utilities {
43            .content-auto {
44                content-visibility: auto;
45            }
46
47            .scrollbar-hide::-webkit-scrollbar {
48                display: none;
49            }
50
51            .scrollbar-hide {
52                -ms-overflow-style: none;
53                scrollbar-width: none;
54            }
55
56            .message-appear {
57                animation: messageAppear 0.3s ease-out forwards;
58            }
59
60            .to-secondary {
61                --tw-gradient-to: #818CF8 var(--tw-gradient-to-position);
62            }
63            /* 直接定义渐变背景类 */
64            .bg-primary-to-secondary {
65                background: linear-gradient(to right, #4F46E5, #818CF8);
66            }
67
68            @keyframes messageAppear {
69                from {
70                    opacity: 0;
71                    transform: translateY(10px);
72                }
73
74                to {
75                    opacity: 1;
76                    transform: translateY(0);
77                }
78            }
79
80            .typing-animation span {
81                animation: typing 1.4s infinite both;
82            }
83
84                .typing-animation span:nth-child(2) {
85                    animation-delay: 0.2s;
86                }
87
88                .typing-animation span:nth-child(3) {
89                    animation-delay: 0.4s;
90                }
91
92            @keyframes typing {
93                0% {
94                    opacity: 0.2;
95                }
96
97                20% {
98                    opacity: 1;
99                }
100
101                100% {
102                    opacity: 0.2;
103                }
104            }
105
106            .markdown-preview {
107                padding-bottom: 5px;
108            }
109
110                .markdown-preview h1, .markdown-preview h2, .markdown-preview h3 {
111                    margin-top: 1rem;
112                    margin-bottom: 0.5rem;
113                }
114
115                .markdown-preview ul, .markdown-preview ol {
116                    margin-top: 0.5rem;
117                    margin-bottom: 0.75rem;
118                    padding-left: 1.5rem;
119                }
120
121                .markdown-preview li {
122                    margin-bottom: 0.25rem;
123                }
124
125            .incomplete-placeholder::after {
126                content: '';
127                display: inline-block;
128                width: 0.5em;
129                height: 1em;
130                vertical-align: text-bottom;
131                animation: blink 1s infinite;
132            }
133
134            /* 审核加载样式 */
135            .audit-loading {
136                display: flex;
137                align-items: center;
138                justify-content: center;
139                padding: 20px;
140                background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
141                border: 2px solid #e2e8f0;
142                border-radius: 12px;
143                margin: 10px 0;
144                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
145                animation: audit-glow 2s ease-in-out infinite !important;
146            }
147
148            .audit-loading-content {
149                display: flex;
150                flex-direction: column;
151                align-items: center;
152                gap: 12px;
153            }
154
155            .audit-spinner {
156                width: 32px;
157                height: 32px;
158                border: 3px solid #e2e8f0;
159                border-top: 3px solid #00ae9e;
160                border-radius: 50%;
161                animation: audit-spin 1s linear infinite;
162            }
163
164            .audit-text {
165                color: #4a5568;
166                font-size: 14px;
167                font-weight: 500;
168                text-align: center;
169            }
170
171            .audit-subtitle {
172                color: #718096;
173                font-size: 12px;
174                text-align: center;
175            }
176
177            @keyframes audit-spin {
178                0% {
179                    transform: rotate(0deg);
180                }
181
182                100% {
183                    transform: rotate(360deg);
184                }
185            }
186
187            .audit-loading-fade-out {
188                animation: audit-fade-out 0.5s ease-out forwards;
189            }
190
191            @keyframes audit-fade-out {
192                0% {
193                    opacity: 1;
194                    transform: translateY(0);
195                }
196
197                100% {
198                    opacity: 0;
199                    transform: translateY(-10px);
200                }
201            }
202
203            /* 审核加载动画效果 - 简化版本 */
204            .audit-loading .audit-dots {
205                display: inline-block !important;
206                animation: audit-dots-bounce 1s infinite !important;
207                color: #ff0000 !important;
208                font-weight: bold !important;
209                font-size: 20px !important;
210            }
211
212            .audit-loading .audit-pulse {
213                animation: audit-pulse 1s ease-in-out infinite !important;
214                color: #ff0000 !important;
215                font-size: 16px !important;
216            }
217
218            @keyframes audit-dots-bounce {
219                0%, 33% {
220                    opacity: 0.3;
221                    transform: scale(0.8);
222                }
223
224                50% {
225                    opacity: 1;
226                    transform: scale(1.2);
227                }
228
229                66%, 100% {
230                    opacity: 0.3;
231                    transform: scale(0.8);
232                }
233            }
234
235            @keyframes audit-pulse {
236                0%, 100% {
237                    opacity: 0.7;
238                    transform: scale(1);
239                }
240
241                50% {
242                    opacity: 1;
243                    transform: scale(1.08);
244                }
245            }
246
247            /* 增强审核加载整体效果 - 已合并到上面的 .audit-loading 中 */
248
249            @keyframes audit-glow {
250                0%, 100% {
251                    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
252                    border-color: #e2e8f0;
253                    background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
254                }
255
256                50% {
257                    box-shadow: 0 8px 20px rgba(255, 0, 0, 0.5);
258                    border-color: #ff0000;
259                    background: linear-gradient(135deg, #ffe6e6 0%, #ffcccc 100%);
260                }
261            }
262
263            @keyframes blink {
264                0%, 100% {
265                    opacity: 1;
266                }
267
268                50% {
269                    opacity: 0;
270                }
271            }
272
273            a {
274                color: red;
275            }
276        }
277    </style>
278
279    <style>
280        .fa {
281            font-size: inherit !important; /* 继承父元素字体大小,而非默认的 14px */
282            line-height: inherit !important; /* 继承父元素行高 */
283            font-weight: normal !important; /* 强制普通字重 */
284            font-style: normal !important; /* 强制正常样式(非斜体) */
285        }
286        /* 可选:清除图标大小相关的辅助类(如 fa-lg、fa-2x 等) */
287        .fa-lg, .fa-2x, .fa-3x, .fa-4x, .fa-5x {
288            font-size: inherit !important;
289        }
290
291        :root {
292            --tw-color-primary: #4F46E5;
293            --tw-color-secondary: #818CF8;
294            --tw-color-neutral: #1F2937;
295            --tw-color-neutral-light: #F3F4F6;
296            --tw-gradient-to-secondary: #818CF8;
297        }
298
299        .to-secondary {
300            --tw-gradient-to: var(--tw-gradient-to-secondary) var(--tw-gradient-to-position);
301        }
302
303        .from-primary {
304            background: linear-gradient(to right, var(--tw-gradient-from, #4F46E5), var(--tw-gradient-to-secondary));
305            --tw-gradient-from: #4F46E5 var(--tw-gradient-from-position);
306        }
307
308        .to-secondary {
309            --tw-gradient-to: #818CF8 var(--tw-gradient-to-position);
310        }
311
312        .border-primary {
313            border-color: rgb(79,70,229) !important;
314        }
315
316        .bg-primary\/10 {
317            background-color: rgb(79 70 229 / 0.1);
318        }
319
320        .bg-primary {
321            background-color: #00ae9e !important;
322            /* background-color: var(--tw-color-primary); */
323        }
324
325        .text-secondary {
326            color: var(--tw-color-secondary);
327        }
328
329        @font-face {
330            font-family: "Aller";
331            font-style: normal;
332            font-weight: normal;
333            src: local("Aller"), url("/css/webfont/Aller_Rg.woff") format("woff");
334        }
335
336        @font-face {
337            font-family: "Aller Bold";
338            font-style: normal;
339            font-weight: normal;
340            src: local("Aller Bold"), url("/css/webfont/Aller_Bd.woff") format("woff");
341        }
342
343        .markdown-body {
344            font-family: "Aller" !important;
345        }
346
347        #app {
348            padding-bottom: 10px;
349            /* background:linear-gradient(to bottom, #ffffff, #e0f3ff); */
350        }
351
352        .aifontSize {
353            font-size: 15px;
354        }
355
356        .avatar-container {
357            margin-right: 12px !important;
358        }
359
360        @media (max-width: 991.98px) {
361            #messagesContainer {
362                max-height: calc(100vh - 155px);
363            }
364        }
365
366        #messagesContainer {
367            max-height: calc(100vh - 220px);
368            overflow-y: auto;
369            scrollbar-width: thin;
370            scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
371            /* padding-top: 0px; */
372        }
373
374            #messagesContainer::-webkit-scrollbar {
375                width: 6px;
376            }
377
378            #messagesContainer::-webkit-scrollbar-track {
379                background: transparent;
380            }
381
382            #messagesContainer::-webkit-scrollbar-thumb {
383                background-color: rgba(156, 163, 175, 0.5);
384                border-radius: 3px;
385            }
386
387        .think-content, .markdown-content, .prose {
388            word-break: break-word;
389        }
390
391        .thinkHtml a {
392            color: #00ae9e !important;
393        }
394
395        pre {
396            white-space: pre-wrap;
397        }
398
399        .incomplete-placeholder {
400            display: none;
401            width: 10px;
402            height: 10px;
403            background-color: #f3f4f6;
404            border-radius: 50%;
405            margin-left: 2px;
406            animation: dot-pulse 1.5s infinite;
407        }
408
409        .loading-icon {
410            position: absolute;
411        }
412
413        @keyframes dot-pulse {
414            0%, 100% {
415                opacity: 1;
416            }
417
418            50% {
419                opacity: 0.3;
420            }
421        }
422
423        .send-icon, .cancel-icon {
424            transition: opacity 0.3s ease; /* 添加过渡动画 */
425        }
426
427        .cancel-icon {
428            display: none; /* 默认隐藏取消图标 */
429        }
430
431        .hideThinking {
432            display: none;
433        }
434
435        .showThinkinglogo {
436            white-space: nowrap;
437            display: flex;
438            align-items: center;
439            gap: 6px;
440            font-size: 14px;
441            padding: 8px 0;
442        }
443
444        /* 文字:深主题绿(#008a7d),和圆点亮绿呼应 */
445        .thinking-text {
446            display: flex;
447            gap: 1px;
448        }
449
450            .thinking-text span {
451                font-weight: 700;
452                color: #1f2328; /* 深主题绿,比圆点暗一点,有层次 */
453                text-shadow: 0 0 1px #1f2328; /* 圆点同色描边,强化统一感 */
454                animation: charJump 1.2s infinite ease-in-out;
455            }
456
457                .thinking-text span:nth-child(1) {
458                    animation-delay: 0s;
459                }
460
461                .thinking-text span:nth-child(2) {
462                    animation-delay: 0.1s;
463                }
464
465                .thinking-text span:nth-child(3) {
466                    animation-delay: 0.2s;
467                }
468
469                .thinking-text span:nth-child(4) {
470                    animation-delay: 0.3s;
471                }
472
473                .thinking-text span:nth-child(5) {
474                    animation-delay: 0.4s;
475                }
476
477        /* 圆点:保持亮主题绿(#00ae9e) */
478        .dot {
479            width: 6px;
480            height: 6px;
481            border-radius: 50%;
482            background-color: #1f2328; /* 和文字同色系,明度更高 */
483            animation: dotJump 1.2s infinite ease-in-out;
484        }
485
486            .dot:nth-child(2) {
487                animation-delay: 0.2s;
488            }
489
490            .dot:nth-child(3) {
491                animation-delay: 0.4s;
492            }
493
494            .dot:nth-child(4) {
495                animation-delay: 0.6s;
496            }
497
498        /* 动画不变(仅改配色,动效保留) */
499        @keyframes charJump {
500            0%, 100% {
501                opacity: 0.8;
502                transform: translateY(0);
503            }
504
505            50% {
506                opacity: 1;
507                transform: translateY(-2px);
508            }
509        }
510
511        @keyframes dotJump {
512            0%, 100% {
513                transform: translateY(0);
514                opacity: 0.7;
515            }
516
517            50% {
518                transform: translateY(-3px);
519                opacity: 1;
520            }
521        }
522        /* 动画关键帧:点的缩放+透明度变化 */
523        @keyframes loadingDot {
524            0%, 100% {
525                transform: scale(1);
526                opacity: 0.6;
527            }
528
529            50% {
530                transform: scale(1.4);
531                opacity: 1;
532            }
533        }
534
535        #sendBtn.loading .send-icon {
536            display: none;
537        }
538
539        #sendBtn.loading .cancel-icon {
540            display: block;
541        }
542
543        #sendBtn.loading {
544            background-color: #ef4444 !important; /* 加载时按钮变红,提示可取消 */
545        }
546
547        .send-tooltip, .cancel-tooltip {
548            white-space: nowrap;
549            transform-origin: bottom center;
550        }
551
552        #sendBtn.loading .send-tooltip {
553            display: none;
554        }
555
556        #sendBtn.loading .cancel-tooltip {
557            display: block;
558        }
559
560        /* 发送按钮禁用状态样式 */
561        #sendBtn:disabled {
562            background-color: #9ca3af !important; /* 灰色背景 */
563            color: #6b7280 !important; /* 灰色文字 */
564            cursor: not-allowed !important; /* 禁用光标 */
565            opacity: 0.6 !important; /* 降低透明度 */
566            transform: none !important; /* 禁用缩放效果 */
567            box-shadow: none !important; /* 移除阴影 */
568        }
569
570            #sendBtn:disabled:hover {
571                background-color: #9ca3af !important; /* 悬停时保持灰色 */
572                transform: none !important; /* 禁用悬停缩放 */
573            }
574
575            #sendBtn:disabled .send-icon {
576                color: #6b7280 !important; /* 图标也变灰色 */
577            }
578
579        .slide-in {
580            transform: translateX(0) !important;
581            opacity: 1;
582            transition: transform 0.3s ease, opacity 0.3s ease;
583        }
584
585        .slide-out {
586            transform: translateX(100%) !important;
587            opacity: 0;
588            transition: transform 0.3s ease, opacity 0.3s ease;
589        }
590
591        .right-panel {
592            transform: translateX(100%);
593            opacity: 0;
594        }
595
596        /* 左侧内容过渡效果 */
597        .left-content {
598            transition: width 0.3s ease;
599        }
600
601        .sticky {
602            position: sticky; /* 粘性定位,相对于父元素固定 */
603            top: 0; /* 距离顶部0像素 */
604            z-index: 10; /* 确保在内容上方显示 */
605        }
606        /* 消息项基础样式 */
607        .message-item {
608            transition: all 0.2s ease; /* 平滑过渡效果 */
609        }
610
611            /* 触摸/鼠标悬停状态 */
612            .message-item:hover,
613            .message-item:focus-within {
614                cursor: pointer;
615                border-color: rgb(79,70,229) !important; /* 亮蓝色边框 */
616                box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); /* 轻微发光效果 */
617            }
618
619        .group-header h3 {
620            font-size: 15px;
621            white-space: nowrap; /* 强制文本不换行 */
622            overflow: hidden; /* 隐藏超出容器的内容 */
623            text-overflow: ellipsis; /* 超出部分显示省略号 */
624            max-width: calc(100% - 40px); /* 预留图标的空间(根据实际调整) */
625        }
626
627        @media (max-width: 750px) {
628            #rightPanel {
629                width: 100% !important; /* !important确保覆盖其他样式(必要时使用) */
630            }
631        }
632
633        .think-section {
634            margin: 10px 0px;
635        }
636
637        .markdown-content table {
638            width: fit-content !important;
639        }
640
641        ul {
642            list-style: disc; /* disc 是 ul 的默认原点样式 */
643        }
644
645        /* 确保 ol(有序列表)保留数字 */
646        ol {
647            list-style: decimal; /* decimal 是 ol 的默认数字样式 */
648        }
649
650        /* 针对嵌套列表(如 ul 里的 ul),可单独设置 */
651        ul ul {
652            list-style: circle; /* 嵌套无序列表默认是空心圆,可选 */
653        }
654
655        ol ol {
656            list-style: lower-alpha; /* 嵌套有序列表默认是小写字母,可选 */
657        }
658
659        .able-intro {
660            max-width: 1000px;
661            margin: 0px auto;
662            margin-bottom: 20px;
663            /* padding: 0 20px; */
664            text-align: center;
665            margin-top: 8px;
666        }
667            /* 标题样式,突出显示 */
668            .able-intro h1 {
669                font-size: 28px;
670                font-weight: 600;
671                margin-bottom: 12px;
672                display: flex;
673                align-items: center;
674                justify-content: center;
675            }
676        /* 蓝色标签样式,模拟 “Able beta” 效果 */
677        .able-badge {
678            /* background: linear-gradient(to right, #60a5fa, #3b82f6);  */
679            /* color: #fff; */
680            /* padding: 6px 12px; */
681            /* border-radius: 6px; */
682            /* font-size: 16px; */
683            /* font-weight: 500; */
684            margin: 0 8px;
685            /* box-shadow: 0 2px 4px rgba(0,0,0,0.1); */
686            position: relative;
687        }
688            /* beta 标记样式,小型上标 */
689            .able-badge .beta {
690                font-size: 13px;
691                padding: 2px 4px;
692            }
693        /* 副标题样式,引导性文字 */
694        .able-intro .subtitle {
695            font-size: 16px;
696            color: #6b7280; /* 浅灰色文字,弱化次要信息 */
697            /* margin-bottom: 30px; */
698        }
699        /* 使用说明标题,突出层级 */
700        .able-intro h2 {
701            font-size: 20px;
702            font-weight: 600;
703            margin-bottom: 20px;
704        }
705
706        .able-intro h3 {
707            font-weight: 600;
708            margin-bottom: 20px;
709            text-align: left; /* 左对齐,和示例一致 */
710        }
711        /* 内容分栏容器,用 Flex 实现左右布局 */
712        .intro-content {
713            display: flex;
714            flex-wrap: wrap; /* 小屏幕自动换行 */
715            gap: 40px;
716            justify-content: center; /* 让分栏在小屏幕居中 */
717        }
718        /* 左右分栏通用样式 */
719        .intro-column {
720            flex: 1;
721            min-width: 250px; /* 小屏幕下保证宽度 */
722            text-align: left;
723        }
724
725            .intro-column h3 {
726                font-weight: 600;
727                margin-bottom: 10px;
728            }
729            /* 列表项样式,带图标 */
730            .intro-column ul {
731                list-style: none;
732                padding-left: 0;
733            }
734
735            .intro-column li {
736                margin-bottom: 8px;
737                display: flex;
738                align-items: center;
739            }
740                /* 对勾和叉号图标,用伪元素实现,也可以换成字体图标 */
741                .intro-column li::before {
742                    content: "✓";
743                    color: #22c55e; /* 绿色对勾 */
744                    margin-right: 8px;
745                    font-weight: bold;
746                }
747
748            .intro-column.cant-help li::before {
749                content: "✕"; /* 叉号符号(比"×"更粗实,视觉更清晰) */
750                color: #ef4444; /* 红色叉号,和对勾颜色区分明显 */
751                font-size: 12px;
752                /* 无需重复写 margin/weight,会继承上面的样式,只需改 content 和 color */
753            }
754
755            .intro-column .cant-help li::before {
756                content: "✕";
757                color: #ef4444; /* 红色叉号 */
758            }
759
760        .font-inter {
761            background: linear-gradient(to bottom, #ffffff, #e0f3ff)
762        }
763
764        #userInput {
765            box-sizing: border-box;
766            font-size: 15px;
767            line-height: 24px;
768            min-height: 48px; /* 与 sendBtn h-12 对齐 */
769            max-height: calc(24px * 5 + 24px); /* 最多 5 行 + 上下 padding */
770            padding: 12px;
771            overflow-y: auto;
772            vertical-align: middle;
773        }
774
775        #attachFileBtn {
776            color: #6b7280;
777            width: 40px;
778            height: 40px;
779            border-radius: 9999px;
780            display: flex;
781            align-items: center;
782            justify-content: center;
783            transition: all 0.2s ease;
784            flex-shrink: 0;
785        }
786
787            #attachFileBtn:hover {
788                color: #00ae9e;
789                background-color: #f0fdfa;
790            }
791
792        .file-preview-bar {
793            margin-bottom: 8px;
794            padding: 0 4px;
795        }
796
797        .file-preview-chip {
798            display: inline-flex;
799            align-items: center;
800            gap: 8px;
801            max-width: 100%;
802            padding: 8px 12px;
803            background: #f3f4f6;
804            border: 1px solid #e5e7eb;
805            border-radius: 10px;
806            font-size: 13px;
807            color: #374151;
808        }
809
810            .file-preview-chip img {
811                width: 36px;
812                height: 36px;
813                object-fit: cover;
814                border-radius: 6px;
815                flex-shrink: 0;
816            }
817
818        .file-preview-name {
819            overflow: hidden;
820            text-overflow: ellipsis;
821            white-space: nowrap;
822            max-width: 220px;
823        }
824
825        .file-preview-size {
826            color: #9ca3af;
827            flex-shrink: 0;
828        }
829
830        .file-preview-remove {
831            color: #9ca3af;
832            width: 24px;
833            height: 24px;
834            border-radius: 9999px;
835            display: flex;
836            align-items: center;
837            justify-content: center;
838            flex-shrink: 0;
839            transition: all 0.2s ease;
840        }
841
842            .file-preview-remove:hover {
843                color: #ef4444;
844                background: #fee2e2;
845            }
846
847        #chatInputDropzone.drag-over {
848            border-color: #00ae9e !important;
849            background-color: #f0fdfa;
850            box-shadow: 0 0 0 2px rgba(0, 174, 158, 0.25);
851        }
852
853        .user-attachment {
854            margin-bottom: 8px;
855            padding: 8px 10px;
856            background: rgba(255, 255, 255, 0.7);
857            border-radius: 8px;
858            font-size: 13px;
859            color: #374151;
860            display: inline-flex;
861            align-items: center;
862            gap: 8px;
863            max-width: 100%;
864        }
865
866            .user-attachment img {
867                width: 48px;
868                height: 48px;
869                object-fit: cover;
870                border-radius: 6px;
871            }
872
873        .open-live-chat {
874            cursor: pointer;
875            color: #00ae9e;
876        }
877        /* 3D容器样式 */
878        .pdb-3d-container {
879            background: #f8f9fa;
880            border: 1px solid #e5e7eb;
881            border-radius: 8px;
882            padding: 15px;
883            margin: 10px 0;
884            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
885        }
886
887            .pdb-3d-container .mol-container {
888                border-radius: 6px;
889                overflow: hidden;
890                position: relative;
891            }
892
893            .pdb-3d-container .btn {
894                font-size: 12px;
895                padding: 4px 8px;
896            }
897
898            .pdb-3d-container h6 {
899                color: #374151;
900                font-weight: 600;
901            }
902
903        /* 复制图标样式 */
904        .copy-icon-container {
905            display: flex;
906            justify-content: flex-end;
907            margin-top: 8px;
908            padding-top: 8px;
909            border-top: 1px solid #e5e7eb;
910        }
911
912        .copy-icon {
913            display: inline-flex;
914            align-items: center;
915            justify-content: center;
916            width: 28px;
917            height: 28px;
918            background-color: #f3f4f6;
919            border: 1px solid #d1d5db;
920            border-radius: 6px;
921            cursor: pointer;
922            transition: all 0.2s ease;
923            color: #6b7280;
924            font-size: 12px;
925        }
926
927            .copy-icon:hover {
928                background-color: #e5e7eb;
929                border-color: #9ca3af;
930                color: #374151;
931                transform: translateY(-1px);
932            }
933
934            .copy-icon:active {
935                transform: translateY(0);
936                background-color: #d1d5db;
937            }
938
939            .copy-icon.copied {
940                background-color: #10b981;
941                border-color: #059669;
942                color: white;
943            }
944
945                .copy-icon.copied::after {
946                    content: "✓";
947                    font-weight: bold;
948                }
949
950        /* 导出PDF按钮样式 */
951        .export-pdf-icon {
952            display: inline-flex;
953            align-items: center;
954            justify-content: center;
955            width: 28px;
956            height: 28px;
957            background-color: #f3f4f6;
958            border: 1px solid #d1d5db;
959            border-radius: 6px;
960            cursor: pointer;
961            transition: all 0.2s ease;
962            color: #6b7280;
963            font-size: 12px;
964            margin-left: 8px;
965        }
966
967            .export-pdf-icon:hover {
968                background-color: #e5e7eb;
969                border-color: #9ca3af;
970                color: #374151;
971                transform: translateY(-1px);
972            }
973
974            .export-pdf-icon:active {
975                transform: translateY(0);
976                background-color: #d1d5db;
977            }
978
979            .export-pdf-icon.exporting {
980                background-color: #3b82f6;
981                border-color: #2563eb;
982                color: white;
983            }
984
985                .export-pdf-icon.exporting::after {
986                    content: "⏳";
987                    font-weight: bold;
988                }
989
990            .export-pdf-icon.success {
991                background-color: #10b981;
992                border-color: #059669;
993                color: white;
994            }
995
996                .export-pdf-icon.success::after {
997                    content: "✓";
998                    font-weight: bold;
999                }
1000
1001        /* 重置按钮样式 */
1002        .reset-icon {
1003            display: inline-flex;
1004            align-items: center;
1005            justify-content: center;
1006            width: 28px;
1007            height: 28px;
1008            background-color: #f3f4f6;
1009            border: 1px solid #d1d5db;
1010            border-radius: 6px;
1011            cursor: pointer;
1012            transition: all 0.2s ease;
1013            color: #6b7280;
1014            font-size: 12px;
1015            margin-right: 8px;
1016        }
1017
1018            .reset-icon:hover {
1019                background-color: #e5e7eb;
1020                border-color: #9ca3af;
1021                color: #374151;
1022                transform: translateY(-1px);
1023            }
1024
1025            .reset-icon:active {
1026                transform: translateY(0);
1027                background-color: #d1d5db;
1028            }
1029
1030            .reset-icon.resetting {
1031                background-color: #f59e0b;
1032                border-color: #d97706;
1033                color: white;
1034            }
1035
1036                .reset-icon.resetting::after {
1037                    content: "⏳";
1038                    font-weight: bold;
1039                }
1040
1041            .reset-icon.success {
1042                background-color: #10b981;
1043                border-color: #059669;
1044                color: white;
1045            }
1046
1047                .reset-icon.success::after {
1048                    content: "✓";
1049                    font-weight: bold;
1050                }
1051
1052        a:hover {
1053            cursor: pointer;
1054        }
1055
1056        /* 【新增】end-message-btn 按钮样式 */
1057        .end-message-btn {
1058            display: none;
1059            /* display: inline-block; */
1060            margin: 4px 8px 4px 0;
1061            padding: 8px 16px;
1062            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1063            color: white;
1064            border: none;
1065            border-radius: 20px;
1066            cursor: pointer;
1067            font-size: 14px;
1068            transition: all 0.3s ease;
1069            box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
1070        }
1071
1072            .end-message-btn:hover {
1073                transform: translateY(-2px);
1074                box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
1075            }
1076
1077            .end-message-btn:active {
1078                transform: translateY(0);
1079                box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
1080            }
1081
1082            .end-message-btn.visible {
1083                display: inline-block;
1084            }
1085
1086        .task-content-body p {
1087            margin: 0px;
1088        }
1089        /* 统一task容器样式 */
1090        .unified-task-group {
1091            border: 1px solid #dee2e6;
1092            border-radius: 10px;
1093            padding: 5px;
1094            margin: 10px 0;
1095            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
1096        }
1097
1098            .unified-task-group .think-header {
1099                padding: 8px 12px;
1100                background: white;
1101                border-radius: 6px;
1102                transition: all 0.2s ease;
1103            }
1104
1105                .unified-task-group .think-header:hover {
1106                    background: #f1f3f5;
1107                    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
1108                }
1109
1110            .unified-task-group .think-content-body {
1111                padding: 8px;
1112                padding-top: 0px;
1113            }
1114        /* 隐藏报错的 Mermaid 容器 */
1115        body > div[id^="dmermaid-graph-"] svg[aria-roledescription="error"] {
1116            display: none !important;
1117        }
1118        /* .content-body {
1119            margin-top: 10px;
1120        }
1121
1122        .avatar-container {
1123            margin-right: 12px !important;
1124        } */
1125
1126
1127
1128    </style>
1129
1130
1131</head>
1132<body class="font-inter bg-gradient-to-br  min-h-screen flex flex-col">
1133    <div id="app" class="flex-1 flex flex-col max-w-7xl w-full mx-auto 
1133sm:p-3 lg:p-3" style="padding-top: 0px;">
1134        <!-- 头部 -->
1135        <!-- <header class="mb-6 flex items-center justify-between border-b border-gray-200 pb-4">
1136            <div class="flex items-center space-x-3">
1137                <div class="w-10 h-10 rounded-lg bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-white text-xl font-bold shadow-lg" id="openRight">AI</div>
1138                <h1 class="text-[clamp(1.5rem,3vw,2rem)] font-bold text-neutral titleLanguage">智能对话助手</h1>
1139            </div>
1140            <button id="clearBtn" class="text-gray-500 hover:text-primary transition-colors duration-300 flex items-center space-x-1.5">
1141                <i class="fa fa-refresh"></i>
1142                <span class="hidden sm:inline clearLanguage">清空对话</span>
1143            </button>
1144        </header> -->
1145        <!-- Able 介绍模块容器 -->
1146        <!-- 消息容器 -->
1147        <main id="messagesContainer" class="flex-1 overflow-y-auto space-y-6 p-2 max-h-[calc(100vh-80px)]  min-[992px]:max-h-[calc(100vh-150px)]">
1148            <section class="able-intro">
1149                <!-- 标题行:Hello, I’m Able beta -->
1150                <h2 style="margin-bottom: 0px;">
1151                    Hello, I’m
1152                    <span class="">
1153                        Able
1154                    </span>
1155                </h2>
1156                <!-- 副标题:辅助说明 -->
1157                <p class="subtitle">
1158                    Helping you find the right product in multiple languages.
1159                </p>
1160                <div style="background-color: white;border-radius: 15px;padding:20px">
1161                    <!-- 使用说明标题 -->
1162                    <h2>How to use this search</h2>
1163                    <h3>Able is trained to provide you tailored product recommendations</h3>
1164
1165                    <!-- 内容分栏:能做的 / 不能做的 -->
1166                    <div class="intro-content">
1167                        <div class="intro-column">
1168                            <h3>Able can help you with:</h3>
1169                            <ul>
1170                                <li>Find the right product for your experiment or project.</li>
1171                                <li>Recommend products for research areas and applications. e.g  ‘Able, find markers for subcellular organelles.</li>
1172                                <li>Gain deeper insights into our products through validation data, scientific knowledge, and citations.</li>
1173                                <!-- 可继续添加能做的内容 -->
1174                            </ul>
1175                        </div>
1176                        <div class="intro-column cant-help">
1177                            <h3>Able can’t help you with:</h3>
1178                            <ul>
1179                                <li><span> General troubleshooting (use <span class="open-live-chat">Live Chat</span> instead) </span></li>
1180                                <li><span>Customer service and shipping queries (use <span class="open-live-chat">Live Chat</span> instead)</span></li>
1181                                <!-- 可继续添加不能做的内容 -->
1182                            </ul>
1183                        </div>
1184                    </div>
1185
1186                    <div style="text-align: left;word-break: break-all;">
1187                        <!-- <div style="font-size: 15px;font-weight: bold;">Disclaimer:</div> -->
1188                        <div class="text-xs text-gray-500 mt-1.5 pl-1 flex items-center">
1189                            <!-- <i class="fa fa-info-circle mr-1"></i> -->
1190                            <span class="inputClearLanguage"><span style="font-weight: bold;">Disclaimer</span> Able AI is experimental and still learning how to provide the best assistance. It may occasionally generate incorrect or incomplete responses. Please do not rely solely on its recommendations when making purchasing decisions or designing experiments.</span>
1191                        </div>
1192                    </div>
1193
1194                    <!-- <div class="intro-contet">
1195                        <h3 style="margin-bottom: 5px;">Disclaimer: </h3>
1196                        <div class="subtitle" style="text-align: left;font-size:14px;">Able AI is experimental and still learning how to provide the best assistance. It may occasionally generate incorrect or incomplete responses. Please do not rely solely on its recommendations when making purchasing decisions or designing experiments.</div>
1197                    </div> -->
1198                </div>
1199
1200            </section>
1201            <!-- 初始欢迎消息 -->
1202            <!-- <div class="message bot-message message-appear">
1203                <div class="flex items-start space-x-3">
1204                    <div class="w-8 h-8 rounded-full overflow-hidden shadow-md flex-shrink-0">
1205                        <img src="/img/ptg.jpg" alt="AI助手头像" class="w-full h-full object-cover">
1206                    </div>
1207                    <div class="bg-white rounded-lg shadow-md p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-l-4 border-primary">
1208                        <div class="prose max-w-none markdown-preview welcomeLanguage">
1209                            <p>
1209你好!我是你的AI助手,有什么可以帮助你的吗?</p>
1210                        </div>
1211                    </div>
1212                </div>
1213            </div> -->
1214        </main>
1215
1216        <!-- 输入区域 -->
1217
1218        <div class="relative" style="position: sticky;bottom:0px" id="chatInputWrapper">
1219            <div id="filePreviewBar" class="file-preview-bar" style="display:none;">
1220                <div class="file-preview-chip">
1221                    <i class="fa fa-file-o file-preview-icon"></i>
1222                    <img class="file-preview-thumb hidden" alt="preview">
1223                    <span class="file-preview-name"></span>
1224                    <span class="file-preview-size"></span>
1225                    <span class="file-preview-status" style="color:#00ae9e;font-size:12px;"></span>
1226                    <button type="button" id="removeFileBtn" class="file-preview-remove" title="Remove file">
1227                        <i class="fa fa-times"></i>
1228                    </button>
1229                </div>
1230            </div>
1231            <div id="chatInputDropzone" class="bg-white rounded-xl shadow-lg p-2 flex items-center space-x-2 border border-gray-200 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/20 transition-all duration-300">
1232                <input type="file" id="fileInput" class="hidden" accept="image/*,.pdf,.doc,.docx,.txt,.csv,.xls,.xlsx,.ppt,.pptx">
1233                <button type="button" id="attachFileBtn" title="Attach file">
1234                    <i class="fa fa-paperclip text-lg"></i>
1235                </button>
1236                <textarea id="userInput" rows="1" autocomplete="off" placeholder="Ask Able to find you the perfect product" class="flex-1 bg-transparent outline-none rounded-lg text-neutral resize-none focus:ring-0"></textarea>
1237                <button id="sendBtn" class="bg-primary hover:bg-primary/90 text-white w-12 h-12 rounded-full flex items-center justify-center transition-all duration-300 transform hover:scale-105 active:scale-95 shadow-md">
1238                    <!-- 使用更细长的箭头图标 -->
1239                    <i class="fa fa-long-arrow-up text-lg send-icon" style="font-size: 20px;"></i>
1240                    <i class="fa fa-ban text-xl cancel-icon hidden"></i>
1241                    <!-- 发送状态提示 -->
1242                </button>
1243            </div>
1244
1245            <div class="text-xs text-gray-500 mt-1.5 pl-1 flex items-center">
1246                <i class="fa fa-info-circle mr-1"></i>
1247                <span class="inputClearLanguage">By submitting a query and entering into conversation with the Able AI Tool, you are agreeing to the terms of the Proteintech <a href="https://www.ptglab.com/news/blog/privacy-policy/" style="color:#00ae9e;" target="_blank">privacy policy</a>.</span>
1248            </div>
1249        </div>
1250
1251
1252    </div>
1253
1254    <!-- 右侧弹出层(关闭按钮固定在顶部) -->
1255    <div id="rightPanel" class="fixed top-0 right-0 w-1/4 h-full bg-white shadow-lg z-10 overflow-hidden right-panel">
1256        <!-- 固定在顶部的头部(包含关闭按钮) -->
1257        <div class="sticky top-0 z-10 bg-white border-b border-gray-200">
1258            <div class="p-4 flex justify-between items-center">
1259                <h2 class="text-xl font-bold canText">参考内容</h2>
1260                <button id="closeBtn" class="text-gray-500 hover:text-gray-700 transition-colors">
1261                    <i class="fa fa-times text-xl"></i>
1262                </button>
1263            </div>
1264        </div>
1265
1266        <!-- 可滚动的内容区域 -->
1267        <div class="p-4 overflow-y-auto h-[calc(100%-64px)]" id="scrollableContent" style="height: 95%;">
1268            <!-- 减去头部高度 -->
1269            <!-- 右侧内容示例 -->
1270            <div class="mb-4">
1271                <h3 class="font-bold"></h3>
1272                <ul class="mt-2 space-y-2">
1273                </ul>
1274            </div>
1275        </div>
1276    </div>
1277
1278    
1278<script type="text/javascript" src="https://ptglab.my.site.com/ESWLiveChatEU1694620743265/assets/js/bootstrap.min.js"></script>
1278
1279
1280    <!-- html2canvas库用于图片导出功能 -->
1281    
1281<script src="/scripts/html2canvas.min.js"></script>
1281
1282    <!-- jsPDF库用于PDF导出功能 -->
1283    
1283<script src="/scripts/jspdf.umd.min.js"></script>
1283
1284    <!-- html2pdf库用于HTML到PDF转换 -->
1285    
1285<script src="/scripts/html2pdf.bundle.min.js"></script>
1285
1286    
1286<script src="/scripts/3d.js"></script>
1286
1287
1288    
1288<script>
1289        const mdParser = window.markdownit({
1290            html: true,
1291            linkify: true,
1292            typographer: false,
1293            breaks: true
1294        });
1295        // 全局变量
1296        let isLoading = false;
1297        let abortController = null;
1298        const enableLogging = true;
1299        // 【关键修改1】统一缓冲区结构:存储每个消息的think/content/isLoading状态(避免全局变量冲突)
1300        const responseBuffers = {}; // 格式: { messageId: { think: '', content: '', isLoading: true } }
1301        const responseCompleted = {};
1302        const auditLoadingStates = {}; // 存储每个消息的审核加载状态
1303        let language = "en-US";
1304        const messageGroups = new Map();
1305        const collapseStates = new Map();
1306        const processedPdbLinks = new Map();
1307
1308        let ReviewID = "";
1309        const userLanguage = ""
1310
1311        let isShowReview = ""
1312        // 【新增】移动端检测函数
1313        function isMobileDevice() {
1314            // 精准匹配手机设备的UA正则(排除平板/PC等)
1315            // 扩展UA正则:新增微信/QQ浏览器、安卓/苹果移动端标识、国产浏览器关键词
1316            const mobileUA = /Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|Windows Phone|Mobile|MicroMessenger|QQBrowser|MiuiBrowser|UCWEB/i.test(navigator.userAgent);
1317            // 检测iPad/平板(可选:如需包含平板,删除这行判断)
1318            const isTablet = /iPad|Macintosh/.test(navigator.userAgent) && 'ontouchend' in document;
1319            const result = mobileUA && !isTablet;
1320            return result;
1321        }
1322        // 【新增】页面可见性状态和连接恢复
1323        let isPageVisible = true;
1324        // let connectionLostWhileHidden = false; // 标记是否在隐藏时连接断开
1325        const isMobile = isMobileDevice(); // 检测是否是移动端
1326
1327        // 【新增】监听页面可见性变化
1328        document.addEventListener('visibilitychange', () => {
1329            isPageVisible = !document.hidden;
1330            // 当页面恢复可见时,检查是否有正在进行的请求
1331            if (isPageVisible && isMobile && uuid) {
1332                // 显示恢复加载动画
1333                showConversationRestoreLoading();
1334
1335                // 延迟一下,确保页面完全恢复
1336                setTimeout(() => {
1337                    if (uuid) {
1338                        loadConversationHistory(uuid);
1339                    }
1340                }, 500);
1341            }
1342        });
1343        // 存储rebuild-chat接口返回的round_id集合
1344        const rebuildChatRoundIds = new Set();
1345        let resultList = [];
1346        let userChat = [];
1347        let pendingFileAttachment = null;
1348        let pendingFilePreviewUrl = null;
1349        let pendingUploadedFileInfo = null;
1350        let pendingFileUploading = false;
1351        let pendingFileUploadError = null;
1352        const UPLOAD_API_BASE = 'https://aipublic.ptglab.com';
1353        const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
1354        const ACCEPTED_FILE_EXTENSIONS = ['.pdf', '.doc', '.docx', '.txt', '.csv', '.xls', '.xlsx', '.ppt', '.pptx'];
1355
1356        function isImageFile(file) {
1357            if (!file) return false;
1358            if (file.type && file.type.startsWith('image/')) return true;
1359            const lowerName = file.name.toLowerCase();
1360            return IMAGE_EXTENSIONS.some(ext => lowerName.endsWith(ext));
1361        }
1362
1363        function isAcceptedAttachmentFile(file) {
1364            if (!file) return false;
1365            if (isImageFile(file)) return true;
1366            const lowerName = (file.name || '').toLowerCase();
1367            return ACCEPTED_FILE_EXTENSIONS.some(ext => lowerName.endsWith(ext));
1368        }
1369
1370        function normalizePastedFile(file) {
1371            if (!file) return file;
1372            if (file.name) return file;
1373            if (file.type && file.type.startsWith('image/')) {
1374                const ext = (file.type.split('/')[1] || 'png').replace('jpeg', 'jpg');
1375                return new File([file], `pasted-image.${ext}`, { type: file.type });
1376            }
1377            return new File([file], 'pasted-file', { type: file.type || 'application/octet-stream' });
1378        }
1379
1380        function getFileFromDataTransfer(dataTransfer) {
1381            if (!dataTransfer || !dataTransfer.files || !dataTransfer.files.length) return null;
1382            return dataTransfer.files[0];
1383        }
1384
1385        function getFileFromClipboardEvent(e) {
1386            const clipboardData = e.originalEvent ? e.originalEvent.clipboardData : e.clipboardData;
1387            if (!clipboardData) return null;
1388            const items = clipboardData.items;
1389            if (items) {
1390                for (let i = 0; i < items.length; i++) {
1391                    const item = items[i];
1392                    if (item.kind === 'file') {
1393                        const file = item.getAsFile();
1394                        if (file) return file;
1395                    }
1396                }
1397            }
1398            if (clipboardData.files && clipboardData.files.length > 0) {
1399                return clipboardData.files[0];
1400            }
1401            return null;
1402        }
1403
1404        function attemptAttachFile(file, isPaste) {
1405            if (isLoading) return;
1406            if (!file) return;
1407            const normalizedFile = isPaste ? normalizePastedFile(file) : file;
1408            if (!isAcceptedAttachmentFile(normalizedFile)) {
1409                showError(language === 'Chinese'
1410                    ? '不支持的文件类型,请上传图片或 PDF/Office 文档。'
1411                    : 'Unsupported file type. Please upload an image or PDF/Office document.');
1412                return;
1413            }
1414            handleFileSelect(normalizedFile);
1415        }
1416
1417
1418        function formatFileSize(bytes) {
1419            if (bytes < 1024) return `${bytes} B`;
1420            if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1421            return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1422        }
1423
1424        function clearPendingFile(revokePreview = true) {
1425            if (revokePreview && pendingFilePreviewUrl) {
1426                URL.revokeObjectURL(pendingFilePreviewUrl);
1427                pendingFilePreviewUrl = null;
1428            }
1429            pendingFileAttachment = null;
1430            pendingUploadedFileInfo = null;
1431            pendingFileUploading = false;
1432            pendingFileUploadError = null;
1433            $('#fileInput').val('');
1434            $('#filePreviewBar').hide();
1435            $('#filePreviewBar .file-preview-status').text('');
1436        }
1437
1438        async function ensureDialogId() {
1439            if (!localStorage.getItem('chat_uuid') && !uuid) {
1440                const id = await generateSafeNumericId();
1441                uuid = id;
1442                localStorage.setItem('chat_uuid', id);
1443            }
1444            return localStorage.getItem('chat_uuid') || uuid;
1445        }
1446
1447        function updateFilePreview() {
1448            if (!pendingFileAttachment) {
1449                $('#filePreviewBar').hide();
1450                return;
1451            }
1452
1453            const $previewBar = $('#filePreviewBar');
1454            const isImage = isImageFile(pendingFileAttachment);
1455            $previewBar.find('.file-preview-name').text(pendingFileAttachment.name);
1456            $previewBar.find('.file-preview-size').text(formatFileSize(pendingFileAttachment.size));
1457
1458            if (pendingFileUploading) {
1459                $previewBar.find('.file-preview-status').text(language === 'Chinese' ? '上传中...' : 'Uploading...');
1460            } else if (pendingFileUploadError) {
1461                $previewBar.find('.file-preview-status').text(pendingFileUploadError);
1462            } else if (pendingUploadedFileInfo) {
1463                $previewBar.find('.file-preview-status').text(language === 'Chinese' ? '已上传' : 'Uploaded');
1464            } else {
1465                $previewBar.find('.file-preview-status').text('');
1466            }
1467
1468            if (isImage && pendingFilePreviewUrl) {
1469                $previewBar.find('.file-preview-icon').hide();
1470                $previewBar.find('.file-preview-thumb').removeClass('hidden').attr('src', pendingFilePreviewUrl).show();
1471            } else {
1472                $previewBar.find('.file-preview-thumb').addClass('hidden').hide();
1473                $previewBar.find('.file-preview-icon')
1474                    .show()
1475                    .removeClass('fa-file-image-o fa-file-pdf-o fa-file-o')
1476                    .addClass(isImage ? 'fa-file-image-o' : (pendingFileAttachment.name.toLowerCase().endsWith('.pdf') ? 'fa-file-pdf-o' : 'fa-file-o'));
1477            }
1478
1479            $previewBar.show();
1480        }
1481
1482        async function uploadAttachmentFile(file, dialogId) {
1483            const formData = new FormData();
1484            formData.append('file', file);
1485            const response = await fetch(`${UPLOAD_API_BASE}/llm/v2/upload-file/${dialogId}`, {
1486                method: 'POST',
1487                body: formData
1488            });
1489            const result = await response.json();
1490            if (!response.ok || !result.ok || !result.data?.url) {
1491                console.error('upload-file response:', result);
1492                throw new Error(result.msg || `Upload failed: ${response.status}`);
1493            }
1494            const { url, filename, mime_type } = result.data;
1495            const isImage = isImageFile(file);
1496            return {
1497                url,
1498                name: filename || file.name,
1499                mimeType: mime_type || file.type || 'application/octet-stream',
1500                isImage
1501            };
1502        }
1503
1504        async function handleFileSelect(file) {
1505            clearPendingFile();
1506            pendingFileAttachment = file;
1507            if (isImageFile(file)) {
1508                pendingFilePreviewUrl = URL.createObjectURL(file);
1509            }
1510            updateFilePreview();
1511
1512            pendingFileUploading = true;
1513            pendingFileUploadError = null;
1514            updateFilePreview();
1515
1516            try {
1517                const dialogId = await ensureDialogId();
1518                pendingUploadedFileInfo = await uploadAttachmentFile(file, dialogId);
1519            } catch (err) {
1520                console.error('选择文件后上传失败:', err);
1521                pendingFileUploadError = language === 'Chinese' ? '上传失败' : 'Upload failed';
1522                pendingUploadedFileInfo = null;
1523            } finally {
1524                pendingFileUploading = false;
1525                updateFilePreview();
1526            }
1527            userInput.focus();
1528        }
1529
1530        function buildUserInputMultiContent(userText, uploadedFileInfo) {
1531            const multiContent = [];
1532            if (uploadedFileInfo) {
1533                multiContent.push({
1534                    type: 'file_url',
1535                    file: {
1536                        url: uploadedFileInfo.url,
1537                        name: uploadedFileInfo.name,
1538                        mime_type: uploadedFileInfo.mimeType
1539                    }
1540                });
1541            }
1542            if (userText) {
1543                multiContent.push({
1544                    type: 'text',
1545                    text: userText
1546                });
1547            }
1548            return multiContent;
1549        }
1550
1551        function buildFileChatRequestBody(dialogId, userText, uploadedFileInfo, topHref, topTitle,messageId) {
1552            return {
1553                dialog_id: dialogId,
1554                messages: [{
1555                    role: 'user',
1556                    user_input_multi_content: buildUserInputMultiContent(userText, uploadedFileInfo)
1557                }],
1558                round_id:messageId,
1559                agent_name: 'Able',
1560                country: language === 'Chinese' ? 'China' : (isMobile ? 'Mobile United States' : 'United States'),
1561                current_title: topTitle || '',
1562                current_url: topHref || ''
1563            };
1564        }
1565
1566        function buildDownloadFileUrl(dialogId, fileUrl) {
1567            return `${UPLOAD_API_BASE}/llm/v2/download-file/${dialogId}/${fileUrl}`;
1568        }
1569
1570        function isImageByMimeOrName(mimeType, name) {
1571            if (mimeType && mimeType.startsWith('image/')) return true;
1572            if (name) {
1573                const lowerName = name.toLowerCase();
1574                return IMAGE_EXTENSIONS.some(ext =>
1574 lowerName.endsWith(ext));
1575            }
1576            return false;
1577        }
1578
1579        function buildUserHistoryMessageContent(message) {
1580            const dialogId = message.dialog_id || localStorage.getItem('chat_uuid') || uuid || '';
1581            let attachmentHtml = '';
1582            let textContent = message.content || '';
1583
1584            if (message.user_input_multi_content && Array.isArray(message.user_input_multi_content)) {
1585                const textParts = [];
1586                message.user_input_multi_content.forEach(item => {
1587                    if (item.type === 'file_url' && item.file && item.file.url) {
1588                        const fileUrl = item.file.url;
1589                        const fileName = item.file.name || fileUrl;
1590                        const mimeType = item.file.mime_type || '';
1591                        const downloadUrl = buildDownloadFileUrl(dialogId, fileUrl);
1592                        if (isImageByMimeOrName(mimeType, fileName)) {
1593                            attachmentHtml += '<div class="user-attachment"><img src="' + downloadUrl + '" alt="' + fileName + '"><span>' + fileName + '</span></div>';
1594                        } else {
1595                            const iconClass = fileName.toLowerCase().endsWith('.pdf') ? 'fa-file-pdf-o' : 'fa-file-o';
1596                            attachmentHtml += '<div class="user-attachment"><i class="fa ' + iconClass + '"></i><span>' + fileName + '</span></div>';
1597                        }
1598                    } else if (item.type === 'text' && item.text) {
1599                        textParts.push(item.text);
1600                    }
1601                });
1602                if (!textContent.trim() && textParts.length > 0) {
1603                    textContent = textParts.join('\n');
1604                }
1605            }
1606
1607            const textHtml = textContent ? mdParser.render(textContent) : '';
1608            return attachmentHtml + (textHtml ? '<div>' + textHtml + '</div>' : '');
1609        }
1610        // 【关键修改2】废弃全局loadMore,改用缓冲区isLoading控制(避免多消息状态混乱)
1611        // let loadMore = false;
1612        let record_MCP = "";
1613
1614        // DOM元素
1615        const messagesContainer = $('#messagesContainer');
1616        const userInput = $('#userInput');
1617        const sendBtn = $('#sendBtn');
1618        const clearBtn = $('#clearBtn');
1619
1620        // 设置打字效果参数
1621        const baseSpeed = 1;
1622        const speedMultiplier = 6;
1623        const typingSpeed = Math.max(1, Math.floor(baseSpeed / speedMultiplier));
1624        const sessionId = Math.floor(Math.random() * 1000);
1625
1626        if ($('#global-tooltip').length === 0) {
1627            $('body').append(`
1628                                                                                        <div id="global-tooltip" class="fixed z-50 pointer-events-none">
1629                                                                                            <div style="position:absolute;width:max-content;" class="bg-gray-800 text-white text-xs py-1 px-2 rounded shadow-lg opacity-0 transition-opacity duration-200 max-w-xs whitespace-normal"></div>
1630                                                                                        </div>
1631                                                                                    `);
1632        }
1633
1634        const $tooltip = $('#global-tooltip > div');
1635        function showTooltip(element) {
1636            const $element = $(element);
1637            const content = $element.data('full-content');
1638            if (!content) return;
1639            const offset = $element.offset();
1640            const height = $element.outerHeight();
1641            $tooltip.text(content)
1642                .css({
1643                    top: `${offset.top + height + 5}px`,
1644                    left: `${offset.left}px`
1645                })
1646                .addClass('opacity-100');
1647        }
1648        function hideTooltip() {
1649            $tooltip.removeClass('opacity-100');
1650        }
1651
1652        const getUserLanguage = () => {
1653            const primaryLanguage = navigator.language || navigator.userLanguage;
1654            const allLanguages = navigator.languages || [primaryLanguage];
1655            return { primary: "en-US", all: allLanguages };
1656        };
1657        const languageInfo = getUserLanguage();
1658        // 拿到唯一ID(保持递增版)
1659        async function generateSafeNumericId() {
1660            try {
1661                // 核心修改:fetch 添加 POST 请求配置
1662                const timestampdA = Date.now().toString();
1663                const res = await fetch(`https://aipublic.ptglab.com/llm/v2/snowflake-id/${timestampdA}`, {
1664                    method: 'POST', // 改为 POST 请求
1665                    headers: {
1666                        // 常规 JSON 接口的请求头(根据实际接口要求调整)
1667                        'Content-Type': 'application/json',
1668                        // 如果接口需要其他请求头(如 token、appId 等),可在此添加
1669                        // 'Authorization': 'Bearer xxx',
1670                        // 'App-Id': 'xxx'
1671                    },
1672                    // 若接口要求传请求体,可在这里补充(无参数则传空对象)
1673                    body: JSON.stringify({})
1674                });
1675
1676                if (!res.ok) {
1677                    throw new Error(`接口报错:${res.status} ${res.statusText}`);
1678                }
1679                const data = await res.json();
1680                const snowflakeId = data.snowflakeId || data;
1681                const finalId = snowflakeId.data;
1681 // 假设接口返回的 snowflakeId 本身是递增的(snowflake 算法特性)
1682                console.log('通过接口获取递增 snowflake-id:', finalId);
1683                return finalId;
1684
1685            } catch (error) {
1686                console.error('生成唯一ID失败,使用保底递增方案:', error);
1687
1688                const timestamp = Date.now().toString();
1689                // 核心:用函数属性持久化计数器(函数也是对象,可存储属性,多次调用不重置)
1690                if (!generateSafeNumericId.counter) {
1691                    // 初始化:随机 0~9999 之间的数(避免每次启动都从 0 开始,减少重复风险)
1692                    generateSafeNumericId.counter = Math.floor(Math.random() * 10000);
1693                }
1694                // 计数器自增 + 取模(确保始终是 5 位,避免溢出)
1695                generateSafeNumericId.counter = (generateSafeNumericId.counter + 1) % 100000; // 0~99999
1696                // 补全 5 位(不足前面补 0)
1697                const counter = generateSafeNumericId.counter.toString().padStart(5, '0');
1698
1699                const fallbackId = timestamp + counter;
1700                reportErrorToApi(
1701                    fallbackId,
1702                    `0`,
1703                    `${error},msg:snowflake-id error`,
1704                );
1705                console.log('保底递增方案生成ID:', fallbackId);
1706                return fallbackId;
1707            }
1708        }
1709
1710
1711        //判断是否嵌套页面
1712        function isInIframe() {
1713            try {
1714                return window.self !== window.top;
1715            } catch (e) {
1716                return true;
1717            }
1718        }
1719        //判断是否是这个网址
1720        function isCurrentPageTargetUrlFlexible() {
1721            try {
1722                const targetBaseUrls = [
1723                    'http://www.ptglab.com/Able',
1724                    'http://www.ptglab.com/Able/',
1725                    'https://www.ptglab.com/Able',
1726                    'https://www.ptglab.com/Able/',
1727                    'http://www.ptglab.com/able',
1728                    'http://www.ptglab.com/able/',
1729                    'https://www.ptglab.com/able',
1730                    'https://www.ptglab.com/able/'
1731                ];
1732
1733                // 关键修改:获取顶层页面(浏览器地址栏)的URL,而非iframe的URL
1734                // window.top 指向浏览器的顶层window对象,其location是地址栏的URL
1735                const currentUrl = window.top.location.href;
1736                //console.log('顶层页面URL:', currentUrl);
1737
1738                // 保留原有的匹配逻辑
1739                const isMatch = targetBaseUrls.includes(currentUrl);
1740                return isMatch;
1741            } catch (error) {
1742                // 补充跨域异常说明:iframe跨域访问top.location会触发安全错误
1743                console.warn('判断目标页面时发生异常(大概率是跨域/特殊环境):', error);
1744                return false; // 异常时返回 false,不影响后续代码执行
1745            }
1746        }
1747
1748        let uuid; // 使用 let 而不是 const,因为清空对话时需要重新生成
1749        const storedUuid = localStorage.getItem('chat_uuid');
1750        if (storedUuid) {
1751            uuid = storedUuid;
1752        } else {
1753            //generateSafeNumericId()
1754            //    .then(id => {
1755            //        uuid = id
1756            //        localStorage.setItem('chat_uuid', uuid);
1757            //        // 后续逻辑
1758            //    })
1759        }
1760
1761        // 如果不在当前窗口就
1762        if (!isInIframe() || isCurrentPageTargetUrlFlexible()) {
1763            generateSafeNumericId()
1764                .then(id => {
1765                    uuid = id
1766                    localStorage.setItem('chat_uuid', uuid);
1767                    // 后续逻辑
1768                })
1769            localStorage.setItem('chat_uuid', uuid);
1770        }
1771
1772        // loadConversationHistory(uuid)
1773        function handleLinkClick(event) {
1774            // 这里可以保留你原有的逻辑
1775            const originalUrl = $(this).attr('data-original-href');
1776            try {
1777                const urlObj = new URL(originalUrl);
1778                const pdbName = urlObj.pathname.split('/af-pdbs/')[1] || '';
1779                const epitopeStart = urlObj.searchParams.get('epitopeStart') || '';
1780                const epitopeEnd = urlObj.searchParams.get('epitopeEnd') || '';
1781
1782                const targetUrl = `https://www.ptglab.com/mapping/index.html?pdbName=${encodeURIComponent(pdbName)}&epitopeStart=${epitopeStart}&epitopeEnd=${epitopeEnd}`;
1783
1784                if (targetUrl && pdbName) {
1785                    window.open(targetUrl, '_blank');
1786                } else {
1787                }
1788            } catch (error) {
1789                console.error("跳转失败:", error);
1790            }
1791        }
1792
1793        function isSingleGeneWord(input) {
1794            let s = input.trim();
1795
1796            // 统一替换 特殊横杠 → 标准横杠(支持 26975‑1‑AP)
1797            s = s.replace(/\u2011/g, '-');
1798
1799            // 1. 基础校验:非空 + 仅含合法字符(字母/数字/-/_)
1800            const notEmpty = s !== '';
1801            const validChars = /^[A-Za-z0-9_\-]+$/.test(s);
1802            const basicValid = notEmpty && validChars;
1803
1804            // 2. 排除打招呼词汇(不区分大小写)
1805            const lowerS = s.toLowerCase();
1806            const greetings = ['hi', 'hello'];
1807            const isGreet = greetings.includes(lowerS);
1808
1809            // 最终返回
1810            return basicValid && !isGreet;
1811        }
1812        async function sendMessage() {
1813            isShowReview = ""
1814            const displayValue = $(".able-intro").css("display");
1815            // 判断:如果不是display: none,则设置为display: none
1816            if (displayValue !== "none") {
1817                $(".able-intro").css("display", "none");
1818            }
1819            const userText = userInput.val().trim();
1820
1821            const attachedUploadInfo = pendingUploadedFileInfo;
1822            if ((!userText && !attachedUploadInfo) || isLoading) return;
1823            if (pendingFileUploading) {
1824                showError(language === 'Chinese' ? '文件正在上传,请稍候...' : 'File is uploading, please wait...');
1825                return;
1826            }
1827            if (pendingFileAttachment && !attachedUploadInfo) {
1828                showError(language === 'Chinese' ? '文件上传失败,请重新选择文件。' : 'File upload failed, please select again.');
1829                return;
1830            }
1831            if (isLoading) return;
1832            if (userText === '/clear') {
1833                clearMessages();
1834                userInput.val('');
1835                autoResizeUserInput();
1836                return;
1837            }
1838            const hasMessage = $('#messagesContainer .message').length > 0;
1839            if (isSingleGeneWord(userText) && !hasMessage) {
1840                top.location.href = `https://www.ptglab.com/results?q=${userText}`;
1841                return
1842            }
1843
1844            // 检查是否是重新生成模式
1845            let isRegenerating = window.isRegenerating || false;
1846            let regenerateMessageId = window.regenerateMessageId;
1847            let regenerateDialogId = window.regenerateDialogId;
1848            let regenerateRoundId = window.regenerateRoundId;
1849
1850            // 清除重新生成标记
1851            window.isRegenerating = false;
1852            window.regenerateMessageId = null;
1853            window.regenerateDialogId = null;
1854            window.regenerateRoundId = null;
1855            let messageId;
1856            if (!isRegenerating) {
1857                // 开始新对话时,移除所有历史对话的重置图标
1858                removeAllResetIcons();
1859
1860                const savedPreviewUrl = pendingFilePreviewUrl;
1861                pendingFilePreviewUrl = null;
1862                const savedUploadInfo = attachedUploadInfo;
1863                addUserMessage(userText, savedUploadInfo ? {
1864                    name: savedUploadInfo.name,
1865                    isImage: savedUploadInfo.isImage,
1866                    previewUrl: savedPreviewUrl
1867                } : null);
1868                userInput.val('');
1869                autoResizeUserInput();
1870                clearPendingFile(false);
1871                messageId = addBotMessage("正在思考你的问题...");
1872                isLoading = true;
1873            } else {
1874                messageId = regenerateMessageId;
1875                isLoading = true;
1876            }
1877
1878            //addUserMessage(userText);
1879            //userInput.val('');
1880            //const messageId = addBotMessage("正在思考你的问题...");
1881            //isLoading = true;
1882            $('#sendBtn').addClass('loading');
1883            $('#sendBtn').off('click').on('click', function () {
1884                // $(".able-intro").hide()
1885                if (isLoading) {
1886                    abortRequest(true);
1887                    isLoading = false;
1888                    $('#sendBtn').removeClass('loading');
1889                    $('#sendBtn').off('click').on('click', sendMessage);
1890                }
1891            });
1892
1893            // 【关键修改3】初始化当前消息的缓冲区(think/content/isLoading)
1894            responseBuffers[messageId] = {
1895                segments: [],
1896                lastSegmentType: '',
1897                isLoading: false,
1898                lastType: "",
1899                toolContentBuffer: '', // 【新增】缓存不完整的工具标签内容
1900                renderOrder: [], // 【新增】记录渲染顺序:{ type: 'group'|'reasoning'|'tool', index: number }
1901                taskSegments: [], // 【新增】任务段列表
1902                nonContentGroups: [], // 【新增】非content组列表
1903                hasReviewed: false, // 【新增】是否已审核
1904                afterTaskDoneNoValidContent: false // 【新增】task_done后是否还没有遇到有效content
1905                , renderedMermaidCache: {}
1906                , pendingMermaidKeys: {}
1907                , pendingFormatGroupIds: {} // 待渲染的组:task 出现 / task_done / 含 markdown 的 content 时标记
1908            };
1909            responseCompleted[messageId] = false;
1910            auditLoadingStates[messageId] = false; // 初始化审核加载状态
1911            record_MCP = "";
1912            userChat.push({ content: userText, round_id: `${messageId}` });
1913            const apiUrl = 'https://aipublic.ptglab.com/llm/v2/product/chat';
1914            // abortRequest();
1915            abortController = new AbortController();
1916            const signal = abortController.signal;
1917            // 构建请求体,根据是否是重新生成模式使用不同的参数
1918            let requestBody;
1919
1920            if (!localStorage.getItem('chat_uuid') && !uuid) {
1921                // 用 await 替代 .then,确保 id 赋值给 uuid 后,再执行后续代码
1922                const id = await generateSafeNumericId();
1923                uuid = id;
1924                // 可选:同时存入 localStorage,避免下次重复生成
1925                localStorage.setItem('chat_uuid', id);
1926            }
1927
1928
1929            let topHref, topTitle;
1930            try {
1931                // 尝试获取顶层窗口的属性(跨域时会抛错)
1932                topHref = window.top.location.href;
1933                topTitle = window.top.document.title;
1934            } catch (e) {
1935                // 跨域报错时,降级为当前页面的属性
1936                topHref = window.self.location.href; // 当前页面URL
1937                topTitle = window.self.document.title; // 当前页面标题
1938                console.warn('跨域无法访问顶层窗口属性,已降级为当前页面值:', e);
1939            }
1940            const dialogId = isRegenerating
1941                ? regenerateDialogId
1942                : (localStorage.getItem('chat_uuid') ? localStorage.getItem('chat_uuid') : uuid);
1943
1944
1945            if (isRegenerating) {
1946                // 重新生成模式:使用指定的dialog_id和round_id,添加regenerate标记
1947                requestBody = JSON.stringify({
1948                    messages: [{ role: "user", content: userText }],
1949                    dialog_id: regenerateDialogId,
1950                    round_id: `${regenerateRoundId}`,
1951                    stream: true,
1952                    lang: languageInfo?.primary,
1953                    use_context: true,
1954                    prompt_template: "",
1955                    timestamp: new Date().getTime(),
1956                    country: isMobile ? "Mobile United States" : "United States",
1957                    regenerate: true,  // 添加重新生成标记
1958                    current_url: topHref,
1959                    current_title: topTitle
1960                });
1961
1962            } else if (attachedUploadInfo) {
1963                // 点击发送:使用选择文件时已上传的 url 调 chat
1964                requestBody = JSON.stringify(buildFileChatRequestBody(dialogId, userText, attachedUploadInfo, topHref, topTitle,messageId));
1965                ReviewID = `${messageId}`;
1966            } else {
1967                // 正常模式:使用新的参数
1968                requestBody = JSON.stringify({
1969                    messages: [{ role: "user", content: userText }],
1970                    dialog_id: localStorage.getItem('chat_uuid') ? localStorage.getItem('chat_uuid') : uuid,
1971                    round_id: `${messageId}`,
1972                    stream: true,
1973                    lang: languageInfo?.primary,
1974                    use_context: true,
1975                    prompt_template: "",
1976                    timestamp: new Date().getTime(),
1977                    language: userLanguage,
1978                    country: isMobile ? "Mobile United States" : "United States",
1979                    current_url: topHref,
1980                    current_title: topTitle
1981                });
1982                ReviewID = `${messageId}`
1983            }
1984            $(".end-message-btn").hide()
1985            fetch(apiUrl, {
1986                method: 'POST',
1987                headers: { 'Content-Type': 'application/json' },
1988                body: requestBody,
1989                signal: signal
1990            })
1991                .then(response => {
1992                    if (!response.ok) {
1993                        throw new Error(`HTTP错误,状态码: ${response.status}`);
1994                    }
1995                    const reader = response.body.getReader();
1996                    const decoder = new TextDecoder();
1997                    record_MCP = "";
1998
1999                    function read() {
2000                        reader.read().then(({ done, value }) => {
2001                            if (done) {
2002                                try {
2003                                    isShowReview = 'Reviewed'
2004                                    const $messageDom = $(`#${messageId}`);
2005                                    updateStatusIndicator($messageDom, "")
2006                                    // 【关键修改4】响应结束:标记加载完成,移除所有加载器
2007                                    const buffer = responseBuffers[messageId];
2008                                    if (buffer) buffer.isLoading = false;
2009                                    $('.loading-indicator, .load-more-indicator').remove();
2010
2011                                    completeResponse(messageId);
2012                                    let $megID = $(`#${messageId}`);
2013                                    // 检测当前消息的.content-body中的.pdb链接并处理3D显示
2014                                    // 只有在没有现有3D容器时才处理PDB链接,避免重复创建
2015                                    if (messageId) {
2016                                        const $message = $(`#${messageId}`);
2017                                        const hasExistingContainers = $message.find('.pdb-3d-container').length > 0 ||
2018                                            $message.data('has-3d-containers') === true;
2019                                        if (!hasExistingContainers
2019) {
2020                                            checkAndProcessPdbLinks(messageId);
2021                                        }
2022                                    } else {
2023                                        console.error('messageId为空,无法检查PDB链接');
2024                                    }
2025                                    // 在.content-body最末尾添加复制图标
2026                                    addCopyIconToContent(messageId);
2027                                    // 为最后一轮对话添加重置图标
2028                                    addResetIconToLastMessage(messageId);
2029
2030                                    // 判断URL是否为图片
2031                                    function isImageUrl(url, extensions) {
2032                                        const lowerUrl = url.toLowerCase();
2033                                        return extensions.some(ext => lowerUrl.endsWith(ext));
2034                                    }
2035
2036                                    // 从URL提取文件名
2037                                    function getFileName(url) {
2038                                        const parts = url.split('/');
2039                                        return parts[parts.length - 1] || '图片';
2040                                    }
2041                                    const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
2042
2043                                    $('a:not(:has(img))').each(function () {
2044                                        const $link = $(this);
2045                                        if ($link.find('img').length > 0) {
2046                                            return
2047                                        }
2048                                        const href = $link.attr('href');
2049
2050                                        // 检查链接是否为图片
2051                                        if (href && isImageUrl(href, imageExtensions)) {
2052
2053                                            // 创建图片元素
2054                                            const $img = $('<img>', {
2055                                                'src': href,
2056                                                'alt': '图片预览: ' + getFileName(href),
2057                                                'data-jq-auto-added': 'true'
2058                                            }).css({
2059                                                'vertical-align': 'middle',
2060                                                'border-radius': '3px'
2061                                            });
2062                                            let imagesToLoad = 1;  // 本次循环只添加1张图片
2063                                            // 添加到链接末尾
2064                                            $link.append($img);
2065
2066
2067                                            $img.on('load error', function () {
2068                                                imagesToLoad--;  // 一张图片加载完成(无论成功失败)
2069
2070                                                // 当所有图片都处理完毕后执行滚动
2071                                                if (imagesToLoad === 0) {
2072                                                    setTimeout(() => {
2073                                                        scrollToBottom(true);
2074                                                    }, 300);
2075                                                }
2076                                            });
2077                                        }
2078                                    });
2079
2080
2081                                    $('.thinkHtml a[href="@ptglab-live-chat"]').each(function () {
2082                                        const $link = $(this);
2083                                        // 移除 href 属性,防止点击跳转
2084                                        $link.removeAttr('href');
2085                                        // 添加 class
2086                                        $link.addClass('open-live-chat');
2087                                        // 确保文本是 Live Chat
2088                                        $link.text('Live Chat');
2089                                    });
2090
2091                                    // 处理纯文本形式的 @ptglab-live-chat
2092                                    $megID.find('.thinkHtml').each(function () {
2093                                        const $thinkHtml = $(this);
2094
2095                                        // 保存3D容器,避免被清空
2096                                        const $pdbContainers = $thinkHtml.find('.pdb-3d-container').detach();
2097
2098                                        let htmlContent = $thinkHtml.html();
2099
2100                                        // 1. 先处理所有包含@ptglab-live-chat的a标签ï¼
2100Œå®Œå…¨ç§»é™¤a标签
2101                                        // 使用更精确的正则,确保捕获整个a标签并替换为span
2102                                        htmlContent = htmlContent.replace(
2103                                            /<a\s+[^>]*?>([^<]*?)@ptglab-live-chat([^<]*?)<\/a>/gi,
2104                                            function (match, prefix, suffix) {
2105                                                // 保留@前后的内容,用span包裹整个结果
2106                                                return `<span class="open-live-chat">${prefix}Live Chat${suffix}</span>`;
2107                                            }
2108                                        );
2109
2110                                        // 2. 处理纯文本形式的@ptglab-live-chat
2111                                        htmlContent = htmlContent.replace(
2112                                            /([^<>\s])?@ptglab-live-chat([^<>\s])?/g,
2113                                            function (match, prefix, suffix) {
2114                                                // 保留可能存在的前后字符,避免破坏上下文
2115                                                const pre = prefix || '';
2116                                                const suf = suffix || '';
2117                                                return `${pre}<span class="open-live-chat">Live Chat</span>${suf}`;
2118                                            }
2119                                        );
2120
2121                                        // 3. 清理可能残留的空a标签
2122                                        htmlContent = htmlContent.replace(/<a\s+[^>]*?><\/a>/gi, '');
2123
2124                                        // 4. 【新增】处理<end_message>标签,转换为可点击按钮
2125                                        // 先处理可能被转义的标签
2126                                        htmlContent = htmlContent.replace(/&lt;end_message&gt;/gi, '<end_message>');
2127                                        htmlContent = htmlContent.replace(/&lt;\/end_message&gt;/gi, '</end_message>');
2128
2129                                        htmlContent = htmlContent.replace(
2130                                            /<end_message>([\s\S]*?)<\/end_message>/gi,
2131                                            function (match, content) {
2132                                                const questionText = content.trim();
2133                                                if (!questionText) return '';
2134                                                return `<button class="end-message-btn" data-question="${questionText.replace(/"/g, '&quot;')}">${questionText}</button>`;
2135                                            }
2136                                        );
2137                                        // 更新内容
2138                                        $thinkHtml.html(htmlContent);
2139                                        // 恢复3D容器
2140                                        if ($pdbContainers.length > 0) {
2141                                            $thinkHtml.append($pdbContainers);
2142                                            // console.log('恢复3D容器在thinkHtml处理中:', $pdbContainers.length, '个');
2143                                        }
2144                                    });
2145
2146                                    // 流式结束:渲染最后一个仍在活跃的组 + reasoning/tool 段
2147                                    const doneBuffer = responseBuffers[messageId];
2148                                    const $segmentsContainer = $messageDom.find('.segments-container');
2149                                    if ($segmentsContainer.length) {
2150                                        reinjectCachedMermaidInContainer(messageId, doneBuffer, $segmentsContainer);
2151                                        formatAllNonContentGroupsAtStreamEnd(messageId, doneBuffer, $segmentsContainer);
2152                                        formatOrphanSegmentGroups(messageId, $segmentsContainer);
2153                                        sweepUnrenderedMermaid(messageId, $segmentsContainer);
2154                                    }
2155
2156                                    $(".end-message-btn").addClass("visible");
2157
2158                                    $(document).off('click', 'a[data-original-href]', handleLinkClick);
2159                                    $(document).on('click', 'a[data-original-href]', handleLinkClick);
2160
2161                                    $('.open-live-chat')
2162                                        .off('click') // 移除所有已绑定的click事件
2163                                        .click(function () {
2164                                            const data = {
2165                                                type: 'fromChild',
2166                                                message: 'msg',
2167                                                status: 'success'
2168                                            };
2169                                            window.parent.postMessage(data, '*');
2170                                        });
2171
2172                                    //最后再绑定一下 点击展开事件
2173                                    $('#messagesContainer')
2174                                        .off('click', '.think-header') // 移除已有的事件处理
2175                                        .on('click', '.think-header', function (e) { // 重新绑定事件
2176                                            e.stopPropagation();
2177                                            const $container = $(this).closest('.segment-group');
2178                                            const $contentBody = $container.find('.think-content-body');
2179                                            const $toggleIcon = $container.find('.think-toggle-icon');
2180                                            $contentBody.slideToggle(300, function () {
2181                                                $toggleIcon.toggleClass('fa-chevron-down fa-chevron-right');
2182                                            });
2183                                        });
2184
2185                                    $(document)
2186                                        .off('click', '.end-message-btn')
2187                                        .on('click', '.end-message-btn', function (e) {
2188                                            e.preventDefault();
2189                                            // e.stopPropagation();
2190                                            // const questionText = $(this).data('question');
2191                                            const questionText = $(this).text();
2192                                            if (questionText) {
2193                                                // 将问题填入输入框
2194                                                $('#userInput').val(questionText);
2195                                                // 可选:自动发送消息
2196                                                $('#sendBtn').click();
2197
2198                                            }
2199                                        });
2200
2201                                    // 在.content-body最末尾添加复制图标
2202                                    addCopyIconToContent(messageId);
2203                                    setTimeout(() => {
2204                                        // let $messagehideThinking = $(`#${messageId}`);
2205                                        // $messagehideThinking.find('.hideThinking').removeClass('hideThinking');
2206                                        scrollToBottom(true);
2207                                    }, 300);
2208                                    return;
2209                                }
2210                                catch (doneError) {
2211                                    console.error('over:', doneError);
2212                                    updateSendButtonState(false); // 启用发送按钮
2213                                    reportErrorToApi(uuid, `${messageId}`, `${doneError.message}, msg:Error in done phase`,)
2214                                } finally { // 新增:确保状态标记
2215                                    responseCompleted[messageId] = true;
2216                                    return;
2217                                }
2218                            }
2219
2220                            const chunk = decoder.decode(value, { stream: true });
2221
2222                            // MCP标签处理(保留原逻辑)
2223                            if (chunk.includes('"type":"tool_response"')) {
2224                                console.log('"type":"tool_response",准备请求接口');
2225                                // fetchMcpHostResult(uuid, messageId);
2226                            }
2227                            if (chunk.includes('/tool_call')) {
2228                                record_MCP += chunk;
2229                                const hasTag1 = record_MCP.includes('</think>') && record_MCP.includes('</MCP_HOST_TASK>')
2230
2231                                if (hasTag1) {
2232                                    record_MCP = "";
2233                                    const $messageContainer = $(`#${messageId}`);
2234                                    if (!$messageContainer.length) return;
2235                                    const $taskTag = $messageContainer.find('mcp_host_task:has(span.incomplete-placeholder)');
2236                                    if ($taskTag.length) {
2237                                        const loaderId = `loader-${messageId}-${$taskTag.index()}`;
2238                                        const $existingLoading = $(`#${loaderId}`);
2239                                        if (!$existingLoading.length) {
2240                                            const $loading = $(`<div id="${loaderId}" class="loading-indicator ml-1 inline-block"></div>`)
2241                                                .css('color', 'dimgray')
2242                                                .html('<i class="fa fa-spinner fa-spin"></i>');
2243                                            $taskTag.after($loading);
2244                                        }
2245                                    }
2246                                }
2247                            }
2248
2249                            // 【关键修改5】处理SSE chunk(分割多data行+容错解析)
2250                            // const processedChunks = processSseChunk(chunk);
2251                            // log('[响应数据块]', processedChunks);
2252                            // updateBotMessage(messageId, processedChunks);
2253
2254                            try {
2255                                const processedChunks = processSseChunk(chunk);
2256                                updateBotMessage(messageId, processedChunks);
2257                            } catch (codeError) {
2258                                updateSendButtonState(false); // 启用发送按钮
2259                                reportErrorToApi(uuid, `${messageId}`, `${codeError},msg:Error processing AI response data`)
2260                                console.error('error:', codeError);
2261                                return; // 不再继续读取
2262                            }
2263
2264                            read();
2265                        }).catch(error => {
2266                            console.error('读取响应时出错:', error);
2267                            isLoading = false;
2268                            $('#sendBtn').removeClass('loading');
2269                            $('#sendBtn').off('click').on('click', sendMessage);
2270                            $('.loading-indicator, .load-more-indicator').remove();
2271                            hideAuditLoading(messageId);
2272                            updateSendButtonState(false); // 启用发送按钮
2273                            // 【新增】移动端网络错误处理
2274                            const isNetworkError = error.message && (error.message.includes('network') || error.message.includes('Failed to fetch'));
2275                            if (error.name === 'AbortError') {
2276                                showError(language === 'Chinese' ? '请求已取消' : 'Request cancelled');
2277                                // reportErrorToApi(uuid, `${messageId}`, `${error},msg:Request cancelled`)
2278                            } else if (isMobile && isNetworkError) {
2279                                // 移动端网络错误:不显示错误,而是恢复对话
2280
2281                                // 标记连接在隐藏时断开
2282                                if (!isPageVisible) {
2283                                    // connectionLostWhileHidden = true;
2284                                } else {
2285                                    // 如果页面仍然可见但连接断开,立即恢复
2286                                    $(`#${messageId}`).remove(); // 移除未完成的消息
2287                                    showConversationRestoreLoading(); // 显示恢复加载
2288
2289                                    setTimeout(() => {
2290                                        if (uuid) {
2291                                            loadConversationHistory(uuid);
2292                                        } else {
2293                                            hideConversationRestoreLoading();
2294                                            showError(language === 'Chinese' ? '抱歉,无法恢复会话。' : 'Sorry, unable to restore conversation.');
2295                                        }
2296                                    }, 500);
2297                                }
2298                                reportErrorToApi(uuid, `${messageId}`, `${error},msg:Mobile network error - attempting recovery`)
2299                            } else {
2300                                showError(language === 'Chinese' ? '抱歉,请求处理过程中出现错误。' : 'Sorry, there was an error during the request processing.');
2301                                reportErrorToApi(uuid, `${messageId}`, `${error},msg:Sorry, there was an error during the request processing.`)
2302                            }
2303                        });
2304                    }
2305                    read();
2306                })
2307                .catch(error => {
2308                    console.error('Fetch请求出错:', error);
2309                    isLoading = false;
2310                    $('#sendBtn').removeClass('loading');
2311                    $('#sendBtn').off('click').on('click', sendMessage);
2312                    hideAuditLoading(messageId);
2313                    updateSendButtonState(false); // 启用发送按钮
2314
2315
2316                    // 【新增】手机端连接中断处理:检测是否为网络错误且是移动设备
2317                    const isMobile = isMobileDevice();
2318                    const isNetworkError = error.name === 'TypeError' || error.message.includes('Failed to fetch') || error.message.includes('NetworkError');
2319
2320                    if (isMobile && isNetworkError && messageId) {
2321                        // 手机端网络中断:尝试恢复会话而不是显示错误
2322                        console.log('手机端检测到网络中断,尝试恢复会话...');
2323                        $(`#${messageId}`).remove(); // 移除未完成的消息
2324                        showConversationRestoreLoading(); // 显示恢复加载提示
2325
2326                        // 延迟后尝试加载历史会话
2327                        setTimeout(() => {
2328                            if (uuid) {
2329                                loadConversationHistory(uuid);
2330                            } else {
2331                                hideConversationRestoreLoading();
2332                                showError(language === 'Chinese' ? '抱歉,无法恢复会话。' : 'Sorry, unable to restore conversation.');
2333                            }
2334                        }, 500);
2335                    } else if (error.name !== 'AbortError') {
2336                        showError(language === 'Chinese' ? '抱歉,无法连接到AI服务。' : 'Sorry, unable to connect to AI service.');
2337                        $(`#${messageId}`).remove()
2338                        reportErrorToApi(uuid, `${messageId}`, `${error},msg:Sorry, unable to connect to AI service.`)
2339                    } else {
2340                        showError(language === 'Chinese' ? '请求已取消' : 'Request cancelled');
2341                       // reportErrorToApi(uuid, `${messageId}`, `${error},msg:Request cancelled`)
2342                    }
2343                });
2344        }
2345
2346        // 【关键修改6】修复SSE chunk处理:分割多data行+单条解析容错
2347        function processSseChunk(chunk) {
2348            // 1. 先按换行符分割(解决多data行混排问题)
2349            const allLines = typeof chunk === 'string' ? chunk.split(/\r?\n/) : [];
2350
2351            return allLines
2352                .map(line => line.trim()) // 清理空白
2353                .filter(line => line.startsWith('data: ')) // 只保留data行
2354                .map(dataLine => {
2355                    try {
2356                        const jsonStr = dataLine.slice(6).trim(); // 移除"data: "
2357                        if (!jsonStr) return null;
2358                        const parsed = JSON.parse(jsonStr);
2359                        // 验证chunk结构(必须有type和content)
2360                        if (!parsed.type || typeof parsed.content !== 'string') {
2361                            console.warn('无效的chunk结构:', parsed);
2362                            return null;
2363                        }
2364                        // 处理转义换行
2365                        // parsed.content = parsed.content.replace(/\\n/g, '\n');
2366                        parsed.content = parsed.content.replace(/\\n/g, '\n').replace(/\\"/g, '"');
2367                        return parsed;
2368                    } catch (e) {
2369                        // 单条解析失败不阻断整体
2370                        console.error('解析单条chunk失败:', e, '内容:', dataLine.slice(6));
2371                        return null;
2372                    }
2373                })
2374                .filter(Boolean); // 过滤无效chunk
2375        }
2376
2377        // 添加用户消息(保留原逻辑)
2378        function addUserMessage(text, attachment) {
2379            let spanText = language === 'Chinese' ? 'ä½ ' : 'U';
2380            let attachmentHtml = '';
2381            if (attachment) {
2382                if (attachment.isImage && attachment.previewUrl) {
2383                    attachmentHtml = '<div class="user-attachment"><img src="' + attachment.previewUrl + '" alt="' + attachment.name + '"><span>' + attachment.name + '</span></div>';
2384                } else {
2385                    const iconClass = attachment.name.toLowerCase().endsWith('.pdf') ? 'fa-file-pdf-o' : 'fa-file-o';
2386                    attachmentHtml = '<div class="user-attachment"><i class="fa ' + iconClass + '"></i><span>' + attachment.name + '</span></div>';
2387                }
2388            }
2389            const messageContent = attachmentHtml + (text ? '<div>
2389' + text + '</div>' : '');
2390
2391            const messageHtml = `
2392                                                                                            <div class="message user-message message-appear">
2393                                                                                                <div class="flex items-start justify-end space-x-3" style="margin-left:45px">
2394                                                                                                    <div class="bg-primary/10 rounded-lg shadow-sm p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-r-4 border-primary aifontSize">
2395                                                                                                        <div class="prose max-w-none markdown-preview">
2396                                                                                                            ${messageContent}
2397                                                                                                        </div>
2398                                                                                                    </div>
2399                                                                                                    <div class="mobileUser w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 text-sm font-bold flex-shrink-0 shadow-sm">${spanText}</div>
2400                                                                                                </div>
2401                                                                                            </div>
2402                                                                                        `;
2403            messagesContainer.append(messageHtml);
2404            scrollToBottom(true);
2405        }
2406
2407        // 添加AI临时消息(保留原逻辑)
2408        function addBotMessage(text) {
2409            const messageId = generateUniqueId();
2410            const messageHtml = `
2411                                                                        <div class="message bot-message message-appear" id="${messageId}">
2412                                                                            <div class="flex items-start">
2413                                                                                <div class="w-8 h-8 avatar-container rounded-full overflow-hidden shadow-md flex-shrink-0">
2414                                                                                    <img src="/img/Able-Profile-Picture.jpg" alt="AI助手头像" class="w-full h-full object-cover">
2415                                                                                </div>
2416                                                                                <div class="mobilePadding bg-white rounded-lg shadow-md p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-l-4 border-primary" style="width:100%;overflow:auto;min-height: 10px;position: relative;padding-bottom: 40px;">
2417                                                                                    <!-- prose容器:替换原有样式为width:100% -->
2418                                                                                    <div class="prose max-w-none markdown-preview" style="width:100%;">
2419                                                                                        <!-- thinkHtml:新增segments-container子元素 -->
2420                                                                                        <div class="thinkHtml markdown-body">
2421                                                                                            <div class="segments-container"></div>
2422                                                                                        </div>
2423                                                                                    </div>
2424                                                                                    <!-- showThinkinglogo:移出prose容器,新增绝对定位样式 -->
2425                                                                                    <div class="showThinkinglogo" style="position: absolute;bottom: 10px;left: 70px;transform: translateX(-50%);">
2426                                                                                        <sp
2426an class="thinking-text">
2427                                                                                           <span>T</span><span>h</span><span>i</span><span>n</span><span>k</span><span>ing</span>
2428                                                                                        </span>
2429                                                                                        <span class="dot"></span>
2430                                                                                        <span class="dot"></span>
2431                                                                                        <span class="dot"></span>
2432                                                                                    </div>
2433                                                                                </div>
2434                                                                            </div>
2435                                                                        </div>
2436                                                                    `;
2437            messagesContainer.append(messageHtml);
2438            scrollToBottom(true);
2439            return messageId;
2440        }
2441
2442
2443        // 执行done分支的所有逻辑
2444        function executeDoneLogic(messageId, isReviewMode = false) {
2445            // 【关键修改4】响应结束:标记加载完成,移除所有加载器
2446            // const buffer = responseBuffers[messageId];
2447            // if (buffer) buffer.isLoading = false;
2448            // $('.loading-indicator, .load-more-indicator').remove();
2449            // completeResponse(messageId);
2450
2451            // 获取当前消息的jQuery对象
2452            let $message = $('#' + messageId);
2453
2454            // 检测当前消息的.content-body中的.pdb链接并处理3D显示
2455            // 只有在没有现有3D容器的情况下才重新生成
2456
2457            $message.find('a:not([target="_blank"])').attr('target', '_blank');
2458            // 图片加载逻辑
2459            function loadImages() {
2460                // 判断URL是否为图片
2461                function isImageUrl(url, extensions) {
2462                    const lowerUrl = url.toLowerCase();
2463                    return extensions.some(ext => lowerUrl.endsWith(ext));
2464                }
2465
2466                // 从URL提取文件名
2467                function getFileName(url) {
2468                    const parts = url.split('/');
2469                    return parts[parts.length - 1] || '图片';
2470                }
2471                const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
2472
2473                $message.find('a:not(:has(img))').each(function () {
2474                    const $link = $(this);
2475                    const href = $link.attr('href');
2476
2477                    // 检查链接是否为图片
2478                    if (href && isImageUrl(href, imageExtensions)) {
2479                        // 创建图片元素
2480                        const $img = $('<img>', {
2481                            'src': href,
2482                            'alt': '图片预览: ' + getFileName(href),
2483                            'data-jq-auto-added': 'true'
2484                        }).css({
2485                            'vertical-align': 'middle',
2486                            'border-radius': '3px'
2487                        });
2488                        let imagesToLoad = 1;  // 本次循环只添加1张图片
2489                        // 添加到链接末尾
2490                        $link.append($img);
2491
2492                        $img.on('load error', function () {
2493                            imagesToLoad--;  // 一张图片加载完成(无论成功失败)
2494
2495                            // 当所有图片都处理完毕后执行滚动
2496                            if (imagesToLoad === 0) {
2497                                setTimeout(() => {
2498                                    scrollToBottom(true);
2499                                }, 300);
2500                            }
2501                        });
2502                    }
2503                });
2504            }
2505
2506            // 如果是review模式,延迟执行图片加载,等待内容完全渲染
2507            if (isReviewMode) {
2508                setTimeout(() => {
2509                    loadImages();
2510                    // 只查找当前消息的.content-body中的.pdb链接
2511                    const $pdbLinks = $message.find('.content-body a[href*=".pdb"]');
2512                    if ($pdbLinks.length > 0) {
2513                        // 为每个PDB链接处理href属性
2514                        $pdbLinks.each(function () {
2515                            const $link = $(this);
2516                            const originalHref = $link.attr('href');
2517
2518                            // 移除原始href属性,将其值保存到data-original-href中
2519                            $link.removeAttr('href');
2520                            $link.attr('data-original-href', originalHref);
2521                        });
2522                    }                    // 只查找当前消息的.content-body中的.pdb链接
2523                    $(document).off('click', 'a[data-original-href]', handleLinkClick);
2524                    $(document).on('click', 'a[data-original-href]', handleLinkClick);
2525                    $message.find('.thinkHtml').each(function () {
2526                        const $thinkHtml = $(this);
2527                        let htmlContent = $thinkHtml.html();
2528                        htmlContent = htmlContent.replace(/&lt;end_message&gt;/gi, '<end_message>');
2529                        htmlContent = htmlContent.replace(/&lt;\/end_message&gt;/gi, '</end_message>');
2530
2531                        htmlContent = htmlContent.replace(
2532                            /<end_message>([\s\S]*?)<\/end_message>/gi,
2533                            function (match, content) {
2534                                const questionText = content.trim();
2535                                if (!questionText) return '';
2536                                return `<button class="end-message-btn" data-question="${questionText.replace(/"/g, '&quot;')}">${questionText}</button>`;
2537                            }
2538                        );
2539                        // 更新内容
2540                        $thinkHtml.html(htmlContent);
2541
2542                    })
2543
2544                }, 500); // 延迟500ms等待内容渲染
2545
2546            } else {
2547                loadImages();
2548            }
2549
2550            $message.find('.thinkHtml a[href="@ptglab-live-chat"]').each(function () {
2551                const $link = $(this);
2552                // 移除 href 属性,防止点击跳转
2553                $link.removeAttr('href');
2554                // 添加 class
2555                $link.addClass('open-live-chat');
2556                // 确保文本是 Live Chat
2557                $link.text('Live Chat');
2558            });
2559
2560            // 处理纯文本形式的 @ptglab-live-chat
2561            $message.find('.thinkHtml').each(function () {
2562                const $thinkHtml = $(this);
2563                let htmlContent = $thinkHtml.html();
2564                // 1. 先处理所有包含@ptglab-live-chat的a标签,完全移除a标签
2565                // 使用更精确的正则,确保捕获整个a标签并替换为span
2566                htmlContent = htmlContent.replace(
2567                    /<a\s+[^>]*?>([^<]*?)@ptglab-live-chat([^<]*?)<\/a>/gi,
2568                    function (match, prefix, suffix) {
2569                        // 保留@前后的内容,用span包裹整个结果
2570                        return `<span class="open-live-chat">${prefix}Live Chat${suffix}</span>`;
2571                    }
2572                );
2573
2574                // 2. 处理纯文本形式的@ptglab-live-chat
2575                htmlContent = htmlContent.replace(
2576                    /([^<>\s])?@ptglab-live-chat([^<>\s])?/g,
2577                    function (match, prefix, suffix) {
2578                        // 保留可能存在的前后字符,避免破坏上下文
2579                        const pre = prefix || '';
2580                        const suf = suffix || '';
2581                        return `${pre}<span class="open-live-chat">Live Chat</span>${suf}`;
2582                    }
2583                );
2584
2585                // 3. 清理可能残留的错误标签(如空a标签)
2586                htmlContent = htmlContent.replace(/<a\s+[^>]*?><\/a>/gi, '');
2587                // 更新内容
2588                $thinkHtml.html(htmlContent);
2589
2590            });
2591
2592
2593
2594            $message.find('.open-live-chat')
2595                .off('click') // 移除所有已绑定的click事件
2596                .click(function () {
2597                    const data = {
2598                        type: 'fromChild',
2599                        message: 'msg',
2600                        status: 'success'
2601                    };
2602                    window.parent.postMessage(data, '*');
2603                });
2604
2605            //最后再绑定一下 点击展开事件
2606            $message
2607                .off('click', '.think-header') // 移除已有的事件处理
2608                .on('click', '.think-header', function (e) { // 重新绑定事件
2609                    e.stopPropagation();
2610                    const $container = $(this).closest('.segment-group');
2611                    const $contentBody = $container.find('.think-content-body');
2612                    const $toggleIcon = $container.find('.think-toggle-icon');
2613                    $contentBody.slideToggle(300, function () {
2614                        $toggleIcon.toggleClass('fa-chevron-down fa-chevron-right');
2615                    });
2616
2617                });
2618
2619            //最后再绑定一下 点击展开事件
2620            $message
2621                .off('click', '.think-header') // 移除已有的事件处理
2622                .on('click', '.think-header', function (e) { // 重新绑定事件
2623                    e.stopPropagation();
2624                    const $container = $(this).closest('.segment-group');
2625                    const $contentBody = $container.find('.think-content-body');
2626                    const $toggleIcon = $container.find('.think-toggle-icon');
2627                    $contentBody.slideToggle(300, function () {
2628                        $toggleIcon.toggleClass('fa-chevron-down fa-chevron-right');
2629                    });
2630                });
2631
2632            $message
2633                .off('click', '.tool-header') // 移除已有的事件处理
2634                .on('click', '.tool-header', function (e) { // 重新绑定事件
2635                    const $header = $(this);
2636                    const $segmentGroup = $header.closest('.segment-group');
2637                    const segmentIndex = $segmentGroup.index();
2638                    const currentMessageId = $segmentGroup.closest('.message').attr('id');
2639
2640                    // 获取对应的segment数据
2641                    const buffer = responseBuffers[currentMessageId];
2642                    if (!buffer || !buffer.segments[segmentIndex]) return;
2643
2644                    // 切换折叠状态
2645                    const segment = buffer.segments[segmentIndex];
2646                    segment.isCollapsed = !segment.isCollapsed;
2647
2648                    // 更新UI
2649                    $header.next('.tool-content-body').toggleClass('hidden', segment.isCollapsed);
2650                    $header.find('.tool-toggle-icon')
2651                        .toggleClass('fa-chevron-down', !segment.isCollapsed)
2652                        .toggleClass('fa-chevron-right', segment.isCollapsed);
2653                });
2654            setTimeout(() => {
2655                scrollToBottom(true);
2656            }, 300);
2657        }
2658
2659
2660        // 【最终优化版】更新AI消息:支持仅content非空独立展示,保留所有原有功能 + 默认展开think-
2660header/think-content-body
2661        function updateBotMessage(messageId, chunks) {
2662            let buffer = responseBuffers[messageId];
2663            if (!buffer) return;
2664
2665            // ========== 关键修复1:提前初始化DOM核心容器(不依赖任何其他chunk类型) ==========
2666            const $message = $(`#${messageId}`);
2667            if ($message.length) {
2668                // 检查是否已存在头像容器,避免重复创建
2669                let $avatarContainer = $message.find('.avatar-container, .w-8.h-8.rounded-full.overflow-hidden');
2670                if (!$avatarContainer.length) {
2671                    // 只在没有头像容器时才创建
2672                    $avatarContainer = $(`
2673                                                    <div class="avatar-container" style="width: 32px; height: 32px; border-radius: 50%; overflow: hidden; flex-shrink-0; margin-right: 12px;">
2674                                                        <img src="/img/Able-Profile-Picture.jpg" alt="AI助手头像" class="w-full h-full object-cover">
2675                                                    </div>
2676                                                `);
2677                }
2678
2679                // 内容容器(确保必存在,默认隐藏Thinking动画)
2680                let $thinkingWrapper = $message.find('.bg-white.rounded-lg.shadow-md.border-l-4.border-primary');
2681                if (!$thinkingWrapper.length) {
2682                    $thinkingWrapper = $(`
2683                                                    <div class="mobilePadding bg-white rounded-lg shadow-md p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-l-4 border-primary" style="width:100%;overflow:auto;min-height: 10px;position: relative;padding-bottom: 40px;">
2684                                                        <div class="prose max-w-none markdown-preview" style="width:100%;">
2685                                                            <div class="thinkHtml markdown-body"></div>
2686                                                        </div>
2687                                                        <div class="showThinkinglogo hidden" style="position: absolute;bottom: 10px;left: 70px;transform: translateX(-50%);">
2688                                                            <span class="thinking-text"><span>T</span><span>h</span><span>i</span><span>n</span><span>k</span><span>i</span><span>n</span><span>g</span></span>
2689                                                            <span class="dot"></span><span class="dot"></span><span class="dot"></span>
2690                                                        </div>
2691                                                    </div>
2692                                                `);
2693                }
2694
2695                // 只在需要时构建或更新DOM结构(头像+内容容器)
2696                if (!$message.find('.avatar-container, .w-8.h-8.rounded-full.overflow-hidden').length || !$thinkingWrapper.length) {
2697                    $message.empty().css({ display: 'flex', 'align-items': 'flex-start' });
2698                    $message.append($avatarContainer).append($thinkingWrapper);
2699                }
2700            }
2701
2702            // ========== 缓存初始化(原有逻辑保留) ==========
2703            if (!Array.isArray(buffer.taskSegments)) buffer.taskSegments = [];
2704            if (!Array.isArray(buffer.nonContentGroups)) {
2705                buffer.nonContentGroups = []; // { id, segments, groupContent, isCollapsed, isActive }
2706            }
2707            if (!Array.isArray(buffer.segments)) buffer.segments = []; // 【新增】初始化segments
2708            if (!Array.isArray(buffer.renderOrder)) buffer.renderOrder = []; // 【新增】初始化renderOrder
2709            if (buffer.lastSegmentType === undefined) buffer.lastSegmentType = '';
2710            if (buffer.toolContentBuffer === undefined) buffer.toolContentBuffer = '';
2711            if (buffer.isLoading === undefined) buffer.isLoading = false;
2712            if (buffer.hasReviewed === undefined) buffer.hasReviewed = false;
2713            if (buffer.afterTaskDoneNoValidContent === undefined) buffer.afterTaskDoneNoValidContent = false;
2714
2715
2716            // ========== 关键修改1:定义退出标志 + 替换forEach为for循环(支持强制中断) ==========
2717            let shouldExit = false; // 标记是否需要退出整个函数
2718            const chunkList = Array.isArray(chunks) ? chunks : [chunks];
2719
2720            // 用for循环替代forEach,支持break中断
2721            for (let i = 0; i < chunkList.length; i++) {
2722                const chunk = chunkList[i];
2723                if (!chunk || shouldExit) break; // 已标记退出则直接中断循环
2724
2725                switch (chunk.type) {
2726                    case 'review':
2727                        buffer.hasReviewed = true;
2728                        if (!auditLoadingStates[messageId]) {
2729                            const $thinkContainer = $(`#${messageId}`).find('.markdown-preview');
2730                            if ($thinkContainer.length > 0) {
2731                                showAuditLoading(messageId);
2732                                //  showContentAuditLoading(messageId);
2733                            }
2734                        }
2735                        updateSendButtonState(true);
2736                        executeDoneLogic(messageId, true);
2737                        isShowReview = "Reviewing"
2738                        break; // 替换return为break,仅终止当前case
2739                    case 'retract':
2740                        isShowReview = "Reviewing"
2741                        updateSendButtonState(false);
2742                        hideAuditLoading(messageId);
2743                        //   hideContentAuditLoading(messageId); // <-- 新增
2744
2745                        if (buffer.hasReviewed) {
2746                            // --- 审核后撤回:只清除内容(content),保留上下文 ---
2747                            // ... (这部分逻辑不变)
2748                            buffer.toolContentBuffer = '';
2749                            buffer.segments.forEach(seg => {
2750                                if (seg.type === 'reasoning') seg.content = '';
2751                            });
2752                            buffer.nonContentGroups.forEach(group => {
2753                                group.groupContent = '';
2754                            });
2755                            buffer.isLoading = false;
2756                            buffer.lastSegmentType = 'retracted_after_review';
2757                        } else {
2758                            // --- 审核前撤回:重置所有缓存和DOM ---
2759
2760                            // 1. 重置所有数据缓存
2761                            buffer.taskSegments = [];
2762                            buffer.nonContentGroups = [];
2763                            buffer.segments = [];
2764                            buffer.renderOrder = []; // 【新增】清除渲染顺序
2765                            buffer.toolContentBuffer = '';
2766                            buffer.lastSegmentType = '';
2767                            buffer.afterTaskDoneNoValidContent = false; // 【新增】重置标记
2768                            buffer.isLoading = false;
2769
2770                            // 2. 【修改】重置DOM结构,但保留状态指示器
2771                            const $message = $(`#${messageId}`);
2772                            if ($message.length) {
2773                                // --- 开始修改 ---
2774
2775                                // 步骤1: 保存已有的状态指示器
2776                                let $statusIndicator = null;
2777                                const $existingIndicator = $message.find('.status-indicator');
2778                                if ($existingIndicator.length > 0) {
2779                                    $statusIndicator = $existingIndicator.detach(); // 使用detach()保留元素以便后续恢复
2780                                }
2781
2782                                // 检查是否已存在头像容器,避免重复创建
2783                                let $avatarContainer = $message.find('.avatar-container, .w-8.h-8.rounded-full.overflow-hidden');
2784                                if (!$avatarContainer.length) {
2785                                    $avatarContainer = $(`
2786                                                                <div class="avatar-container" style="width: 32px; height: 32px; border-radius: 50%; overflow: hidden; flex-shrink: 0; margin-right: 12px;">
2787                                                                    <img src="/img/Able-Profile-Picture.jpg" alt="AI助手头像" class="w-full h-full object-cover">
2788                                                                </div>
2789                                                            `);
2790                                }
2791                                const $newContent = $(`
2792                                                                <div class="mobilePadding bg-white rounded-lg shadow-md p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-l-4 border-primary" style="width:100%;overflow:auto;min-height: 120px;position: relative;padding-bottom: 40px;">
2793                                                                    <div class="prose max-w-none markdown-preview" style="width:100%;"><div class="thinkHtml markdown-body"></div></div>
2794                                                                    <div class="showThinkinglogo" style="position: absolute;bottom: 10px;left: 70px;transform: translateX(-50%);">
2795                                                                        <sp
2795an class="thinking-text"><span>T</span><span>h</span><span>i</span><span>n</span><span>k</span><span>i</span><span>n</span><span>g</span></span>
2796                                                                        <span class="dot"></span><span class="dot"></span><span class="dot"></span>
2797                                                                    </div>
2798                                                                </div>
2799                                                            `);
2800                                $message.empty().css({ display: 'flex', 'align-items': 'flex-start' });
2801
2802                                // 步骤2: 先添加状态指示器(如果存在),再添加其他内容
2803                                if ($statusIndicator) {
2804                                    $message.append($statusIndicator);
2805                                }
2806
2807                                $message.append($avatarContainer).append($newContent);
2808                                $('.load-more-indicator').remove();
2809
2810                                // --- 修改结束 ---
2811                            }
2812                        }
2813
2814                        buffer.hasReviewed = false;
2815                        break; // 替换return为break,仅终止当前case
2816                    case 'success':
2817                        isShowReview = "Reviewed"
2818                        updateSendButtonState(false);
2819                        hideAuditLoading(messageId);
2820                        //  hideContentAuditLoading(messageId); // <-- 新增
2821                        shouldExit = true; // 标记需要退出整个函数
2822                        break; // 中断switch,然后for循环会因shouldExit=true中断
2823                    default:
2824                        // 其他类型的chunk继续处理
2825                        break;
2826                }
2827
2828                // ========== 非switch的chunk处理逻辑(原有逻辑保留) ==========
2829                if (shouldExit) break; // 已标记退出则中断循环
2830
2831                const isContentChunk = chunk.type === 'content';
2832                const isNonContentChunk = !isContentChunk && ['task', 'task_done', 'reasoning_content', 'tool'].includes(chunk.type);
2833
2834                // content出现时【不再折叠】上一个活跃非content组(修改点1)
2835                if (isContentChunk) {
2836                    // 兼容版:替代 findLast,实现“找最后一个isActive为true的group”
2837                    // 1. 先校验数组有效性,避免非数组调用方法报错
2838                    // 2. slice() 复制数组 → reverse() 反转 → find() 找第一个,等价于原数组的最后一个
2839                    const lastActiveGroup = Array.isArray(buffer.nonContentGroups)
2840                        ? buffer.nonContentGroups.slice().reverse().find(group => group.isActive)
2841                        : null;
2842
2843                    if (lastActiveGroup) {
2844                        // 检查该组是否有 task
2845                        const hasTaskInGroup = lastActiveGroup.segments.some(seg => seg.type === 'task');
2846
2847                        // 检查content是否为有效内容(不是空白字符)
2848                        const hasValidContent = chunk.content && chunk.content.trim() !== '';
2849
2850                        // 如果遇到有效content,重置标记
2851                        if (hasValidContent) {
2852                            buffer.afterTaskDoneNoValidContent = false;
2853
2854                            // 如果之前是task_done后的状态,现在遇到有效content,则创建新组
2855                            if (buffer.lastSegmentType === 'task_done' || buffer.lastSegmentType === 'reasoning_content') {
2856                                // 折叠当前活跃组
2857                                if (hasTaskInGroup) {
2858                                    lastActiveGroup.isCollapsed = true;
2859                                }
2860                                // 将该组标记为非活跃,后续会创建新组
2861                                lastActiveGroup.isActive = false;
2862                            }
2863                        } else {
2864                            // 空content,不改变afterTaskDoneNoValidContent状态
2865                            // 如果不在task_done后的阶段,按原逻辑处理
2866                            if (!buffer.afterTaskDoneNoValidContent) {
2867                                if (hasTaskInGroup) {
2868                                    lastActiveGroup.isCollapsed = true;
2869                                }
2870                                lastActiveGroup.isActive = false;
2871                            }
2872                        }
2873                        // 如果是task_done后的空content(afterTaskDoneNoValidContent=true),不做任何处理,继续使用当前活跃组
2874                    }
2875                }
2876
2877                // 处理reasoning_content
2878                if (chunk.type === 'reasoning_content') {
2879                    const $message = $(`#${messageId}`);
2880                    if (!$message.length) continue; // 用continue替代return,仅跳过当前迭代
2881
2882                    // 重建DOM(保留Thinking动画)
2883                    // 检查是否已存在头像容器,避免重复创建
2884                    let $avatarContainer = $message.find('.avatar-container, .w-8.h-8.rounded-full.overflow-hidden');
2885                    if (!$avatarContainer.length) {
2886                        $avatarContainer = $(`
2887                                                        <div class="avatar-container" style="width: 32px; height: 32px; border-radius: 50%; overflow: hidden; flex-shrink: 0; margin-right: 12px;">
2888                                                            <img src="/img/Able-Profile-Picture.jpg" alt="AI助手头像" class="w-full h-full object-cover">
2889                                                        </div>
2890                                                    `);
2891                    }
2892                    const $thinkingWrapper = $(`
2893                                                    <div class="mobilePadding bg-white rounded-lg shadow-md p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-l-4 border-primary" style="width:100%;overflow:auto;min-height: 10px;position: relative;padding-bottom: 40px;">
2894                                                        <div class="prose max-w-none markdown-preview" style="width:100%;"><div class="thinkHtml markdown-body"></div></div>
2895                                                        <div class="showThinkinglogo" style="position: absolute;bottom: 10px;left: 70px;transform: translateX(-50%);">
2896                                                            <span class="thinking-text"><span>T</span><span>h</span><span>i</span><span>n</span><span>k</span><span>i</span><span>n</span><span>g</span></span>
2897                                                            <span class="dot"></span><span class="dot"></span><span class="dot"></span>
2898                                                        </div>
2899                                                    </div>
2900                                                `);
2901                    $message.empty().css({ display: 'flex', 'align-items': 'flex-start' });
2902                    $message.append($avatarContainer).append($thinkingWrapper);
2903
2904                    // 保留3D容器
2905                    const $thinkHtml = $thinkingWrapper.find('.thinkHtml');
2906                    const pdbContainersHtml = $thinkHtml.find('.pdb-3d-container').map(() => this.outerHTML).get();
2907                    $thinkHtml.empty();
2908                    if (pdbContainersHtml.length > 0) $thinkHtml.append(pdbContainersHtml.join(''));
2909
2910                    // 空内容终止
2911                    if (!chunk.content || chunk.content.trim() === '') {
2912                        buffer.lastSegmentType = 'reasoning_content';
2913                        buffer.isLoading = false;
2914                        continue; // 用continue替代return,仅跳过当前迭代
2915                    }
2916
2917                    // 初始化/追加思考内容
2918                    if (buffer.segments.length === 0) {
2919                        buffer.segments[0] = { type: 'reasoning', think: chunk.content, content: '', isCollapsed: false };
2920                        // 记录渲染顺序
2921                        buffer.renderOrder.push({ type: 'reasoning', index: 0 });
2922                    } else {
2923                        const reasoningSeg = buffer.segments.find(seg => seg.type === 'reasoning') || buffer.segments[0];
2924                        reasoningSeg.think += chunk.content;
2925                    }
2926
2927                    buffer.lastSegmentType = 'reasoning_content';
2928                    buffer.isLoading = false;
2929                }
2930                // 处理task
2931                else if (chunk.type === 'task') {
2932                    $(`#${messageId}`).find('.showThinkinglogo').hide();
2933                    const taskParts = chunk.content.split('|').map(part => part.trim());
2934                    const taskContent = taskParts[1] || chunk.content;
2935
2936                    const newTaskSeg = {
2937                        type: 'task',
2938                        originalContent: chunk.content,
2939                        taskContent: taskContent,
2940                        isLoading: true,
2941                        isCompleted: false
2942                    };
2943
2944                    // 创建/获取活跃非content组
2945                    let activeGroup = buffer.nonContentGroups.find(group => group.isActive);
2946                    if (!activeGroup) {
2947                        activeGroup = {
2948                            id: `non-content-group-${Date.now()}-${buffer.nonContentGroups.length}`,
2949                            segments: [],
2950                            groupContent: '',
2951                            isCollapsed: false,
2952                            isActive: true
2953                        };
2954                        buffer.nonContentGroups.push(activeGroup);
2955                        // 记录渲染顺序
2956                        buffer.renderOrder.push({ type: 'group', index: buffer.nonContentGroups.length - 1 });
2957                    }
2958                    activeGroup.segments.push(newTaskSeg);
2959                    buffer.taskSegments.push(newTaskSeg);
2960                    // 任务出现即标记当前组待渲染(不等下一个任务)
2961                    markGroupForFormat(buffer, activeGroup.id, 'task');
2962
2963                    buffer.lastSegmentType = 'task';
2964                    buffer.afterTaskDoneNoValidContent = false; // 重置标记
2965                    buffer.isLoading = false;
2966
2967
2968                    // 如果当前活跃组的任务列表是隐藏的,则展开它
2969                    const $message = $(`#${messageId}`);
2970                    // 找到当前活跃组对应的 unified-task-group
2971                    const $activeTaskGroup = $message.find(`.unified-task-group[data-group-id="${activeGroup.id}"]`);
2972                    if ($activeTaskGroup.length > 0) {
2973                        const $taskContentBody = $activeTaskGroup.find('.think-content-body');
2974                        if ($taskContentBody.css('display') === 'none') {
2975                            $taskContentBody.show();
2976                            // 同时更新切换图标
2977                            const $toggleIcon = $activeTaskGroup.find('.think-toggle-icon');
2978                            $toggleIcon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
2979                            // 更新组的折叠状态
2980                            activeGroup.isCollapsed = false;
2981                        }
2982                    }
2983                }
2984                // 处理task_done
2985                else if (chunk.type === 'task_done') {
2986                    // 更新task状态
2987                    buffer.taskSegments.forEach(seg => {
2988                        if (seg.originalContent === chunk.content) {
2989                            seg.isLoading = false;
2990                            seg.isCompleted = true;
2991                        }
2992                    });
2993                    buffer.nonContentGroups.forEach(group => {
2994                        group.segments.forEach(seg => {
2995                            if (seg.type === 'task' && seg.originalContent === chunk.content) {
2996                                seg.isLoading = false;
2997                                seg.isCompleted = true;
2998                                markGroupForFormat(buffer, group.id, 'task_done');
2999                            }
3000                        });
3001                    });
3002
3003                    buffer.lastSegmentType = 'task_done';
3004                    buffer.afterTaskDoneNoValidContent = true; // 标记task_done后还没有遇到有效content
3005                    buffer.isLoading = false;
3006                }
3007                // ========== 关键修复2:content处理(非空必存储) ==========
3008                else if (chunk.type === 'content') {
3009                    $(`#${messageId}`).find('.showThinkinglogo').hide();
3010                    const hasToolTag = chunk.content.includes('<tool_call>') || chunk.content.includes('</tool_call>');
3011                    const isToolRelated = hasToolTag || buffer.toolContentBuffer;
3012                    const shouldCollapse = buffer.lastSegmentType === 'reasoning_content';
3013
3014                    // 处理工具类型content
3015                    if (isToolRelated) {
3016                        buffer.toolContentBuffer += chunk.content;
3017                        // 循环切割完整工具调用
3018                        while (true) {
3019                            const closeIdx = buffer.toolContentBuffer.indexOf('</tool_call>');
3020                            const openIdx = buffer.toolContentBuffer.lastIndexOf('<tool_call>', closeIdx);
3021                            if (openIdx === -1 || closeIdx === -1) break;
3022
3023                            const toolContent = buffer.toolContentBuffer.slice(openIdx, closeIdx + 13);
3024                            // 修改点2:工具段默认展开(原逻辑:isCollapsed: true → false)
3025                            buffer.segments.push({ type: 'tool', toolContent: toolContent, isCollapsed: false });
3026                            buffer.renderOrder.push({ type: 'tool', index: buffer.segments.length - 1 });
3027                            buffer.toolContentBuffer = buffer.toolContentBuffer.slice(closeIdx + 13);
3028                        }
3029
3030                        buffer.lastSegmentType = buffer.toolContentBuffer ? 'tool_content_pending' : 'tool_content';
3031                        // 【不再折叠】思考段(修改点3)
3032                        if (shouldCollapse) {
3033                            const reasoningSeg = buffer.segments.find(seg => seg.type === 'reasoning');
3034                            if (reasoningSeg) reasoningSeg.isCollapsed = false; // 原逻辑:true → false
3035                        }
3036                        continue; // 用continue替代return,仅跳过当前迭代
3037                    }
3038
3039                    // 处理普通content(非空必存储)
3040                    // 兼容版:替换findLast,保留原判断逻辑,且不修改原数组
3041                    const targetGroup = Array.isArray(buffer.nonContentGroups)
3042                        ? buffer.nonContentGroups.slice().reverse().find(group =>
3043                            // 完全复用原有的判断条件,逻辑不变
3044                            group.isActive || (group.isActive === false && group.segments.length > 0)
3045                        )
3046                        : null; // 非数组时返回null,避免后续逻辑报错
3047
3048                    if (targetGroup) {
3049                        targetGroup.groupContent += chunk.content;
3050                        // 正文含 markdown 代码块时再标记(mermaid 在 groupContent 里)
3051                        if (chunk.content.includes('```')) {
3052                            markGroupForFormat(buffer, targetGroup.id, 'content-markdown');
3053                        }
3054                    } else {
3055                        // 无对应组,主动创建reasoning段存储
3056                        let reasoningSeg = buffer.segments.find(seg => seg.type === 'reasoning');
3057                        if (!reasoningSeg) {
3058                            reasoningSeg = { type: 'reasoning', think: '', 
3058content: '', isCollapsed: false };
3059                            buffer.segments.push(reasoningSeg);
3060                            // 记录渲染顺序
3061                            buffer.renderOrder.push({ type: 'reasoning', index: buffer.segments.length - 1 });
3062                        }
3063                        // 修改点4:普通content时不再折叠思考段(原逻辑:true → false)
3064                        if (shouldCollapse) reasoningSeg.isCollapsed = false;
3065                        reasoningSeg.content += chunk.content;
3066                    }
3067
3068                    buffer.lastSegmentType = 'content';
3069                }
3070            }
3071
3072            // ========== 关键修改2:判断退出标志,若为true则直接终止函数 ==========
3073            if (shouldExit) {
3074                return; // 终止整个updateBotMessage函数,不再执行后续代码
3075            }
3076
3077            // ========== 关键修复3:DOM渲染(content非空必展示) ==========
3078            const $messageDom = $(`#${messageId}`);
3079            const $thinkingWrapperDom = $messageDom.find('.bg-white.rounded-lg.shadow-md.border-l-4.border-primary');
3080            const $thinkContainerDom = $thinkingWrapperDom.find('.thinkHtml');
3081            if (!$messageDom.length || !$thinkingWrapperDom.length || !$thinkContainerDom.length) return;
3082
3083            // 保留3D容器
3084            const pdbContainersHtml = $thinkContainerDom.find('.pdb-3d-container').map(() => this.outerHTML).get();
3085            $thinkContainerDom.empty();
3086            const $combinedContainer = $('<div class="segments-container"></div>');
3087
3088
3089            // ========== 按照 renderOrder 的顺序渲染所有内容 ==========
3090            buffer.renderOrder.forEach(item => {
3091                if (item.type === 'group') {
3092                    // 渲染 nonContentGroup
3093                    const group = buffer.nonContentGroups[item.index];
3094                    if (!group) return;
3095
3096                    // 收集当前组的所有task
3097                    let groupTasksHtml = '';
3098                    let hasTaskInGroup = false;
3099
3100                    group.segments.forEach((taskSeg) => {
3101                        if (taskSeg.type !== 'task') return;
3102                        hasTaskInGroup = true;
3103                        const statusIcon = taskSeg.isLoading
3104                            ? `<i class="fa fa-spinner fa-spin ml-2" style="color: dimgray;"></i>`
3105                            : (taskSeg.isCompleted ? `<i class="fa fa-check ml-2" style="color: #28a745;"></i>` : '');
3106
3107                        groupTasksHtml += `
3108                                                            <div class="task-segment mb-2">
3109                                                                <div class="task-content-body aifontSize" style="display:flex; align-items:center; background-color:#f8f9fa; border-radius:8px; padding:12px 16px; box-shadow:0 1px 3px rgba(0,0,0,0.05);color:#333;">
3110                                                                    <div class="left" style="flex-grow: 1; line-height: 1.5;">
3111                                                                        ${mdParser.render(taskSeg.taskContent || '')}
3112                                                                    </div>
3113                                                                    <div class="right" style="margin-left: 12px;">
3114                                                                        ${statusIcon}
3115                                                                    </div>
3116                                                                </div>
3117                                                            </div>
3118                                                        `;
3119                    });
3120
3121                    // 如果当前组有task,渲染一个独立的折叠框
3122                    if (hasTaskInGroup) {
3123                        const groupCollapsed = group.isCollapsed !== undefined ? group.isCollapsed : false;
3124
3125                        const unifiedTaskHtml = `
3126                                                            <div class="segment-group unified-task-group" data-type="unified-task" data-group-id="${group.id}" data-group-index="${item.index}">
3127                                                                <div class="think-header flex items-center cursor-pointer text-sm text-gray-700">
3128                                                                    <i class="fa ${groupCollapsed ? 'fa-chevron-right' : 'fa-chevron-down'} think-toggle-icon mr-1"></i>
3129                                                                    <span style="font-weight:bold;">${language === 'Chinese' ? '任务列表' : 'Task List'}</span>
3130                                                                </div>
3131                                                                <div class="think-content-body text-gray-600 text-sm aifontSize" style="display: ${groupCollapsed ? 'none' : 'block'};">
3132                                                                    ${groupTasksHtml}
3133                                                                </div>
3134                                                            </div>
3135                                                        `;
3136                        $combinedContainer.append(unifiedTaskHtml);
3137                    }
3138
3139                    // 渲染当前组的 groupContent(如果有)
3140                    const groupContentHtml = group.groupContent && group.groupContent.trim() !== '' ? `
3141                                                        <div class="content-body pl-3 mt-1">
3142                                                            <div class="content-rendered text-gray-800 aifontSize" style="">
3143                                                                ${mdParser.render(group.groupContent)}
3144                                                            </div>
3145                                                        </div>
3146                                                    ` : '';
3147
3148                    if (groupContentHtml) {
3149                        const contentGroupHtml = `
3150                                                            <div class="segment-group" data-group-id="${group.id}" data-group-index="${item.index}" data-type="content">
3151                                                                ${groupContentHtml}
3152                                                            </div>
3153                                                        `;
3154                        $combinedContainer.append(contentGroupHtml);
3155                    }
3156                } else if (item.type === 'reasoning') {
3157                    // 渲染 reasoning segment
3158                    const segment = buffer.segments[item.index];
3159                    if (!segment) return;
3160
3161                    const isReasoning = (segment.type === 'reasoning' || !segment.type);
3162                    const hasThink = segment.think && segment.think.trim() !== '';
3163                    const hasContent = segment.content && segment.content.trim() !== '';
3164                    if (!isReasoning || (!hasThink && !hasContent)) return;
3165
3166                    const thinkHtml = `
3167                                                        <div class="segment-group" data-segment-index="${item.index}" data-type="reasoning">
3168                                                            ${hasThink ? `
3169                                                            <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="${item.index}">
3170                                                                <i class="fa ${segment.isCollapsed ? 'fa-chevron-right' : 'fa-chevron-down'} think-toggle-icon mr-1"></i>
3171                                                                <sp
3171an style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
3172                                                            </div>
3173                                                            <div class="think-content-body ${segment.isCollapsed ? 'hidden' : ''} py-1 text-gray-600 text-sm aifontSize" style="">
3174                                                                ${buildThinkContent(segment.think || '')}
3175                                                            </div>
3176                                                            ` : ''}
3177                                                            ${hasContent ? `
3178                                                            <div class="content-body pl-3 mt-1">
3179                                                                ${mdParser.render(segment.content)}
3180                                                            </div>
3181                                                            ` : ''}
3182                                                        </div>
3183                                                    `;
3184                    $combinedContainer.append(thinkHtml);
3185                } else if (item.type === 'tool') {
3186                    // 渲染 tool segment
3187                    const segment = buffer.segments[item.index];
3188                    if (!segment || segment.type !== 'tool') return;
3189
3190                    const toolHtml = `
3191                                                        <div class="segment-group">
3192                                                            <div class="tool-header flex items-center cursor-pointer text-sm text-black-600 mb-1">
3193                                                                <i class="fa ${segment.isCollapsed ? 'fa-chevron-right' : 'fa-chevron-down'} tool-toggle-icon mr-1"></i>
3194                                                                <span style="font-weight:bold;">${language === 'Chinese' ? '工具调用' : 'Tool Call'}</span>
3195                                                            </div>
3196                                                            <div class="tool-content-body ${segment.isCollapsed ? 'hidden' : ''} pl-3 py-1 aifontSize" style="color:gray;">
3197                                                                ${mdParser.render(segment.toolContent || '')}
3198                                                            </div>
3199                                                        </div>
3200                                                    `;
3201                    $combinedContainer.append(toolHtml);
3202                }
3203            });
3204
3205
3206
3207
3208            //             <div class="status-indicator pl-3 py-1 text-sm" style="background-color: #f8f9fa; border-radius: 4px; margin-top: 4px; margin-bottom: 4px;">
3209            //     <span class="status-icon mr-1"></span>
3210            //     <span class="status-text"></span>
3211            // </div>
3212            // ========== 新增:将状态指示器插入到最后一个 .think-content-body 的末尾 ==========
3213            const statusIndicatorHtml = `
3214                                                    <div class="status-indicator pl-3 py-1 text-sm" style="background-color: #f8f9fa; border-radius: 4px; margin-top: 4px; margin-bottom: 4px; display: none;text-align:center">
3215                                                        <span class="status-text"></span>
3216                                                        <span class="status-icon ml-1"></span>
3217                                                    </div>
3218                                                `;
3219            // 找到 $combinedContainer 中最后一个 .think-content-body 元素,并将状态指示器追加到其内部末尾
3220            $combinedContainer.find('.think-content-body').last().append(statusIndicatorHtml);
3221            // 折叠交互事件(原有逻辑保留)
3222            $combinedContainer.on('click', '.think-header', function (e) {
3223                e.stopPropagation();
3224                const $header = $(this);
3225                const $group = $header.closest('.segment-group');
3226                const type = $group.data('type');
3227                const idx = $header.data('index');
3228                const groupIdx = $header.data('group-index');
3229
3230                let targetItem, targetContent;
3231                if (type === 'reasoning') {
3232                    targetItem = buffer.segments[idx];
3233                    targetContent = $group.find('.think-content-body');
3234                } else if (type === 'task') {
3235                    targetItem = buffer.nonContentGroups[groupIdx];
3236                    targetContent = $group.find('.think-content-body');
3237                } else if (type === 'unified-task') {
3238                    // 处理统一task容器的折叠
3239                    const groupId = $group.data('group-id');
3240                    const groupIdx = $group.data('group-index');
3241                    targetContent = $group.find('.think-content-body');
3242
3243                    // 获取对应的 group 对象
3244                    const targetGroup = buffer.nonContentGroups[groupIdx];
3245                    if (!targetGroup) return;
3246
3247                    // 切换折叠状态
3248                    targetGroup.isCollapsed = !targetGroup.isCollapsed;
3249
3250                    if (targetGroup.isCollapsed) {
3251                        targetContent.hide();
3252                        $header.find('.think-toggle-icon')
3253                            .removeClass('fa-chevron-down')
3254                            .addClass('fa-chevron-right');
3255                    } else {
3256                        targetContent.show();
3257                        $header.find('.think-toggle-icon')
3258                            .removeClass('fa-chevron-right')
3259                            .addClass('fa-chevron-down');
3260                    }
3261                    return;
3262                }
3263                if (!targetItem || !targetContent) return;
3264
3265                targetItem.isCollapsed = !targetItem.isCollapsed;
3266                targetContent.toggleClass('hidden', targetItem.isCollapsed);
3267                $header.find('.think-toggle-icon')
3268                    .removeClass('fa-chevron-down fa-chevron-right')
3269                    .addClass(targetItem.isCollapsed ? 'fa-chevron-right' : 'fa-chevron-down');
3270            });
3271
3272            $combinedContainer.on('click', '.tool-header', function (e) {
3273                e.stopPropagation();
3274                const $header = $(this);
3275                const $group = $header.closest('.segment-group');
3276                const $content = $group.find('.tool-content-body');
3277                const isCollapsed = $content.hasClass('hidden');
3278
3279                $content.toggleClass('hidden', !isCollapsed);
3280                $header.find('.tool-toggle-icon')
3281                    .removeClass('fa-chevron-down fa-chevron-right')
3282                    .addClass(!isCollapsed ? 'fa-chevron-right' : 'fa-chevron-down');
3283            });
3284
3285            // 挂载内容+恢复3D容器
3286            $thinkContainerDom.append($combinedContainer);
3287            if (pdbContainersHtml.length > 0) {
3288                $thinkContainerDom.append(pdbContainersHtml.join(''));
3289                $messageDom.data('has-3d-containers', true);
3290
3291                // 重新渲染3D模型
3292                setTimeout(() => {
3293                    $thinkContainerDom.find('.pdb-3d-container').each(function () {
3294                        const $container = $(this);
3295                        const containerId = $container.attr('id');
3296                        const $molContainer = $container.find('.mol-container');
3297                        if ($molContainer.length && (!$molContainer[0].viewer || $molContainer.is(':empty'))) {
3298                            $molContainer.html(`
3299                                                            <div class="text-center text-white p-4">
3300                                                                <i class="fa fa-spinner fa-spin fa-2x mb-2"></i>
3301                                                                <p>loading 3D model...</p>
3302                                                            </div>
3303                                                        `);
3304                            const fileName = extractFileNameFromContainerId(containerId);
3305                            if (fileName) loadPdbAndRender3D({ fileName: fileName }, messageId, null);
3306                        }
3307                    });
3308                }, 100);
3309            }
3310
3311            // 加载指示器控制
3312            if (buffer.isLoading) {
3313                const $lastGroup = $combinedContainer.find('.segment-group').last();
3314                if (!$lastGroup.find('.load-more-indicator').length) {
3315                    $lastGroup.append(`
3316                                                    <div class="load-more-indicator mt-1 block text-center" style="color: dimgray;">
3317                                                        <i class="fa fa-spinner fa-spin"></i>
3318                                                    </div>
3319                                                `);
3320                }
3321            } else {
3322                $('.load-more-indicator').remove();
3323            }
3324
3325            // 链接 _blank 实时加;mermaid 在 task 出现 / task_done / 含 ``` 的 content 时渲染
3326            $combinedContainer.find('a:not([target="_blank"])').attr('target', '_blank');
3327            reinjectCachedMermaidInContainer(messageId, buffer, $combinedContainer);
3328            formatPendingGroups(messageId, buffer, $combinedContainer);
3329
3330            updateStatusIndicator($messageDom, buffer);
3331            scrollToBottom(true);
3332        }
3333        // 【关键删除】废弃旧的标记处理函数(不再使用)
3334        // function processThinkContent(chunk, messageId) { ... }
3335        // function processMarkContent(chunk) { ... }
3336
3337        // 构建思考内容(保留原格式逻辑)
3338        function buildThinkContent(thinkText) {
3339            if (!thinkText) return '';
3340            // 1. 先转义特殊字符(防止XSS)
3341            const escapedText = $('<div>').text(thinkText).html();
3342            // 2. 再用mdParser解析Markdown格式(和结果内容保持一致)
3343            return mdParser.render(escapedText);
3344        }
3345
3346        // 【关键修改8】废弃旧的Markdown构建函数(直接用mdParser.render)
3347        // function buildMarkdownContent() { ... }
3348
3349        // 更新发送按钮状态(保留原逻辑)
3350        function updateSendButtonState(isLoading) {
3351            const sendBtn = $('#sendBtn');
3352            const sendIcon = sendBtn.find('.send-icon');
3353            const loadingIcon = sendBtn.find('.loading-icon');
3354            if (isLoading) {
3355                sendIcon.addClass('hidden');
3356                loadingIcon.removeClass('hidden');
3357                sendBtn.attr('disabled', 'disabled');
3358            } else {
3359                sendIcon.removeClass('hidden');
3360                loadingIcon.addClass('hidden');
3361                sendBtn.removeAttr('disabled');
3362            }
3363        }
3364
3365        // 标记响应完成(保留原逻辑,补充缓冲区状态)
3366        function completeResponse(messageId) {
3367            responseCompleted[messageId] = true;
3368            const buffer = responseBuffers[messageId];
3369            if (buffer) {
3370                buffer.isLoading = false;
3371                // 【新增】处理未完成的工具缓存:若有未完整的工具内容,作为普通content处理
3372                if (buffer.toolContentBuffer) {
3373                    buffer.segments.push({
3374                        type: 'reasoning',
3375                        think: '',
3376                        content: buffer.toolContentBuffer, // 不完整的工具内容作为普通文本
3377                        isCollapsed: false
3378                    });
3379                    buffer.toolContentBuffer = '';
3380                }
3381                // 原有逻辑:处理最后一组的折叠状态
3382                if (buffer.segments.length > 0) {
3383                    const lastSegment = buffer.segments[buffer.segments.length - 1];
3384                    if ((lastSegment.type === 'reasoning' && lastSegment.think && !lastSegment.content) || !lastSegment.type) {
3385                        lastSegment.isCollapsed = true;
3386                    } else if (lastSegment.type === 'tool') {
3387                        lastSegment.isCollapsed = true;
3388                    }
3389                }
3390            }
3391
3392            // 【新增】隐藏审核加载提示
3393            hideAuditLoading(messageId);
3394            isLoading = false;
3395            $('#sendBtn').removeClass('loading');
3396            $('#sendBtn').off('click').on('click', sendMessage);
3397            $('.load-more-indicator').remove();
3398            // 重新渲染以应用最终状态
3399            //updateBotMessage(messageId, []);
3400        }
3401
3402        // 显示错误消息(保留原逻辑)
3403        function showError(message) {
3404            const messageId = generateUniqueId();
3405            const messageHtml = `
3406                                                                                        <div class="message bot-message message-appear" id="${messageId}">
3407                                                                                            <div class="flex items-start space-x-3">
3408                                                                                                <div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center text-white text-sm font-bold flex-shrink-0 shadow-md">!</div>
3409                                                                                                <div class="bg-red-50 rounded-lg shadow-md p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-l-4 border-red-500">
3410                                                                                                    <div class="prose max-w-none">
3411                                                                                                        <p class="text-red-600">${message}</p>
3412                                                                                                    </div>
3413                                                                                                </div>
3414                                                                                            </div>
3415                                                                                        </div>
3416                                                                                    `;
3417            messagesContainer.append(messageHtml);
3418            scrollToBottom(true);
3419        }
3420
3421        // 清空消息(保留原逻辑)
3422        function clearMessages() {
3423            $('#sendBtn').prop('disabled', false);
3424            let spanHtm = language === 'Chinese' ? '你好!我是你的AI助手,有什么可以帮助ä
3424½ çš„吗?' : 'Hello! I am your AI assistant, can I help you?';
3425            fetch(`https://aipublic.ptglab.com/llm/v1/product/clear-chat-context/${uuid}`, { method: 'GET' })
3426                .then(data => { /* 空处理 */ });
3427            messagesContainer.children().not('.able-intro').remove();
3428            // 【新增】清理审核加载状态
3429            Object.keys(auditLoadingStates).forEach(key => delete auditLoadingStates[key]);
3430            rebuildChatRoundIds.clear();
3431
3432            // 【新增】清除localStorage中的uuid,生成新的uuid
3433            // localStorage.removeItem('chat_uuid');
3434            generateSafeNumericId()
3435                .then(id => {
3436                    uuid = id
3437                    localStorage.setItem('chat_uuid', uuid);
3438                    // 后续逻辑
3439                })
3440            // 【新增】显示介绍页面
3441            $(".able-intro").css("display", "block");
3442        }
3443
3444        // 智能滚动到底部(保留原逻辑)
3445        function scrollToBottom(force = false) {
3446            // 1. 保持DOM元素获取逻辑(无需改)
3447            const container = $('#messagesContainer')[0];
3448            if (!container) return;
3449
3450            // 2. 计算【准确的底部位置】(关键修复)
3451            const scrollHeight = container.scrollHeight;
3452            const clientHeight = container.clientHeight;
3453            const maxScrollTop = scrollHeight - clientHeight; // 真正的“最底部”位置
3454
3455            // 3. 简化判断:强制滚动 或 用户未手动滚离底部(阈值缩小到5px,减少误判)
3456            const scrollTop = container.scrollTop;
3457            const isAtBottom = scrollTop >= maxScrollTop - 5; // 阈值从20改为5,更灵敏
3458
3459            // 4. 滚动到准确的底部位置(而非scrollHeight)
3460            if (force || isAtBottom) {
3461                container.scrollTo({
3462                    top: maxScrollTop, // 用计算出的“真正底部”替换scrollHeight
3463                    behavior: 'auto' // 实时输出场景:把smooth改为auto,避免滚动动画延迟
3464                });
3465            }
3466        }
3467
3468        // 生成单个回复的唯一ID(保留原逻辑)
3469        function generateUniqueId() {
3470            return Date.now().toString();
3471        }
3472
3473        /**
3474* 显示审核加载提示
3475* @param {string} messageId - 消息ID
3476*/
3477        function showAuditLoading(messageId) {
3478            const $message = $(`#${messageId}`);
3479            if (!$message.length) return;
3480
3481            const $thinkContainer = $message.find('.markdown-preview');
3482            if (!$thinkContainer.length) return;
3483
3484            // 检查是否已经显示审核加载提示
3485            if ($thinkContainer.find('.audit-loading').length > 0) return;
3486
3487            // 动态注入CSS样式确保动画生效
3488            if (!$('#audit-animation-styles').length) {
3489                $('head').append(`
3490                                                                                    <style id="audit-animation-styles">
3491                                                                                        .audit-loading {
3492                                                                                            animation: audit-glow 2s ease-in-out infinite !important;
3493                                                                                            text-align: center !important;
3494                                                                                            margin: 10px auto !important;
3495                                                                                            max-width: 200px !important;
3496                                                                                            padding: 12px 16px !important;
3497                                                                                        }
3498                                                                                        .audit-loading .audit-text {
3499                                                                                            animation: audit-pulse 1.5s ease-in-out infinite !important;
3500                                                                                            color: #4b5563 !important;
3501                                                                                            font-weight: 500 !important;
3502                                                                                        }
3503                                                                                        .audit-loading .audit-text::after {
3504                                                                                            content: '...';
3505                                                                                            animation: audit-dots 1.5s infinite !important;
3506                                                                                            display: inline-block !important;
3507                                                                                        }
3508                                                                                        @keyframes audit-glow {
3509                                                                                            0%, 100% {
3510                                                                                                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
3511                                                                                                border-color: #e
35110e7ff;
3512                                                                                                background: #f8fafc;
3513                                                                                            }
3514                                                                                            50% {
3515                                                                                                box-shadow: 0 4px 8px rgba(59, 130, 246, 0.1);
3516                                                                                                border-color: #c7d2fe;
3517                                                                                                background: #f1f5f9;
3518                                                                                            }
3519                                                                                        }
3520                                                                                        @keyframes audit-pulse {
3521                                                                                            0%, 100% {
3522                                                                                                opacity: 0.7;
3523                                                                                            }
3524                                                                                            50% {
3525                                                                                                opacity: 1;
3526                                                                                            }
3527                                                                                        }
3528                                                                                        @keyframes audit-dots {
3529                                                                                            0%, 20% {
3530                                                                                                opacity: 0;
3531                                                                                            }
3532                                                                                            50% {
3533                                                                                                opacity: 1;
3534                                                                                            }
3535                                                                                            80%, 100% {
3536                                                                                                opacity: 0;
3537                                                                                            }
3538                                                                                        }
3539                                                                                    </style>
3540                                                                                `);
3541            }
3542
3543            const auditLoadingHtml = `
3544                                                                                <div class="audit-loading" id="audit-loading-${messageId}">
3545                                                                                    <div class="audit-loading-content">
3546                                                                                        <div class="audit-spinner"></div>
3547                                                                                        <div class="audit-text">
3548                                                                                            ${language === 'Chinese' ? '审核中' : 'Reviewing'}
3549                                                                                        </div>
3550                                                                                    </div>
3551                                                                                </div>
3552                                                                            `;
3553
3554
3555            $thinkContainer.append(auditLoadingHtml);
3556            auditLoadingStates[messageId] = true;
3557            scrollToBottom(true);
3558        }
3559
3560        function updateStatusIndicator($messageDom, buffer) {
3561            const $statusContainer = $messageDom.find('.status-indicator');
3562            const $statusIcon = $statusContainer.find('.status-icon');
3563            const $statusText = $statusContainer.find('.status-text');
3564
3565            // const isAuditing = auditLoadingStates[$messageDom.attr('id')] || false;
3566
3567            if (isShowReview === 'Reviewed') {
3568                $statusContainer.show();
3569                $statusContainer.css('border-left', '3px solid #28a745');
3570                // 【修改】先设置文本,再设置图标
3571                $statusText.text(language === 'Chinese' ? '审核成功' : 'Reviewed ');
3572                $statusIcon.html('<i class="fa fa-check" style="color: #28a745;"></i>');
3573                $statusText.css('color', '#155724');
3574            } else if (isShowReview === 'Reviewing') {
3575                $statusContainer.show();
3576                $statusContainer.css('border-left', '3px solid #ffc107');
3577                // 【修改】先设置文本,再设置图标
3578                $statusText.text(language === 'Chinese' ? '审核中' : 'Reviewing');
3579                $statusIcon.html('<i class="fa fa-spinner fa-spin" style="color: #ffc107;"></i>');
3580                $statusText.css('color', '#856404');
3581            }
3582        }
3583        /**
3584         * 隐藏审核加载提示
3585         * @param {string} messageId - 消息ID
3586         */
3587        function hideAuditLoading(messageId) {
3588            const $auditLoading = $(`#audit-loading-${messageId}`);
3589            if ($auditLoading.length) {
3590                $auditLoading.addClass('audit-loading-fade-out');
3591                setTimeout(() => {
3592                    $auditLoading.remove();
3593                }, 500);
3594                auditLoadingStates[messageId] = false;
3595            }
3596        }
3597
3598
3599        // 通知后台用户主动中断了当前对话
3600        function notifyCancelChat(dialogId) {
3601            const id = dialogId || localStorage.getItem('chat_uuid') || uuid;
3602            if (!id) return;
3603            fetch(`https://aipublic.ptglab.com/llm/v1/product/cancel-chat/${id}`, { method: 'GET' })
3604                .catch(err => console.warn('cancel-chat failed:', err));
3605        }
3606
3607        // 中止当前请求(保留原逻辑)
3608        function abortRequest(notifyServer, dialogId) {
3609            if (abortController) {
3610                abortController.abort();
3611            }
3612            if (notifyServer) {
3613                notifyCancelChat(dialogId);
3614            }
3615        }
3616
3617        // 日志记录函数(保留原逻辑)
3618        function log(...args) {
3619            if (enableLogging) {
3620                // console.log(...args);
3621            }
3622        }
3623
3624
3625        /**
3626       * 在.content-body最末尾添加复制图标
3627       * @param {string} messageId - 消息ID
3628       */
3629        function addCopyIconToContent(messageId) {
3630            const $message = $(`#${messageId}`);
3631            if (!$message.length) {
3632                console.log("Message not found:", messageId);
3633                return;
3634            }
3635
3636            // 只查找最后一个.content-body容器
3637            const $contentBody = $message.find('.content-body').last();
3638
3639            if (!$contentBody.length) {
3640                return;
3641            }
3642
3643            // 检查是否已经添加了复制图标
3644            if ($contentBody.find('.copy-icon-container').length > 0) {
3645                return; // 跳过已添加的
3646            }
3647
3648            // 创建复制和导出图标容器
3649            const copyIconHtml = `
3650                                                                                <div class="copy-icon-container">
3651                                                                                    <div class="copy-icon" title="${language === 'Chinese' ? '复制Markdown内容' : 'Copy Markdown Content'}">
3652                                                                                        <i class="fa fa-copy"></i>
3653                                                                                    </div>
3654                                                                                    <div class="export-pdf-icon" title="${language === 'Chinese' ? '导出为PDF' : 'Export as PDF'}">
3655                                                                                        <i class="fa fa-file-pdf-o"></i>
3656                                                                                    </div>
3657                                                                                </div>
3658                                                                            `;
3659
3660            // 添加到content-body末尾
3661            $contentBody.append(copyIconHtml);
3662
3663            // 绑定点击事件 - 使用事件委托确保事件不被覆盖
3664            const $copyIcon = $contentBody.find('.copy-icon');
3665
3666            // 移除所有可能的事件监听器
3667            $copyIcon.off('click copy click.copyIcon');
3668
3669            // 绑定新的点击事件
3670            $copyIcon.on('click.copyIcon', function (e) {
3671                e.preventDefault();
3672                e.stopPropagation();
3673                copyMarkdownContent($contentBody, $copyIcon);
3674            });
3675
3676            // 绑定导出PDF按钮点击事件
3677            const $exportIcon = $contentBody.find('.export-pdf-icon');
3678
3679            if ($exportIcon.length > 0) {
3680                $exportIcon.off('click export click.exportPdfIcon');
3681                $exportIcon.on('click.exportPdfIcon', function (e) {
3682                    e.preventDefault();
3683                    e.stopPropagation();
3684                    exportContentAsPDF($contentBody, $exportIcon);
3685                });
3686
3687                // 测试点击事件是否绑定成功
3688            } else {
3689                console.error("Export PDF icon not found!");
3690            }
3691        }
3692        /**
3693         * 复制Markdown内容到剪贴板
3694         * @param {jQuery} $contentBody - 内容容器
3695         * @param {jQuery} $copyIcon - 复制图标
3696         */
3697        function copyMarkdownContent($contentBody, $copyIcon) {
3698            try {
3699                // 获取原始HTML内容
3700                const htmlContent = $contentBody.html();
3701
3702                // 移除复制图标容器,避免复制时包含复制按钮
3703                const tempContent = htmlContent.replace(/<div class="copy-icon-container">[\s\S]*?<\/div>/g, '');
3704
3705                // 创建临时div来解析HTML
3706                const $tempDiv = $('<div>').html(tempContent);
3707
3708                // 移除3D容器,因为3D容器不应该被复制
3709                $tempDiv.find('.pdb-3d-container').remove();
3710
3711                // 转换HTML为Markdown格式
3712                const markdownContent = htmlToMarkdown($tempDiv.html());
3713
3714                // 复制到剪贴板
3715                if (navigator.clipboard && window.isSecureContext) {
3716                    navigator.clipboard.writeText(markdownContent).then(() => {
3717                        showCopySuccess($copyIcon);
3718                    }).catch(err => {
3719                        console.error('复制失败:', err);
3720                        fallbackCopyTextToClipboard(markdownContent, $copyIcon);
3721                    });
3722                } else {
3723                    // 降级方案
3724                    fallbackCopyTextToClipboard(markdownContent, $copyIcon);
3725                }
3726            } catch (error) {
3727                console.error('复制过程中出错:', error);
3728                showCopyError($copyIcon);
3729            }
3730        }
3731
3732        /**
3733         * 降级复制方案
3734         * @param {string} text - 要复制的文本
3735         * @param {jQuery} $copyIcon - 复制图标
3736         */
3737        function fallbackCopyTextToClipboard(text, $copyIcon) {
3738            const textArea = document.createElement('textarea');
3739            textArea.value = text;
3740            textArea.style.position = 'fixed';
3741            textArea.style.left = '-999999px';
3742            textArea.style.top = '-999999px';
3743            document.body.appendChild(textArea);
3744            textArea.focus();
3745            textArea.select();
3746
3747            try {
3748                const successful = document.execCommand('copy');
3749                if (successful) {
3750                    showCopySuccess($copyIcon);
3751                } else {
3752                    showCopyError($copyIcon);
3753                }
3754            } catch (err) {
3755                console.error('降级复制失败:', err);
3756                showCopyError($copyIcon);
3757            } finally {
3758                document.body.removeChild(textArea);
3759            }
3760        }
3761
3762        /**
3763         * 显示复制成功状态
3764         * @param {jQuery} $copyIcon - 复制图标
3765         */
3766        function showCopySuccess($copyIcon) {
3767            $copyIcon.addClass('copied');
3768            $copyIcon.find('i').remove();
3769
3770            // 2秒后恢复原状
3771            setTimeout(() => {
3772                $copyIcon.removeClass('copied');
3773                $copyIcon.html('<i class="fa fa-copy"></i>');
3774            }, 2000);
3775        }
3776
3777        /**
3778         * 显示复制错误状态
3779         * @param {jQuery} $copyIcon - 复制图标
3780         */
3781        function showCopyError($copyIcon) {
3782            const originalHtml = $copyIcon.html();
3783            $copyIcon.html('<i class="fa fa-exclamation-triangle"></i>');
3784            $copyIcon.css('background-color', '#ef4444');
3785            $copyIcon.css('border-color', '#dc2626');
3786            $copyIcon.css('color', 'white');
3787
3788            // 1秒后恢复原状
3789            setTimeout(() => {
3790                $copyIcon.html(originalHtml);
3791                $copyIcon.css('background-color', '');
3792                $copyIcon.css('border-color', '');
3793                $copyIcon.css('color', '');
3794            }, 1000);
3795        }
3796
3797        /**
3798         * 将HTML转换为Markdown格式
3799         * @param {string} html - HTML内容
3800         * @returns {string} Markdown内容
3801         */
3802        function htmlToMarkdown(html) {
3803            // 创建临时div来解析HTML
3804            const $temp = $('<div>').html(html);
3805
3806            // 处理各种HTML标签转换为Markdown
3807            $temp.find('h1').each(function () {
3808                $(this).replaceWith(`# ${$(this).text()}\n\n`);
3809            });
3810
3811            $temp.find('h2').each(function () {
3812                $(this).replaceWith(`## ${$(this).text()}\n\n`);
3813            });
3814
3815            $temp.find('h3').each(function () {
3816                $(this).replaceWith(`### ${$(this).text()}\n\n`);
3817            });
3818
3819            $temp.find('h4').each(function () {
3820                $(this).replaceWith(`#### ${$(this).text()}\n\n`);
3821            });
3822
3823            $temp.find('h5').each(function () {
3824                $(this).replaceWith(`##### ${$(this).text()}\n\n`);
3825            });
3826
3827            $temp.find('h6').each(function () {
3828                $(this).replaceWith(`###### ${$(this).text()}\n\n`);
3829            });
3830
3831            $temp.find('strong, b').each(function () {
3832                const text = $(this).text().trim();
3833                if (text) {
3834                    $(this).replaceWith(`**${text}**`);
3835                } else {
3836                    $(this).remove();
3837                }
3838            });
3839
3840            $temp.find('em, i').each(function () {
3841                const text = $(this).text().trim();
3842                if (text) {
3843                    $(this).replaceWith(`*${text}*`);
3844                } else {
3845                    $(this).remove();
3846                }
3847            });
3848
3849            $temp.find('code').each(function () {
3850                $(this).replaceWith(`\`${$(this).text()}\``);
3851            });
3852
3853            $temp.find('pre').each(function () {
3854                const code = $(this).text();
3855                $(this).replaceWith(`\n\`\`\`\n${code}\n\`\`\`\n\n`);
3856            });
3857
3858            $temp.find('ul').each(function () {
3859                const $ul = $(this);
3860                let markdown = '';
3861                $ul.find('li').each(function () {
3862                    const text = $(this).text().trim();
3863                    if (text) {
3864                        markdown += `- ${text}\n`;
3865                    }
3866                });
3867                if (markdown) {
3868                    markdown = '\n' + markdown + '\n';
3869                }
3870                $ul.replaceWith(markdown);
3871            });
3872
3873            $temp.find('ol').each(function () {
3874                const $ol = $(this);
3875                let markdown = '';
3876                $ol.find('li').each(function (index) {
3877                    const text = $(this).text().trim();
3878                    if (text) {
3879                        markdown += `${index + 1}. ${text}\n`;
3880                    }
3881                });
3882                if (markdown) {
3883                    markdown = '\n' + markdown + '\n';
3884                }
3885                $ol.replaceWith(markdown);
3886            });
3887
3888            $temp.find('a').each(function () {
3889                const href = $(this).attr('href');
3890                const text = $(this).text();
3891                $(this).replaceWith(`[${text}](${href})`);
3892            });
3893
3894            $temp.find('blockquote').each(function () {
3895                const text = $(this).text();
3896                $(this).replaceWith(`> ${text}\n\n`);
3897            });
3898
3899            $temp.find('hr').each(function () {
3900                $(this).replaceWith('\n---\n\n');
3901            });
3902
3903            $temp.find('p').each(function () {
3904                const text = $(this).text().trim();
3905                if (text) {
3906                    $(this).replaceWith(`${text}\n\n`);
3907                } else {
3908                    $(this).remove();
3909                }
3910            });
3911
3912            // 获取最终文本并清理
3913            let markdown = $temp.text();
3914
3915            // 清理多余的空行(不变,兼容所有环境)
3916            markdown = markdown.replace(/\n{3,}/g, '\n\n');
3917
3918            // 清理多余的星号(连续的星号)
3919            // 1. 替换 3 个及以上连续星号为 **(不变)
3920            markdown = markdown.replace(/\*{3,}/g, '**');
3921            // 2. 替换 "星号后接空格" 的场景(用捕获组替代正向先行断言,兼容ä
3921½Žç‰ˆæœ¬æµè§ˆå™¨ï¼‰
3922            markdown = markdown.replace(/\*{2,}(\s)/g, '**$1');
3923            // 3. 替换 "空格后接 2 个及以上星号" 的场景(不变)
3924            markdown = markdown.replace(/(\s)\*{2,}/g, '$1**');
3925
3926            // 清理空星号对(不变)
3927            markdown = markdown.replace(/\*\*\s*\*\*/g, '');
3928            markdown = markdown.replace(/\*\s*\*/g, '');
3929
3930            // 清理首尾空白(不变)
3931            markdown = markdown.trim();
3932
3933            return markdown;
3934        }
3935
3936        /**
3937         * 导出内容为PDF
3938         * @param {jQuery} $contentBody - 内容容器
3939         * @param {jQuery} $exportIcon - 导出图标
3940         */
3941        function exportContentAsPDF($contentBody, $exportIcon) {
3942
3943            try {
3944                // 显示导出中状态
3945                $exportIcon.addClass('exporting');
3946                $exportIcon.find('i').remove();
3947
3948                // 创建临时容器用于PDF导出
3949                const $tempContainer = $('<
3949div>').css({
3950                    position: 'absolute',
3951                    left: '-9999px',  // 移到屏幕外避免闪烁
3952                    top: '-9999px',
3953                    width: '800px',
3954                    backgroundColor: 'white',
3955                    padding: '10px', // 减少内边距避免截断
3956                    fontFamily: 'Arial, sans-serif',
3957                    fontSize: '14px',
3958                    lineHeight: '1.6', // 适中的行高
3959                    color: '#333',
3960                    boxShadow: 'none', // 移除阴影避免额外空间
3961                    borderRadius: '0', // 移除圆角
3962                    visibility: 'visible',
3963                    opacity: 1,
3964                    zIndex: 9999,
3965                    overflow: 'visible',
3966                    margin: '0',
3967                    border: 'none'
3968                });
3969
3970                // 复制内容到临时容器
3971                const originalContent = $contentBody.clone();
3972
3973                // 移除复制和导出按钮
3974                originalContent.find('.copy-icon-container').remove();
3975
3976                // 移除3D容器(因为无法在PDF中渲染)
3977                originalContent.find('.pdb-3d-container').remove();
3978
3979                // 保留图标和表格,只处理图片链接
3980                originalContent.find('a[href*=".jpg"], a[href*=".jpeg"], a[href*=".png"], a[href*=".gif"], a[href*=".webp"], a[href*=".svg"]').each(function () {
3981                    const $link = $(this);
3982                    const href = $link.attr('href');
3983                    const fileName = href.split('/').pop() || '图片';
3984                    // 创建固定宽度的图片占位符
3985                    $link.replaceWith(`
3986                                                                                    <div class="image-placeholder">
3987                                                                                        <div>
3988                                                                                            <i class="fa fa-image" style="font-size: 28px; margin-bottom: 8px; display: block;"></i>
3989                                                                                            <div style="font-weight: bold; margin-bottom: 4px;">图片: ${fileName}</div>
3990                                                                                            <div style="font-size: 12px; color: #6c757d;">400×250px</div>
3991                                                                                        </div>
3992                                                                                    </div>
3993                                                                                `);
3994                });
3995
3996                // 处理其他链接,但保留图标
3997                originalContent.find('a').not('a[href*=".jpg"], a[href*=".jpeg"], a[href*=".png"], a[href*=".gif"], a[href*=".webp"], a[href*=".svg"]').each(function () {
3998                    const $link = $(this);
3999                    const href = $link.attr('href');
4000                    const text = $link.text();
4001                    // 如果链接包含图标,保留图标
4002                    if ($link.find('i, .fa, .icon').length > 0) {
4003                        const iconHtml = $link.find('i, .fa, .icon').prop('outerHTML');
4004                        $link.replaceWith(`${iconHtml} ${text} (${href})`);
4005                    } else {
4006                        $link.replaceWith(`${text} (${href})`);
4007                    }
4008                });
4009
4010                // 处理 Live Chat 元素,确保在PDF中正确显示
4011                const liveChatElements = originalContent.find('.open-live-chat');
4012
4013                liveChatElements.each(function () {
4014                    const $liveChat = $(this);
4015                    const text = $liveChat.text();
4016                    // 保留 Live Chat 文本,但移除特殊样式类
4017                    $liveChat.replaceWith(`<span style="color: #007bff; font-weight: bold;">${text}</span>`);
4018                });
4019
4020                // 添加内容到临时容器
4021                $tempContainer.append(originalContent);
4022                $('body').append($tempContainer);
4023                // 调试:检查内容是否正确添加
4024                const tempHtml = $tempContainer.html();
4025                const tempText = $tempContainer.text();
4026
4027                // 检查是否有未闭合的标签
4028                const openTags = (tempHtml.match(/</g) || []).length;
4029                const closeTags = (tempHtml.match(/>/g) || []).length;
4030
4031
4032                // 直接使用html2canvas + jsPDF方案(更可靠)
4033                const html2canvasLib = window.html2canvas || html2canvas;
4034                const jsPDFLib = window.jsPDF || window.jspdf || jsPDF;
4035
4036                // 如果jsPDF是对象,尝试获取构造函数
4037                const jsPDFConstructor = (typeof jsPDFLib === 'object' && jsPDFLib.jsPDF) ? jsPDFLib.jsPDF : jsPDFLib;
4038
4039
4040                if (typeof html2canvasLib !== 'undefined' && typeof jsPDFConstructor !== 'undefined') {
4041                    // 等待DOM完全渲染 - 对于长内容增加等待时间
4042                    setTimeout(() => {
4043                        // 强制重新计算高度 - 清除任何缓存的高度值
4044                        $tempContainer[0].style.height = 'auto'; // 重置高度
4045                        $tempContainer[0].style.minHeight = 'auto'; // 重置最小高度
4046
4047                        // 强制浏览器重新计算布局
4048                        const forceReflow = $tempContainer[0].offsetHeight;
4049
4050                        // 获取实际高度 - 多次测量确保准确
4051                        let actualHeight = Math.max(
4052                            $tempContainer[0].scrollHeight,
4053                            $tempContainer[0].offsetHeight,
4054                            $tempContainer[0].clientHeight
4055                        );
4056
4057                        // 再次测量,确保高度是最新的
4058                        actualHeight = Math.max(actualHeight, $tempContainer[0].scrollHeight);
4059
4060                        // 再次检查HTML内容是否完整
4061                        const finalHtml = $tempContainer.html();
4062                        const finalText = $tempContainer.text();
4063
4064                        html2canvasLib($tempContainer[0], {
4065                            scale: 2, // 提高缩放以获得更清晰的图像
4066                            useCORS: true,
4067                            allowTaint: true,
4068                            backgroundColor: '#ffffff',
4069                            width: 800,
4070                            height: actualHeight + 1000, // 增加更多缓冲确保长内容完整
4071                            logging: false,
4072                            imageTimeout: 20000, // 增加图片加载超时时间
4073                            scrollX: 0,
4074                            scrollY: 0,
4075                            onclone: function (clonedDoc) {
4076                                // 确保克隆的文档中样式正确应用
4077                                const clonedContainer = clonedDoc.querySelector('div');
4078                                if (clonedContainer) {
4079                                    clonedContainer.style.fontFamily = 'Arial, sans-serif';
4080                                    clonedContainer.style.fontSize = '14px';
4081                                    clonedContainer.style.lineHeight = '1.6';
4082                                    clonedContainer.style.color = '#333';
4083                                    clonedContainer.style.padding = '10px';
4084                                    clonedContainer.style.margin = '0';
4085                                    clonedContainer.style.width = '800px';
4086                                    clonedContainer.style.backgroundColor = 'white';
4087                                    clonedContainer.style.boxSizing = 'border-box';
4088                                }
4089
4090                                // 确保图标字体正确加载并改善样式
4091                                const style = clonedDoc.createElement('style');
4092                                style.textContent = `
4093                                                                                                @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css');
4094                                                                                                * { box-sizing: border-box; }
4095                                                                                                .fa, .icon { font-family: FontAwesome !important; }
4096                                                                                                table {
4097                                                                                                    border-collapse: collapse;
4098                                                                                                    width: 100%;
4099                                                                                                    margin: 10px 0;
4100                                                                                                    font-size: 13px;
4101                                                                                                }
4102                                                                                                th, td {
4103                                                                                                    border: 1px solid #ddd;
4104                                                                                                    padding: 8px;
4105                                                                                                    text-align: left;
4106                                                                                                    vertical-align: top;
4107                                                                                                    line-height: 1.5;
4108                                                                                                }
4109                                                                                                th {
4110                                                                                                    background-color: #f8f9fa;
4111                                                                                                    font-weight: bold;
4112                                                                                                    color: #495057;
4113                                                                                                }
4114                                                                                                p {
4115                                                                                                    margin: 8px 0;
4116                                                                                                    line-height: 1.7;
4117                                                                                                    text-align: justify;
4118                                                                                                }
4119                                                                                                h1, h2, h3, h4, h5, h6 {
4120                                                                                                    margin: 15px 0 8px 0;
4121                                                                                                    line-height: 1.3;
4122                                                                                                    font-weight: bold;
4123                                                                                                }
4124                                                                                                img {
4125                                                                                                    max-width: 100%;
4126                                                                                                    height: auto;
4127                                                                                                    margin: 0;
4128                                                                                                    padding: 0;
4129                                                                                                    display: block;
4130                                                                                                }
4131                                                                                                .image-placeholder {
4132                                                                                                    margin: 0;
4133                                                                                                    padding: 0;
4134                                                                                                    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
4135                                                                                                    border: 2px solid #dee2e6;
4136                                                                                                    border-radius: 8px;
4137                                                                                                    width: 400px;
4138                                                                                                    height: 250px;
4139                                                                                                    display: flex;
4140                                                                                                    align-items: center;
4141                                                                                                    justify-content: center;
4142                                                                                                    text-align: center;
4143                                                                                                    font-size: 16px;
4144                                                                                                    color: #495057;
4145                                                                                                    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
4146                                                                                                    flex-direction: column;
4147
4148                                                                                                    position: relative;
4149                                                                                                }
4150                                                                                                .image-placeholder i {
4151                                                                                                    font-size: 32px !important;
4152                                                                                                    margin-bottom: 10px !important;
4153                                                                                                    display: block !important;
4154                                                                                                    color: #007bff !important;
4155                                                                                                }
4156                                                                                                .image-placeholder > div {
4157                                                                                                    display: flex;
4158                                                                                                    flex-direction: column;
4159                                                                                                    align-items: center;
4160                                                                                                    justify-content: center;
4161                                                                                                    height: 100%;
4162                                                                                                }
4163                                                                                                .content-body {
4164                                                                                                    padding: 0;
4165                                                                                                    margin: 0;
4166                                                                                                }
4167                                                                                                .message-content {
4168                                                                                                    margin: 2px 0;
4169                                                                                                    padding: 0;
4170                                                                                                }
4171                                                                                                .open-live-chat {
4172                                                                                                    color: #007bff !important;
4173                                                                                                    font-weight: bold !important;
4174                                                                                                    text-decoration: none !important;
4175                                                                                                }
4176                                                                                            `;
4177                                clonedDoc.head.appendChild(style);
4178                            }
4179                        }).then(canvas => {
4180                            // 检查canvas是否包含完整内容
4181                            const canvasDataUrl = canvas.toDataURL('image/jpeg', 1.0);
4182                            // 再次检查原始HTML内容
4183                            const postCanvasHtml = $tempContainer.html();
4184                            const postCanvasText = $tempContainer.text();
4185                            // 创建PDF
4186                            const pdf = new jsPDFConstructor('p', 'mm', 'a4');
4187                            const imgWidth = 210; // A4宽度 (mm)
4188                            const pageHeight = 295; // A4高度 (mm)
4189                            const bottomMargin = 20; // 底部边距 (mm)
4190                            const effectivePageHeight = pageHeight - bottomMargin; // 有效页面高度
4191                            const imgHeight = (canvas.height * imgWidth) / canvas.width;
4192                            // 计算需要多少页 - 使用完整页面高度,但内容区域考虑底部边距
4193                            const totalPages = Math.ceil(imgHeight / pageHeight);
4194                            // 添加内容完整性检查
4195                            const contentText = $tempContainer.text();
4196                            if (totalPages === 1) {
4197                                // 单页内容 - 使用高质量PNG
4198                                pdf.addImage(canvas.toDataURL('image/jpeg', 0.95), 'JPEG', 0, 0, imgWidth, imgHeight);
4199                                // pdf.addImage(canvas.toDataURL('image/png', 1.0), 'PNG', 0, 0, imgWidth, imgHeight);
4200                            } else {
4201                                // 多页内容,使用简化的分页逻辑 - 减少分页次数降低截断风险
4202                                let currentY = 0;
4203                                let pageCount = 0;
4204
4205                                // 计算像素到毫米的转换比例
4206                                const pixelsPerMm = canvas.width / imgWidth;
4207                                // 使用更大的页面高度来容纳更多内容,减少分页
4208                                const pageHeightPx = pageHeight * pixelsPerMm;
4209
4210                                while (currentY < canvas.height) {
4211                                    if (pageCount > 0) {
4212                                        pdf.addPage();
4213                                    }
4214
4215                                    // 计算当前页的高度 - 简化逻辑,直接使用页面高度
4216                                    const remainingHeight = canvas.height - currentY;
4217                                    let sourceHeight = Math.min(remainingHeight, pageHeightPx);
4218
4219                                    // 计算在PDF中的位置和高度
4220                                    const targetHeight = sourceHeight / pixelsPerMm;
4221
4222                                    // 创建当前页的canvas
4223                                    const pageCanvas = document.createElement('canvas');
4224                                    const pageCtx = pageCanvas.getContext('2d');
4225                                    pageCanvas.width = canvas.width;
4226                                    pageCanvas.height = sourceHeight;
4227
4228                                    // 绘制当前页的内容
4229                                    pageCtx.drawImage(canvas, 0, currentY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight);
4230
4231                                    // 添加到PDF - 使用JPEG高质量格式
4232                                    pdf.addImage(pageCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', 0, 0, imgWidth, targetHeight);
4233
4234                                    // 更新当前位置
4235                                    currentY += sourceHeight;
4236                                    pageCount++;
4237
4238                                    // 防止无限循环
4239                                    if (pageCount > 50) {
4240                                        console.error("分页循环次数过多,强制退出");
4241                                        break;
4242                                    }
4243                                }
4244
4245                                if (canvas.height - currentY > 10) {
4246                                    console.warn(`警告:还有 ${canvas.height - currentY}px 内容未处理!`);
4247                                }
4248                            }
4249
4250                            // 保存PDF
4251                            pdf.save(`content-export-${Date.now()}.pdf`);
4252
4253                            // 清理临时容器
4254                            $tempContainer.remove();
4255
4256                            // 显示成功状态
4257                            showExportSuccess($exportIcon);
4258
4259                        }).catch(error => {
4260                            console.error('html2canvas失败:', error);
4261                            $tempContainer.remove();
4262                            showExportError($exportIcon);
4263                        });
4264                    }, 3000); // 给更多时间渲染,确保内容完全加载
4265                } else {
4266                    console.error('html2canvas或jsPDF库未加载');
4267                    $tempContainer.remove();
4268                    showExportError($exportIcon);
4269                }
4270
4271            } catch (error) {
4272                console.error('导出PDF过程中出错:', error);
4273                showExportError($exportIcon);
4274            }
4275        }
4276
4277
4278        /**
4279         * 显示导出成功状态
4280         * @param {jQuery} $exportIcon - 导出图标
4281         */
4282        function showExportSuccess($exportIcon) {
4283            $exportIcon.removeClass('exporting').addClass('success');
4284
4285            // 2秒后恢复原状
4286            setTimeout(() => {
4287                $exportIcon.removeClass('success');
4288                $exportIcon.html('<i class="fa fa-file-pdf-o"></i>');
4289            }, 2000);
4290        }
4291
4292        /**
4293         * 显示导出错误状态
4294         * @param {jQuery} $exportIcon - 导出图标
4295         */
4296        function showExportError($exportIcon) {
4297            const originalHtml = $exportIcon.html();
4298            $exportIcon.removeClass('exporting').html('<i class="fa fa-exclamation-triangle"></i>');
4299            $exportIcon.css('background-color', '#ef4444');
4300            $exportIcon.css('border-color', '#dc2626');
4301            $exportIcon.css('color', 'white');
4302
4303            // 1秒后恢复原状
4304            setTimeout(() => {
4305                $exportIcon.html(originalHtml);
4306                $exportIcon.css('background-color', '');
4307                $exportIcon.css('border-color', '');
4308                $exportIcon.css('color', '');
4309            }, 1000);
4310        }
4311
4312        /**
4313* 重新生成响应
4314* @param {jQuery} $resetIcon - 重置图标
4315* @param {string} messageId - 消息ID
4316*/
4317        function regenerateResponse($resetIcon, messageId) {
4318            try {
4319                // 显示重置中状态
4320                $resetIcon.addClass('resetting');
4321                $resetIcon.find('i').remove();
4322
4323                // 获取当前消息的内容
4324                const $message = $(`#${messageId}`);
4325                if (!$message.length) {
4326                    console.error('未找到消息:', messageId);
4327                    showResetError($resetIcon);
4328                    return;
4329                }
4330
4331                // 获取用户输入内容(从用户消息中获取)
4332                const $userMessage = $message.prev('.user-message');
4333                if (!$userMessage.length) {
4334                    console.error('未找到对应的用户消息');
4335                    showResetError($resetIcon);
4336                    return;
4337                }
4338
4339                const userText = $userMessage.find('.markdown-preview').text().trim();
4340                if (!userText) {
4341                    console.error('无法获取用户输入内容');
4342                    showResetError($resetIcon);
4343                    return;
4344                }
4345
4346                // 清空当前AI消息内容
4347                $message.find('.thinkHtml').empty();
4348                $message.find('.markHtml').empty();
4349
4350                // 设置重新生成标记
4351                window.isRegenerating = true;
4352                window.regenerateMessageId = messageId;
4353                window.regenerateDialogId = uuid;
4354                window.regenerateRoundId = messageId;
4355
4356                // 设置用户输入并立即清空,确保接口能正常调用但输入框不显示
4357                userInput.val(userText);
4358                sendMessage();
4359                userInput.val(''); // 立即清空输入框
4360                autoResizeUserInput();
4361                // 显示成功状态
4362                setTimeout(() => {
4363                    $resetIcon.removeClass('resetting').addClass('success');
4364                    $resetIcon.find('i').remove();
4365
4366                    // 2秒后恢复原状
4367                    setTimeout(() => {
4368                        $resetIcon.removeClass('success');
4369                        $resetIcon.html('<i class="fa fa-refresh"></i>');
4370                    }, 2000);
4371                }, 1000);
4372
4373            } catch (error) {
4374                console.error('重新生成响应失败:', error);
4375                showResetError($resetIcon);
4376            }
4377        }
4378
4379
4380        /**
4381         * 显示重置错误状态
4382         * @param {jQuery} $resetIcon - 重置图标
4383         */
4384        function showResetError($resetIcon) {
4385            const originalHtml = $resetIcon.html();
4386            $resetIcon.removeClass('resetting').html('<i class="fa fa-exclamation-triangle"></i>');
4387            $resetIcon.css('background-color', '#ef4444');
4388            $resetIcon.css('border-color', '#dc2626');
4389            $resetIcon.css('color', 'white');
4390
4391            // 1秒后恢复原状
4392            setTimeout(() => {
4393                $resetIcon.html(originalHtml);
4394                $resetIcon.css('background-color', '');
4395                $resetIcon.css('border-color', '');
4396                $resetIcon.css('color', '');
4397            }, 1000);
4398        }
4399
4400        /**
4401         * 为最后一轮对话添加重置图标
4402         * @param {string} messageId - 消息ID
4403         */
4404        function addResetIconToLastMessage(messageId) {
4405            // 先移除所有历史对话中的重置图标
4406            removeAllResetIcons();
4407
4408            const $message = $(`#${messageId}`);
4409            if (!$message.length) {
4410                console.log("Message not found:", messageId);
4411                return;
4412            }
4413
4414            // 查找所有.content-body容器
4415            const $contentBodies = $message.find('.content-body');
4416
4417            $contentBodies.each(function () {
4418                const $contentBody = $(this);
4419
4420                // 检查是否已经有重置图标
4421                if ($contentBody.find('.reset-icon').length > 0) {
4422                    return; // 跳过已添加的
4423                }
4424
4425                // 在复制图标前面添加重置图标
4426                const $copyIconContainer = $contentBody.find('.copy-icon-container');
4427                if ($copyIconContainer.length > 0) {
4428                    const resetIconHtml = `
4429                                                                                    <div class="reset-icon" title="Regenerate" data-message-id="${messageId}">
4430                                                                                        <i class="fa fa-refresh"></i>
4431                                                                                    </div>
4432                                                                                `;
4433                    $copyIconContainer.prepend(resetIconHtml);
4434
4435                }
4436            });
4437        }
4438
4439        /**
4440         * 移除所有历史对话中的重置图标
4441         */
4442        function removeAllResetIcons() {
4443            // 移除所有消息中的重置图标
4444            $('.reset-icon').remove();
4445        }
4446
4447
4448
4449        /**
4450* 检测并处理当前消息的.content-body中的.pdb链接
4451* @param {string} messageId - 消息ID
4452*/
4453        function checkAndProcessPdbLinks(messageId) {
4454            const $message = $(`#${messageId}`);
4455            if (!$message.length) {
4456                console.warn('未找到消息容器:', messageId);
4457                return;
4458            }
4459
4460            // 为当前消息初始化处理状态
4461            if (!processedPdbLinks.has(messageId)) {
4462                processedPdbLinks.set(messageId, new Set());
4463            }
4464            const messageProcessedLinks = processedPdbLinks.get(messageId);
4465
4466            // 只查找当前消息的.content-body中的.pdb链接
4467            const $pdbLinks = $message.find('.content-body a[href*=".pdb"]');
4468
4469            if ($pdbLinks.length > 0) {
4470                // 为每个PDB链接处理href属性
4471                $pdbLinks.each(function () {
4472                    const $link = $(this);
4473                    const originalHref = $link.attr('href');
4474
4475                    // 移除原始href属性,将其值保存到data-original-href中
4476                    $link.removeAttr('href');
4477                    $link.attr('data-original-href', originalHref);
4478                });
4479                // 收集所有PDB链接数据,按文件名分组
4480                const pdbDataMap = new Map(); // fileName -> { epitopes: [], originalUrls: [] }
4481
4482                $pdbLinks.each(function (index) {
4483                    const $link = $(this);
4484                    const href = $link.attr('data-original-href');
4485
4486                    // 检查当前消息是否已经处理过这个链接
4487                    if (messageProcessedLinks.has(href)) {
4488                        return;
4489                    }
4490
4491                    if (href && href.includes('.pdb')) {
4492                        const pdbData = parsePdbUrl(href);
4493                        if (pdbData && pdbData.fileName) {
4494                            // console.log('成功解析PDB数据:', pdbData);
4495                            // 标记该链接在当前消息中已处理
4496                            messageProcessedLinks.add(href);
4497
4498                            // 按文件名分组收集数据
4499                            if (!pdbDataMap.has(pdbData.fileName)) {
4500                                pdbDataMap.set(pdbData.fileName, {
4501                                    fileName: pdbData.fileName,
4502                                    epitopes: [],
4503                                    originalUrls: []
4504                                });
4505                            }
4506
4507                            const fileData = pdbDataMap.get(pdbData.fileName);
4508                            fileData.originalUrls.push(href);
4509
4510                            // 如果有epitope信息,添加到列表中
4511                            if (pdbData.epitopeStart && pdbData.epitopeEnd) {
4512                                fileData.epitopes.push({
4513                                    start: pdbData.epitopeStart,
4514                                    end: pdbData.epitopeEnd,
4515                                    originalUrl: href
4516                                });
4517                            }
4518                        } else {
4519                            console.warn('无法解析PDB链接:', href, '解析结果:', pdbData);
4520                        }
4521                    } else {
4522                        console.warn('链接不包含.pdb或为空:', href);
4523                    }
4524                });
4525
4526                // 为每个唯一的PDB文件渲染3D结构
4527                pdbDataMap.forEach((fileData, fileName) => {
4528                    // 找到第一个相关的链接元素作为插入位置
4529                    const firstLink = $pdbLinks.filter(function () {
4530                        const href = $(this).attr('data-original-href');
4531                        if (href && href.includes('.pdb')) {
4532                            const pdbData = parsePdbUrl(href);
4533                            return pdbData && pdbData.fileName === fileName;
4534                        }
4535                        return false;
4536                    }).first();
4537
4538                    loadPdbAndRender3D(fileData, messageId, firstLink.length > 0 ? firstLink : null);
4539                });
4540            } else {
4541                // console.log('当前消息中未找到任何PDB链接');
4542            }
4543        }
4544
4545        /**
4546         * 解析PDB URL,将整个URL作为fileName参数
4547         * @param {string} url - PDB链接
4548         * @returns {Object} 包含fileName的对象
4549         */
4550        function parsePdbUrl(url) {
4551            try {
4552                // 检查URL是否有效
4553                if (!url || typeof url !== 'string') {
4554                    console.warn('无效的URL:', url);
4555                    return null;
4556                }
4557
4558                // 解析URL参数
4559                const urlObj = new URL(url);
4560                const fileName = urlObj.pathname; // 获取路径部分作为fileName
4561                const epitopeStart = urlObj.searchParams.get('epitopeStart');
4562                const epitopeEnd = urlObj.searchParams.get('epitopeEnd');
4563
4564                const result = {
4565                    fileName: fileName,
4566                    originalUrl: url
4567                };
4568
4569                // 如果有epitope参数,添加到结果中
4570                if (epitopeStart && epitopeEnd) {
4571                    result.epitopeStart = parseInt(epitopeStart);
4572                    result.epitopeEnd = parseInt(epitopeEnd);
4573                    // console.log('解析到epitope区段:', { start: result.epitopeStart, end: result.epitopeEnd });
4574                }
4575
4576                // console.log('解析PDB URL成功:', result);
4577                return result;
4578            } catch (error) {
4579                // console.error('解析PDB URL失败:', error, 'URL:', url);
4580                return null;
4581            }
4582        }
4583
4584        /**
4585         * 从容器ID中提取文件名
4586         * @param {string} containerId - 容器ID,格式:pdb3d-${messageId}-${linkHash}
4587         * @returns {string|null} 文件名
4588         */
4589        function extractFileNameFromContainerId(containerId) {
4590            try {
4591                // 容器ID格式:pdb3d-${messageId}-${linkHash}
4592                // 我们需要从linkHash中解码出原始的文件名
4593                const parts = containerId.split('-');
4594                if (parts.length >= 3) {
4595                    const linkHash = parts.slice(2).join('-');
4596                    // 尝试解码base64编码的hash
4597                    try {
4598                        const decoded = atob(linkHash);
4599                        const fileName = decodeURIComponent(decoded);
4600                        // console.log('从容器ID提取文件名:', fileName);
4601                        return fileName;
4602                    } catch (e) {
4603                        // console.warn('无法解码容器ID中的文件名:', containerId);
4604                        return null;
4605                    }
4606                }
4607                return null;
4608            } catch (error) {
4609                console.error('提取文件名失败:', error, 'containerId:', containerId);
4610                return null;
4611            }
4612        }
4613
4614        /**
4615         * 加载PDB文件并渲染3D结构
4616         * @param {Object} pdbData - PDB数据对象,支持多个epitope区段
4617         * @param {string} messageId - 消息ID
4618         * @param {jQuery} $linkElement - PDB链接元素(可选)
4619         */
4620        function loadPdbAndRender3D(pdbData, messageId, $linkElement) {
4621            // 验证输入参数
4622            if (!pdbData || !pdbData.fileName) {
4623                console.error('loadPdbAndRender3D: 无效的pdbData参数', pdbData);
4624                return;
4625            }
4626
4627            if (!messageId) {
4628                console.error('loadPdbAndRender3D: 缺少messageId参数');
4629                return;
4630            }
4631
4632            const { fileName, epitopes = [] } = pdbData;
4633
4634            // 生成基于链接的唯一容器ID
4635            const linkHash = btoa(encodeURIComponent(fileName)).replace(/[^a-zA-Z0-9]/g, '').substring(0, 8);
4636            const containerId = `pdb3d-${messageId}-${linkHash}`;
4637
4638            const $message = $(`#${messageId}`);
4639
4640            // 检查消息容器是否存在
4641            if (!$message.length) {
4642                console.error('loadPdbAndRender3D: 未找到消息容器', messageId);
4643                return;
4644            }
4645
4646            // 检查是否已经存在相同的3D容器
4647            if ($message.find(`#${containerId}`).length > 0) {
4648                // console.log('3D容器已存在,跳过创建:', containerId);
4649                return;
4650            }
4651
4652            // 额外检查:如果已经存在相同文件名的容器,也跳过创建
4653            const existingContainers = $message.find('.pdb-3d-container');
4654            for (let i = 0; i < existingContainers.length; i++) {
4655                const existingId = $(existingContainers[i]).attr('id');
4656                if (existingId && existingId.includes(fileName.replace(/[^a-zA-Z0-9]/g, ''))) {
4657                    // console.log('相同文件名的3D容器已存在,跳过创建:', fileName);
4658                    return;
4659                }
4660            }
4661
4662            const containerHtml = `
4663                                                                                    <div id="${containerId}" class="pdb-3d-container mt-3">
4664                                                                                        <div class="mol-container" style="width: 100%; height: 400px; background: black; border-radius: 4px;"></div>
4665                                                                                    </div>
4666                                                                                `;
4667
4668
4669
4670            // 将3D容器添加到指定位置
4671            if ($linkElement && $linkElement.length) {
4672                // 在链接后面插入3D容器
4673                $linkElement.after(containerHtml);
4674                // 标记该链接已渲染
4675                $linkElement.data('pdb-rendered', true);
4676            } else {
4677                // 如果没有指定链接元素,添加到content-body末尾
4678                $message.find('.content-body').append(containerHtml);
4679            }
4680
4681            // 标记消息已有3D容器
4682            $message.data('has-3d-containers', true);
4683
4684            // 显示加载状态
4685            $(`#${containerId} .mol-container`).html(`
4686                                                                                     <div class="text-center text-white p-4">
4687                                                                                         <i class="fa fa-spinner fa-spin fa-2x mb-2"></i>
4688                                                                                         <p>loading 3D model...</p>
4689                                                                                     </div>
4690                                                                                 `);
4691
4692
4693            // 请求PDB文件,将整个URL作为fileName参数
4694            fetch(`https://www.ptglab.com/api/pdbfile?fileName=${encodeURIComponent(fileName)}`)
4695                .then(response => {
4696                    if (!response.ok) {
4697                        throw new Error(`HTTP错误,状态码: ${response.status}`);
4698                    }
4699                    return response.text();
4700                })
4701                .then(pdbData => {
4702                    if (!pdbData || pdbData.trim() === '') {
4703                        throw new Error('PDB文件内容为空');
4704                    }
4705
4706                    // 添加短暂延迟,确保DOM元素完全准备好
4707                    setTimeout(() => {
4708                        // 传递epitopes信息给渲染函数
4709                        const epitopesInfo = epitopes.length > 0 ? epitopes : null;
4710                        renderPdbStructure(containerId, pdbData, epitopesInfo);
4711                    }, 200);
4712                })
4713                .catch(error => {
4714                    console.error('加载PDB文件失败:', error);
4715                    $(`#${containerId} .mol-container`).html(`
4716                                                                                            <div class="text-center text-white p-4">
4717                                                                                                <i class="fa fa-exclamation-triangle fa-2x mb-2"></i>
4718                                                                                                <p>Error</p>
4719                                                                                                <p class="small">${error.message}</p>
4720                                                                                                <button class="btn btn-sm btn-outline-light mt-2" onclick="retryLoadPdb('${containerId}', '${fileName}', ${epitopes.length > 0 ? JSON.stringify(epitopes) : 'null'})">
4721                                                                                                    <i class="fa fa-refresh"></i> try
4722                                                                                                </button>
4723                                                                                            </div>
4724                                                                                         `);
4725                });
4726        }
4727
4728        /**
4729         * 渲染PDB结构到3D容器
4730         * @param {string} containerId - 容器ID
4731         * @param {string} pdbData - PDB文件数据
4732         * @param {Array} epitopesInfo - epitope区段数组,每个元素包含start、end、originalUrl
4733         */
4734        function renderPdbStructure(containerId, pdbData, epitopesInfo = null) {
4735            try {
4736                // 初始化3Dmol查看器
4737                const element = document.querySelector(`#${containerId} .mol-container`);
4738                if (!element) {
4739                    console.error('未找到3D容器元素:', containerId);
4740                    return;
4741                }
4742
4743                // 检查容器是否已经被其他3D模型占用
4744                if (element.viewer) {
4745                    console.log('容器已有viewer实例,跳过渲染:', containerId);
4746                    return;
4747                }
4748
4749                // console.log(element.innerHTML, "12212");
4750                // 清空容器内容,确保干净的状态
4751                element.innerHTML = '';
4752
4753                const config = { backgroundColor: 'black' };
4754                const viewer = $3Dmol.createViewer(element, config);
4755
4756                // 将viewer实例保存到容器中,以便后续操作
4757                element.viewer = viewer;
4758                // 添加PDB模型
4759                viewer.addModel(pdbData, 'pdb');
4760
4761                // 设置默认样式
4762                viewer.setStyle(
4763                    { chain: 'A' },
4764                    { cartoon: { color: '#f7f7f7', width: 1.2 } }
4765                );
4766
4767                // 如果有epitope区段信息,添加高亮显示
4768                if (epitopesInfo && epitopesInfo.length > 0) {
4769                    // 定义不同颜色用于区分多个区段
4770                    const colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'];
4771                    // 为每个epitope区段设置不同颜色的高亮
4772                    epitopesInfo.forEach((epitope, index) => {
4773                        const color = colors[index % colors.length];
4774                        // 为当前epitope区段设置高亮
4775                        viewer.setStyle(
4776                            {
4777                                chain: 'A',
4778                                resi: Array.from({ length: epitope.end - epitope.start + 1 }, (_, i) => epitope.start + i)
4779                            },
4780                            {
4781                                cartoon: { color: color, width: 2.0 }, // 不同颜色,更粗
4782                                stick: { color: color, radius: 0.3 }    // 不同颜色球棍模型
4783                            }
4784                        );
4785                    });
4786
4787                    // 解析PDB数据,找到具体原子坐标
4788                    const lines = pdbData.split('\n');
4789                    const atomMap = new Map(); // 存储所有原子的坐标信息
4790
4791                    // 收集所有原子坐标
4792                    for (const line of lines) {
4793                        if (line.startsWith('ATOM')) {
4794                            const resSeq = parseInt(line.substring(22, 26).trim(), 10);
4795                            const resName = line.substring(17, 20).trim(); // 残基名称
4796                            const x = parseFloat(line.substring(30, 38).trim());
4797                            const y = parseFloat(line.substring(38, 46).trim());
4798                            const z = parseFloat(line.substring(46, 54).trim());
4799
4800                            atomMap.set(resSeq, { x, y, z, resSeq, resName });
4801                        }
4802                    }
4803
4804                    // 为每个epitope区段添加标签
4805                    epitopesInfo.forEach((epitope, index) => {
4806                        const color = colors[index % colors.length];
4807
4808                        // 添加区段标签
4809                        const centerResidue = Math.floor((epitope.start + epitope.end) / 2);
4810                        viewer.addLabel(
4811                            `Epitope ${index + 1}: ${epitope.start}-${epitope.end}`,
4812                            {
4813                                position: { chain: 'A', resi: centerResidue },
4814                                backgroundColor: color,
4815                                fontColor: 'white',
4816                                fontSize: 12,
4817                                borderThickness: 1,
4818                                borderColor: 'white',
4819                                showBackground: true
4820                            }
4821                        );
4822
4823                        // 添加起始和结束位置的标签
4824                        const startAtom = atomMap.get(epitope.start);
4825                        const endAtom = atomMap.get(epitope.end);
4826
4827                        if (startAtom) {
4828                            viewer.addLabel(
4829                                `${startAtom.resName} ${startAtom.resSeq}`,
4830                                {
4831                                    position: { x: startAtom.x, y: startAtom.y, z: startAtom.z },
4832                                    fontColor: color,
4833                                    fontSize: 14,
4834                                    backgroundOpacity: 0,
4835                                    font: "Arial Bold"
4836                                }
4837                            );
4838                        }
4839
4840                        if (endAtom) {
4841                            viewer.addLabel(
4842                                `${endAtom.resName} ${endAtom.resSeq}`,
4843                                {
4844                                    position: { x: endAtom.x, y: endAtom.y, z: endAtom.z },
4845                                    fontColor: color,
4846                                    fontSize: 14,
4847                                    backgroundOpacity: 0,
4848                                    font: "Arial Bold"
4849                                }
4850                            );
4851                        }
4852                    });
4853                }
4854
4855                // 设置背景色并渲染
4856                viewer.setBackgroundColor('black');
4857                viewer.zoomTo();
4858                viewer.render();
4859                // 添加旋转控制
4860                add3DControls(viewer, containerId);
4861
4862            } catch (error) {
4863                console.error('渲染3D结构失败:', error, 'containerId:', containerId);
4864                const element = document.querySelector(`#${containerId} .mol-container`);
4865                if (element) {
4866                    element.innerHTML = `
4867                                                                                            <div class="text-center text-white p-4">
4868                                                                                                <i class="fa fa-exclamation-triangle"></i>
4869                                                                                                <p>渲染3D结构失败: ${error.message}</p>
4870                                                                                                <button class="btn btn-sm btn-outline-light mt-2" onclick="retryLoadPdb('${containerId}', '${pdbData}', null)">
4871                                                                                                    <i class="fa fa-refresh"></i> 重试
4872                                                                                                </button>
4873                                                                                            </div>
4874                                                                                        `;
4875                }
4876            }
4877        }
4878
4879        /**
4880         * 添加3D控制功能
4881         * @param {Object} viewer - 3Dmol查看器实例
4882         * @param {string} containerId - 容器ID
4883         */
4884        function add3DControls(viewer, containerId) {
4885            const container = document.querySelector(`#${containerId} .mol-container`);
4886            let isDragging = false;
4887            let lastX = 0;
4888            let lastY = 0;
4889
4890            // 鼠标控制
4891            container.addEventListener('mousedown', function (event) {
4892                isDragging = true;
4893                lastX = event.clientX;
4894                lastY = event.clientY;
4895            });
4896
4897            container.addEventListener('mousemove', function (event) {
4898                if (isDragging) {
4899                    const dx = event.clientX - lastX;
4900                    const dy = event.clientY - lastY;
4901                    viewer.rotate(dy * 0.5, dx * 0.5);
4902                    lastX = event.clientX;
4903                    lastY = event.clientY;
4904                }
4905            });
4906
4907            container.addEventListener('mouseup', function () {
4908                isDragging = false;
4909            });
4910
4911            // 滚轮缩放
4912            container.addEventListener('wheel', function (event) {
4913                event.preventDefault();
4914                const scale = event.deltaY > 0 ? 0.9 : 1.1;
4915                viewer.zoom(scale);
4916            });
4917        }
4918
4919        /**
4920         * 切换3D视图显示/隐藏
4921         * @param {string} containerId - 容器ID
4922         */
4923        function toggle3DView(containerId) {
4924            const container = document.getElementById(containerId);
4925            if (container) {
4926                if (container.style.display === 'none') {
4927                    container.style.display = 'block';
4928                } else {
4929                    container.style.display = 'none';
4930                }
4931            }
4932        }
4933
4934        /**
4935         * 重置3D视图
4936         * @param {string} containerId - 容器ID
4937         */
4938        function reset3DView(containerId) {
4939            const container = document.querySelector(`#${containerId} .mol-container`);
4940            if (container && container.viewer) {
4941                container.viewer.zoomTo();
4942                container.viewer.render();
4943            }
4944        }
4945
4946        /**
4947         * 重试加载PDB文件
4948         * @param {string} containerId - 容器ID
4949         * @param {string} fileName - 文件名
4950         * @param {Array} epitopesInfo - epitope区段数组
4951         */
4952        function retryLoadPdb(containerId, fileName, epitopesInfo = null) {
4953            // 显示加载状态
4954            $(`#${containerId} .mol-container`).html(`
4955                                                                                     <div class="text-center text-white p-4">
4956                                                                                         <i class="fa fa-spinner fa-spin fa-2x mb-2"></i>
4957                                                                                         <p>loading...</p>
4958                                                                                     </div>
4959                                                                                 `);
4960
4961            // 重新请求PDB文件
4962            fetch(`https://www.ptglab.com/api/pdbfile?fileName=${encodeURIComponent(fileName)}`)
4963                .then(response => {
4964                    if (!response.ok) {
4965                        throw new Error(`HTTP错误,状态码: ${response.status}`);
4966                    }
4967                    return response.text();
4968                })
4969                .then(pdbData => {
4970                    if (!pdbData || pdbData.trim() === '') {
4971                        throw new Error('PDB文件内容为空');
4972                    }
4973                    renderPdbStructure(containerId, pdbData, epitopesInfo);
4974                })
4975                .catch(error => {
4976                    console.error('重试加载PDB文件失败:', error);
4977                    $(`#${containerId} .mol-container`).html(`
4978                                                                                            <div class="text-center text-white p-4">
4979                                                                                                <i class="fa fa-exclamation-triangle fa-2x mb-2"></i>
4980                                                                                                <p>error</p>
4981                                                                                                <p class="small">${error.message}</p>
4982                                                                                                <button class="btn btn-sm btn-outline-light mt-2" onclick="retryLoadPdb('${containerId}', '${fileName}', ${epitopesInfo ? JSON.stringify(epitopesInfo) : 'null'})">
4983                                                                                                    <i class="fa fa-refresh"></i> try again
4984                                                                                                </button>
4985                                                                                            </div>
4986                                                                                         `);
4987                });
4988        }
4989        // 事件监听(保留原逻辑)
4990        $('#attachFileBtn').on('click', function () {
4991            if (isLoading) return;
4992            $('#fileInput').click();
4993        });
4994        $('#fileInput').on('change', function (e) {
4995            const file = e.target.files && e.target.files[0];
4996            if (!file) return;
4997            handleFileSelect(file);
4998        });
4999
5000        let fileDropCounter = 0;
5001        const $chatInputWrapper = $('#chatInputWrapper');
5002        const $chatInputDropzone = $('#chatInputDropzone');
5003        $chatInputWrapper.on('dragenter', function (e) {
5004            e.preventDefault();
5005            e.stopPropagation();
5006            if (isLoading) return;
5007            fileDropCounter++;
5008            $chatInputDropzone.addClass('drag-over');
5009        });
5010        $chatInputWrapper.on('dragleave', function (e) {
5011            e.preventDefault();
5012            e.stopPropagation();
5013            fileDropCounter--;
5014            if (fileDropCounter <= 0) {
5015                fileDropCounter = 0;
5016                $chatInputDropzone.removeClass('drag-over');
5017            }
5018        });
5019        $chatInputWrapper.on('dragover', function (e) {
5020            e.preventDefault();
5021            e.stopPropagation();
5022        });
5023        $chatInputWrapper.on('drop', function (e) {
5024            e.preventDefault();
5025            e.stopPropagation();
5026            fileDropCounter = 0;
5027            $chatInputDropzone.removeClass('drag-over');
5028            const file = getFileFromDataTransfer(e.originalEvent.dataTransfer);
5029            attemptAttachFile(file, false);
5030        });
5031
5032        $('#chatInputWrapper').on('paste', function (e) {
5033            const file = getFileFromClipboardEvent(e);
5034            if (!file) return;
5035            e.preventDefault();
5036            attemptAttachFile(file, true);
5037        });
5038
5039
5040        $('#removeFileBtn').on('click', function () {
5041            clearPendingFile();
5042            userInput.focus();
5043        });
5044        // 输入框随内容增高,最多 5 行后出现滚动条
5045        function autoResizeUserInput() {
5046            const el = userInput[0];
5047            if (!el) return;
5048            el.style.height = 'auto';
5049            const maxHeight = parseFloat(window.getComputedStyle(el).maxHeight) || el.scrollHeight;
5050            el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px';
5051        }
5052        userInput.on('input', autoResizeUserInput);
5053
5054        // 事件监听(保留原逻辑)
5055        sendBtn.on('click', sendMessage);
5056        userInput.on('keydown', function (e) {
5057            if (e.which === 13 && !e.shiftKey) {
5058                e.preventDefault();
5059                sendMessage();
5060            }
5061        });
5062        clearBtn.on('click', clearMessages);
5063        $(document).on('keydown', function (e) {
5064            if (e.key === 'Escape' && isLoading) {
5065                abortRequest(true);
5066                isLoading = false;
5067                $('#sendBtn').removeClass('loading');
5068                $('#sendBtn').off('click').on('click', sendMessage);
5069                $('.loading-indicator, .load-more-indicator').remove();
5070            }
5071        });
5072        userInput.focus();
5073
5074
5075        // 监听来自父页面的 ableChat 消息(搜索结果页 "Chat with Able" 按钮)
5076        window.addEventListener('message', function (event) {
5077            var data = event.data;
5078            if (data && data.type === 'ableChat' && data.keyword) {
5079                console.log(data.keyword, "data.keyword");
5080
5081                // 先延迟300ms,让 focusInput 等消息先处理完
5082                setTimeout(function () {
5083                    // 等历史记录加载完成后再发送,避免被 renderConversationHistory 清空
5084                    function waitAndSend(keyword, retries) {
5085                        var stillLoading = $('#restoreLoading').length > 0 || $('#sendBtn').prop('disabled');
5086                        if (stillLoading && retries > 0) {
5087                            setTimeout(function () { waitAndSend(keyword, retries - 1); }, 300);
5088                            return;
5089                        }
5090                        // 加载完成(或超时),执行发送
5091                        $(".able-intro").css("display", "none");
5092                        $('#userInput').val(keyword);
5093                        $('#sendBtn').prop('disabled', false);
5094                        isLoading = false;
5095                        sendMessage();
5096                    }
5097
5098                    // 最多等 30 次 × 300ms = 9秒
5099                    waitAndSend(data.keyword, 30);
5100                }, 300);
5101            }
5102        });
5103
5104
5105        function collapseThinkContent(messageId) {
5106            const $message = $(`#${messageId}`);
5107            const $thinkContent = $message.find('.think-content-body');
5108            if ($thinkContent.length) {
5109                $thinkContent.slideUp(300, function () {
5110                    $thinkContent.addClass('think-collapsed');
5111                    $message.find('.think-toggle-icon')
5112                        .removeClass('fa-chevron-down')
5113                        .addClass('fa-chevron-right');
5114                });
5115            }
5116        }
5117
5118        // 【关键删除】废弃旧的思考内容处理函数(不再使用)
5119        // function displayThinkContet(messageId) { ... }
5120
5121        // MCP相关函数(保留原逻辑)
5122        function fetchMcpHostResult(uuid, messageId) {
5123            fetch(`https://aipublic.ptglab.com/llm/v1/mcp/result-list/${uuid}`, { method: 'GET' })
5124                .then(response => {
5125                    if (!response.ok) throw new Error(`HTTP错误,状态码: ${response.status}`);
5126                    return response.json();
5127                })
5128                .then(data => {
5129                    handleApiData(messageId, data);
5130                    resultList = data?.data;
5131                    // if (data?.data && Array.isArray(data.data) && data.data.length > 0) {
5132                    //     const $rightPanel = $('#rightPanel');
5133                    //     if (!$rightPanel.hasClass('slide-in')) {
5134                    //         // $('#openRight').trigger('click');
5135                    //         $('#rightPanel').removeClass('right-panel slide-out').addClass('slide-in');
5136                    //         $('#app').removeClass('mx-auto').removeClass("max-w-7xl").css('width', '75%');
5137                    //     }
5138                    //     setTimeout(() => scrollToBottom(true), 300);
5139                    // }
5140                })
5141                .catch(error => console.error('请求MCP_HOST_RESULT接口失败:', error));
5142        }
5143
5144        function handleApiData(messageId, apiData, userText) {
5145            if (!Array.isArray(apiData?.data)) {
5146                console.warn('数据格式错误:', apiData);
5147                return;
5148            }
5149            messageGroups.clear();
5150            collapseStates.clear();
5151            apiData.data.forEach(item => {
5152                const roundId = item.round_id;
5153                const parsedArgs = parseArgs(item.args);
5154                const messageItem = { ...item, parsedArgs };
5155                if (messageGroups.has(roundId)) {
5156                    messageGroups.get(roundId).messages.push(messageItem);
5157                } else {
5158                    messageGroups.set(roundId, { roundId, messages: [messageItem], count: 1 });
5159                    collapseStates.set(roundId, roundId !== messageId);
5160                }
5161            });
5162            renderGroupedContent();
5163        }
5164
5165        function renderGroupedContent() {
5166            const $container = $('#scrollableContent');
5167            $container.empty();
5168            messageGroups.forEach((group, roundId) => {
5169                const { messages } = group;
5170                const isCollapsed = collapseStates.get(roundId);
5171                const targetItem = userChat.find(item => item.round_id === roundId);
5172                const content = targetItem?.content;
5173                const $group = $(`
5174                                                                                            <div class="group-wrapper border border-gray-200 rounded-lg mb-4">
5175                                                                                                <div class="group-header flex justify-between items-center p-3 bg-gray-50 cursor-pointer">
5176                                                                                                    <h3 class="font-semibold tooltip-trigger" data-full-content="${content || ''}">
5177                                                                                                        ${content || '未知请求'}
5178                                                                                                    </h3>
5179                                                                                                    <i class="fa ${isCollapsed ? 'fa-chevron-down' : 'fa-chevron-up'}"></i>
5180                                                                                                </div>
5181                                                                                                <div class="group-content ${isCollapsed ? 'hidden' : ''} p-3 border-t">
5182                                                                                                </div>
5183                                                                                            </div>
5184                                                                                        `);
5185                const $content = $group.find('.group-content');
5186                messages.forEach((msg) => {
5187                    const $message = $(`
5188                                                                                                <div class="message-item border border-gray-100 rounded mb-3" data-id=${msg.id}>
5189                                                                                                    <div class="args-container"></div>
5190                                                                                                </div>
5191                                                                                            `);
5192                    renderArgs($message.find('.args-container'), msg.parsedArgs, msg.tool);
5193                    $content.append($message);
5194                });
5195                $group.find('.group-header').on('click', function () {
5196                    const newState = !isCollapsed;
5197                    collapseStates.set(roundId, newState);
5198                    $(this).find('i').toggleClass('fa-chevron-down fa-chevron-up');
5199                    $(this).next('.group-content').toggleClass('hidden');
5200                });
5201                const $headerTitle = $group.find('.tooltip-trigger');
5202                $headerTitle
5203                    .off('touchstart touchend touchleave mouseenter mouseleave')
5204                    .on('touchstart', function (e) { e.preventDefault(); showTooltip(this); })
5205                    .on('touchend touchleave', hideTooltip)
5206                    .on('mouseenter', () => showTooltip($headerTitle[0]))
5207                    .on('mouseleave', hideTooltip);
5208                $container.append($group);
5209            });
5210            $('#scrollableContent')
5211                .off('click', '.message-item')
5212                .on('click', '.message-item', clickTab);
5213        }
5214
5215        function clickTab() {
5216            const queryString = $(this).data("id");
5217            const matchingItems = resultList.filter(item => item.id == queryString);
5218            const args = matchingItems.length > 0 ? matchingItems[0].args : "";
5219            let jumpParams = convertJsonToQuery(args);
5220            window.open(`https://ptgcn.com/results?g=&${jumpParams}`, '_blank');
5221        }
5222
5223        function parseArgs(argsStr) {
5224            try {
5225                return argsStr ? JSON.parse(argsStr) : {};
5226            } catch (e) {
5227                console.error('解析args失败:', e);
5228                return { error: '解析失败' };
5229            }
5230        }
5231
5232        function renderArgs($container, argsObj, toolName) {
5233            if (toolName) {
5234                const $toolInfo = $(`
5235                                                                                            <div class="p-2 text-sm font-medium border-b border-gray-100">
5236                                                                                                Tool: ${toolName}
5237                                                                                            </div>
5238                                                                                        `);
5239                $container.append($toolInfo);
5240            }
5241            if (!argsObj || !Object.keys(argsObj).length) {
5242                $container.html('<div class="text-sm text-gray-500 p-2">无参数</div>');
5243                return;
5244            }
5245            const $panel = $(`
5246                                                                                        <div class="p-2 text-sm">
5247                                                                                            ${Object.entries(argsObj).map(([k, v]) => `
5248                                                                                                <div><span class="font-medium">${k}:</span> ${Array.isArray(v) ? v.join(', ') : v}</div>
5249                                                                                            `).join('')}
5250                                                                                        </div>
5251                                                                                    `);
5252            $container.append($panel);
5253        }
5254
5255        function convertJsonToQuery(jsonStr) {
5256            try {
5257                const data = JSON.parse(jsonStr);
5258                const queryParams = [];
5259                function processArray(key, values) {
5260                    values.forEach(value => queryParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`));
5261                }
5262                function processValue(key, value) {
5263                    queryParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
5264                }
5265                Object.entries(data).forEach(([key, value]) => {
5266                    Array.isArray(value) ? processArray(key, value) : processValue(key, value);
5267                });
5268                return queryParams.join('%26');
5269            } catch (error) {
5270                console.error('JSON解析错误:', error);
5271                return '';
5272            }
5273        }
5274
5275
5276        function normalizeMermaidCode(raw) {
5277            let code = (raw || '').trim();
5278            code = code.replace(/&gt;/g, '>').replace(/&lt;/g, '<');
5279            code = code.replace(/^graph LR/gm, 'graph TB');
5280            return code;
5281        }
5282
5283        // 流式过程中 mermaid 代码可能不完整,不完整时不渲染、不显示 error
5284        function isMermaidCodeLikelyComplete(code) {
5285            if (!code || !code.trim()) return false;
5286            const lines = code.split('\n').map(l => l.trim()).filter(Boolean);
5287            if (lines.length < 2) return false;
5288            const lastLine = lines[lines.length - 1];
5289            if (/(-->|---|-\.-|==>|:::|\[|\(|\{|--)$/.test(lastLine)) return false;
5290            if (/^[A-Za-z0-9_]+\s*(-->|---|-\.-)/.test(lastLine) && !/(-->|---|-\.-).+[A-Za-z0-9_\[\(]/.test(lastLine)) {
5291                return false;
5292            }
5293            return true;
5294        }
5295
5296        function restoreMermaidSourceNode(preNode, code) {
5297            if (!preNode || !preNode.parentNode) return;
5298            const restorePre = document.createElement('pre');
5299            const restoreCode = document.createElement('code');
5300            restoreCode.className = 'language-mermaid';
5301            restoreCode.textContent = code;
5302            restorePre.appendChild(restoreCode);
5303            preNode.parentNode.replaceChild(restorePre, preNode);
5304        }
5305
5306        function applyGroupLinkFormatting($group) {
5307            if (!$group || !$group.length) return;
5308            $group.find('a:not([target="_blank"])').attr('target', '_blank');
5309        }
5310
5311        // DOM 全量重建后,把已缓存的 SVG 贴回去(不触发 mermaid.render)
5312        function reinjectCachedMermaidInContainer(messageId, buffer, $container) {
5313            if (!buffer?.renderedMermaidCache || !$container?.length) return;
5314            const targetDom = $container[0];
5315            targetDom.querySelectorAll('pre:not([data-mermaid-rendered="success"]) > code.language-mermaid').forEach(codeEl => {
5316                const cacheKey = normalizeMermaidCode(codeEl.textContent);
5317                const svg = buffer.renderedMermaidCache[cacheKey];
5318                if (!svg) return;
5319                const originalPreEl = codeEl.parentElement;
5320                if (!originalPreEl || originalPreEl.tagName !== 'PRE') return;
5321                const cachedPreEl = document.createElement('pre');
5322                cachedPreEl.className = 'mermaid';
5323                cachedPreEl.dataset.mermaidRendered = 'success';
5324                cachedPreEl.dataset.mermaidCode = cacheKey;
5325                cachedPreEl.innerHTML = svg;
5326                originalPreEl.parentNode.replaceChild(cachedPreEl, originalPreEl);
5327            });
5328        }
5329
5330        // 流式结束:补扫仍未渲染的 mermaid
5331        function sweepUnrenderedMermaid(messageId, $container) {
5332            if (!$container?.length) return;
5333            $container.find('.segment-group').each(function () {
5334                if (this.querySelector('pre:not([data-mermaid-rendered="success"]) > code.language-mermaid')) {
5335                    initMermaidRender(this, { messageId, skipIncomplete: false });
5336                }
5337            });
5338        }
5339
5340        function markGroupForFormat(buffer, groupId, reason) {
5341            if (!buffer || !groupId) return;
5342            if (!buffer.pendingFormatGroupIds) buffer.pendingFormatGroupIds = {};
5343            buffer.pendingFormatGroupIds[groupId] = reason || 'unknown';
5344        }
5345
5346        function formatPendingGroups(messageId, buffer, $container) {
5347            if (!buffer?.pendingFormatGroupIds || !$container?.length) return;
5348            const pendingIds = Object.keys(buffer.pendingFormatGroupIds);
5349            if (!pendingIds.length) return;
5350
5351            pendingIds.forEach(groupId => {
5352                const reason = buffer.pendingFormatGroupIds[groupId];
5353                const $groups = $container.find(`.segment-group[data-group-id="${groupId}"]`);
5354                if (!$groups.length) {
5355                    return;
5356                }
5357
5358
5359                $groups.each(function () {
5360                    applyGroupLinkFormatting($(this));
5361                    initMermaidRender(this, { messageId, skipIncomplete: false });
5362                });
5363
5364                delete buffer.pendingFormatGroupIds[groupId];
5365            });
5366        }
5367
5368        function formatAllNonContentGroupsAtStreamEnd(messageId, buffer, $container) {
5369            if (!buffer?.nonContentGroups?.length) return;
5370            buffer.nonContentGroups.forEach(group => {
5371                markGroupForFormat(buffer, group.id, 'stream-end');
5372            });
5373            formatPendingGroups(messageId, buffer, $container);
5374        }
5375
5376        // reasoning / tool 等无 data-group-id 的段,仅在流式结束时渲染
5377        function formatOrphanSegmentGroups(messageId, $container) {
5378            if (!$container?.length) return;
5379            const $orphans = $container.find('.segment-group:not([data-group-id])');
5380            if (!$orphans.length) return;
5381
5382
5383            $orphans.each(function () {
5384                applyGroupLinkFormatting($(this));
5385                initMermaidRender(this, { messageId, skipIncomplete: false });
5386            });
5387        }
5388
5389        //展示 flow chart
5390        function initMermaidRender(container = document, options = {}) {
5391            const { messageId = null, skipIncomplete = false } = options;
5392            const buffer = messageId ? responseBuffers[messageId] : null;
5393            // 核心逻辑包裹在 try-catch 中,捕获所有异常
5394            try {
5395                // 2. 统一转换为原生DOM(兼容jQuery对象/原生DOM),限定选择器作用域
5396                const targetDom = container instanceof jQuery ? container[0] : container;
5397
5398                // 防御性判断:确保 targetDom 是有效DOM元素,避免 querySelectorAll 报错
5399                if (!targetDom || !(targetDom instanceof HTMLElement)) {
5400                    return; // 提前退出,不影响后续
5401                }
5402
5403                // 已成功渲染的跳过;未渲染的 code.language-mermaid 才处理
5404                const mermaidCodeElements = targetDom.querySelectorAll('pre:not([data-mermaid-rendered="success"]) > code.language-mermaid');
5405
5406
5407
5408                // 关键修改1:如果没有需要渲染的mermaid元素,直接退出
5409                if (mermaidCodeElements.length === 0) {
5410                    return;
5411                }
5412
5413                // 存储新创建的 mermaid 节点,用于后续手动渲染
5414                const newMermaidNodes = [];
5415
5416                // 3. 遍历每个元素,先完成节点改造(提前处理节点结构,后续渲染用)
5417                mermaidCodeElements.forEach(codeEl => {
5418                    // 内层也增加轻量防御,避免单个元素处理失败导致整个遍历中断
5419                    try {
5420                        // 3.1 获取父节点(原始的 pre 标签)
5421                        const originalPreEl = codeEl.parentElement;
5422                        if (!originalPreEl || originalPreEl.tagName !== 'PRE') return;
5423                        if (originalPreEl.dataset.mermaidRendered === 'success') return;
5424
5425                        const mermaidCode = normalizeMermaidCode(codeEl.textContent);
5426                        const cacheKey = mermaidCode;
5427
5428                        // 命中缓存:直接注入 SVG,不再调用 mermaid.render
5429                        if (buffer && buffer.renderedMermaidCache && buffer.renderedMermaidCache[cacheKey]) {
5430                            const cachedPreEl = document.createElement('pre');
5431                            cachedPreEl.className = 'mermaid';
5432                            cachedPreEl.dataset.mermaidRendered = 'success';
5433                            cachedPreEl.dataset.mermaidCode = cacheKey;
5434                            cachedPreEl.innerHTML = buffer.renderedMermaidCache[cacheKey];
5435                            originalPreEl.parentNode.replaceChild(cachedPreEl, originalPreEl);
5436                            return;
5437                        }
5438
5439                        // 流式未结束且代码不完整:不动 DOM,等下次 chunk
5440                        if (skipIncomplete && !isMermaidCodeLikelyComplete(mermaidCode)) {
5441                            return;
5442                        }
5443
5444                        // 同一代码正在异步渲染中,跳过
5445                        if (buffer && buffer.pendingMermaidKeys && buffer.pendingMermaidKeys[cacheKey]) {
5446                            return;
5447                        }
5448
5449                        // 3.3 创建新的 pre 标签(Mermaid 识别的容器)
5450                        const newPreEl = document.createElement('pre');
5451                        newPreEl.className = 'mermaid';
5452                        newPreEl.dataset.mermaidCode = mermaidCode;
5453                        newPreEl.dataset.mermaidRendered = 'pending';
5454                        newMermaidNodes.push({ preNode: newPreEl, cacheKey });
5455
5456                        if (buffer) {
5457                            if (!buffer.pendingMermaidKeys) buffer.pendingMermaidKeys = {};
5458                            buffer.pendingMermaidKeys[cacheKey] = true;
5459                        }
5460
5461                        // 3.4 替换原有节点(删除旧的 pre,插入新的 pre)
5462                        originalPreEl.parentNode.replaceChild(newPreEl, originalPreEl);
5463                    } catch (innerError) {
5464                        // 单个元素处理失败,仅打印日志,继续处理下一个元素
5465                    }
5466                });
5467
5468                if (newMermaidNodes.length === 0) {
5469                    return;
5470                }
5471
5472
5473                // 关键修改2:动态加载mermaid JS文件的函数
5474                function loadMermaidScript() {
5475                    return new Promise((resolve, reject) => {
5476                        // 检查是否已加载过mermaid脚本ï¼
5476Œé¿å…é‡å¤åŠ è½½
5477                        const existingScript = document.querySelector('script[src="/scripts/mermaid.min.js"]');
5478                        if (existingScript) {
5479                            // 如果已加载,直接检查window.mermaid是否存在
5480                            if (window.mermaid) {
5481                                resolve();
5482                            } else {
5483                                // 脚本已插入但还没加载完成,监听加载事件
5484                                existingScript.onload = resolve;
5485                                existingScript.onerror = reject;
5486                            }
5487                            return;
5488                        }
5489
5490                        // 创建新的script标签
5491                        const script = document.createElement('script');
5492                        script.src = '/scripts/mermaid.min.js';
5493                        script.type = 'text/javascript';
5494                        script.async = true; // 异步加载,不阻塞页面
5495
5496                        // 加载成功回调
5497                        script.onload = () => {
5498                            if (window.mermaid) {
5499                                resolve();
5500                            } else {
5501                                reject(new Error('Mermaid not find mermaid'));
5502                            }
5503                        };
5504
5505                        // 加载失败回调
5506                        script.onerror = () => {
5507                            reject(new Error(':/scripts/mermaid.min.js'));
5508                        };
5509
5510                        // 插入到head标签中(也可以插入到body)
5511                        document.head.appendChild(script);
5512                    });
5513                }
5514
5515                // 关键修改3:加载脚本后再初始化并渲染
5516                loadMermaidScript()
5517                    .then(() => {
5518                        // 4. 初始化/重新渲染 Mermaid
5519                        mermaid.initialize({
5520                            startOnLoad: false, // 手动触发渲染,避免重复
5521                            theme: 'default',
5522                            flowchart: { nodeSpacing: 15, rankSpacing: 20 }
5523                        });
5524
5525                        // 5. 手动渲染每个新创建的 mermaid 节点
5526                        newMermaidNodes.forEach(async (item, index) => {
5527                            const { preNode, cacheKey } = item;
5528                            try {
5529                                const code = preNode.dataset.mermaidCode || cacheKey;
5530                                const graphId = `mermaid-graph-${Date.now()}-${index}`;
5531                                const { svg } = await mermaid.render(graphId, code);
5532                                if (!preNode.isConnected) {
5533                                    if (buffer) {
5534                                        if (!buffer.renderedMermaidCache) buffer.renderedMermaidCache = {};
5535                                        buffer.renderedMermaidCache[cacheKey] = svg;
5536                                        if (buffer.pendingMermaidKeys) delete buffer.pendingMermaidKeys[cacheKey];
5537                                    }
5538                                    return;
5539                                }
5540                                preNode.innerHTML = svg;
5541                                preNode.dataset.mermaidRendered = 'success';
5542                                if (buffer) {
5543                                    if (!buffer.renderedMermaidCache) buffer.renderedMermaidCache = {};
5544                                    buffer.renderedMermaidCache[cacheKey] = svg;
5545                                    if (buffer.pendingMermaidKeys) delete buffer.pendingMermaidKeys[cacheKey];
5546                                }
5547                            } catch (renderError) {
5548                                if (buffer && buffer.pendingMermaidKeys) delete buffer.pendingMermaidKeys[cacheKey];
5549                                if (preNode.isConnected) {
5550                                    restoreMermaidSourceNode(preNode, preNode.dataset.mermaidCode || cacheKey);
5551                                }
5552                                console.warn('[mermaid] render fail,保留源码不显示 error', cacheKey.slice(0, 60), renderError.message);
5553                            }
5554                        });
5555                    })
5556                    .catch((loadError) => {
5557                        newMermaidNodes.forEach(item => {
5558                            const { preNode, cacheKey } = item;
5559                            if (buffer && buffer.pendingMermaidKeys) delete buffer.pendingMermaidKeys[cacheKey];
5560                            if (preNode.isConnected) {
5561                                restoreMermaidSourceNode(preNode, preNode.dataset.mermaidCode || cacheKey);
5562                            }
5563                        });
5564                        console.error('[mermaid] script load fail', loadError);
5565                    });
5566
5567            } catch (outerError) {
5568                console.error('Mermaid error:', outerError);
5569            }
5570            // 函数执行完毕(无论是否报错),后续方法可正常执行
5571        }
5572
5573
5574        $(document).ready(function () {
5575
5576            // 全局事件委托 - 作为备用方案处理复制图标点击
5577            $(document).on('click', '.copy-icon', function (e) {
5578                e.preventDefault();
5579                e.stopPropagation();
5580                const $copyIcon = $(this);
5581                const $contentBody = $copyIcon.closest('.content-body');
5582                if ($contentBody.length > 0) {
5583                    copyMarkdownContent($contentBody, $copyIcon);
5584                } else {
5585                    console.log("No content-body found for global handler");
5586                }
5587            });
5588
5589            // 全局事件委托 - 处理导出PDF按钮点击
5590            $(document).on('click', '.export-pdf-icon', function (e) {
5591                e.preventDefault();
5592                e.stopPropagation();
5593                const $exportIcon = $(this);
5594                const $contentBody = $exportIcon.closest('.content-body');
5595                if ($contentBody.length > 0) {
5596                    exportContentAsPDF($contentBody, $exportIcon);
5597                } else {
5598                }
5599            });
5600
5601            // 全局事件委托 - 处理重置按钮点击
5602            $(document).on('click', '.reset-icon', function (e) {
5603                e.preventDefault();
5604                e.stopPropagation();
5605                const $resetIcon = $(this);
5606                const messageId = $resetIcon.data('message-id');
5607                if (messageId) {
5608                    regenerateResponse($resetIcon, messageId);
5609                } else {
5610                    console.error("No messageId found for reset handler");
5611                }
5612            });
5613
5614            $(window).on('message', function (event) {
5615                // // 安全验证:只处理来自信任域名的消息
5616                const trustedOrigins = ['https://www.ptglab.com', 'http://www.ptglab.com', 'https://aipublic.ptglab.com', 'https://ptglab.com', 'http://ptglab.com', 'https://ptglab.my.site.com']; // 替换为实际父页面域名
5617                if (!trustedOrigins.includes(event.originalEvent.origin)) {
5618                    console.warn('拒绝处理来自未知域名的消息:', event.originalEvent.origin);
5619                    return;
5620                }
5621
5622                // 解析消息内容
5623                const message = event.originalEvent.data;
5624                if (message.action === 'showAbleIntro') {
5625                    // 执行显示操作:将.able-intro设置为display: block
5626                    clearMessages()
5627                    $('.able-intro').css('display', 'block');
5628                    // 可以添加其他需要执行的操作
5629                }
5630            });
5631
5632
5633            $(".uuid").text(uuid)
5634            if (languageInfo?.primary?.startsWith('zh')) {
5635                language = 'Chinese';
5636                $(".titleLanguage").text('智能对话助手');
5637                $(".clearLanguage").text("清空对话");
5638                $(".welcomeLanguage").text("你好!我是你的AI助手,有什么可以帮助你的吗?");
5639                // $(".inputClearLanguage").text('输入 /clear 可快速清空对话');
5640                $('#userInput').attr('placeholder', '请输入问题');
5641                $(".canText").text('参考内容');
5642            } else {
5643                language = 'English';
5644                $(".titleLanguage").text('AI conversation assistant');
5645                $(".clearLanguage").text("Clear the conversation");
5646                $(".welcomeLanguage").text("Hello! I am your AI assistant, can I help you?");
5647                // $(".inputClearLanguage").text('Disclaimer: Able AI is experimental and still learning how to provide the best assistance. It may occasionally generate incorrect or incomplete responses. Please do not rely solely on its recommendations when making purchasing decisions or designing experiments.');
5648                $('#userInput').attr('placeholder', 'Ask Able to find you the perfect product');
5649                $(".canText").text('Reference content');
5650            }
5651
5652            $("#openRight").click(function () {
5653                $('#rightPanel').removeClass('right-panel slide-out').addClass('slide-in');
5654                $('#app').removeClass('mx-auto').removeClass("max-w-7xl").css('width', '75%');
5655            });
5656
5657            $("#closeBtn").click(function () {
5658                $('#app').addClass('mx-auto').addClass("max-w-7xl").css('width', '100%');
5659                $('#rightPanel').removeClass('slide-in').addClass('slide-out');
5660                setTimeout(function () {
5661                    $('#rightPanel').removeClass('slide-out').addClass('right-panel');
5662                }, 300);
5663            });
5664
5665
5666            $('.open-live-chat').click(function () {
5667                const data = {
5668                    type: 'fromChild',
5669                    message: 'msg',
5670                    status: 'success'
5671                };
5672                window.parent.postMessage(data, '*');
5673            });
5674        });
5675
5676
5677        //主要点击对话新传入参数存储对话时间
5678        document.addEventListener('DOMContentLoaded', function () {
5679            // 监听父窗口的消息
5680            window.addEventListener('message', function (event) {
5681                // 安全验证:只处理来自信任域名的消息
5682                const trustedOrigins = ['https://www.ptglab.com', 'http://www.ptglab.com', 'https://aipublic.ptglab.com', 'https://ptglab.com', 'http://ptglab.com', 'https://ptglab.my.site.com'];
5683                const isValidMessage =
5684                    event.data &&
5685                    (event.data.type === 'ableChat' || event.data.action === 'focusInput');
5686
5687                if (trustedOrigins.includes(event.origin) && isValidMessage) {
5688                    // 1. 定义基础常量(命名清晰,注释准确)
5689                    const currentTimestamp = Date.now(); // 当前时间戳(毫秒)
5690                    const timestampKey = 'chat_uuid_timestamp'; // 基准时间戳存储key
5691                    const chatUuidKey = 'chat_uuid'; // chat_uuid存储key
5692                    const oneHour = 1 * 60 * 60 * 1000; // 1小时的毫秒数
5693
5694                    // 2. 获取本地存储的基准时间戳
5695                    const lastTimestamp = localStorage.getItem(timestampKey);
5696
5697                    // 3. 核心逻辑:
5698                    // - 无基准时间:初始化基准(为后续判断做准备)
5699                    // - 有基准时间:仅1小时内不清除,其余全部清除
5700                    if (!lastTimestamp) {
5701                        // 场景1:无基准时间(首次访问/刚清除)→ 初始化基准时间
5702                        localStorage.setItem(timestampKey, currentTimestamp.toString());
5703                    } else {
5704                        // 场景2:有基准时间 → 判断是否在1小时内
5705                        const lastTime = parseInt(lastTimestamp, 10); // 解析基准时间为数字
5706
5707                        // 核心判断:只要不在1小时内(超时/时间回拨),就清除
5708                        if (!(currentTimestamp >= lastTime && currentTimestamp < lastTime + oneHour)) {
5709                            localStorage.removeItem(chatUuidKey); // 清除chat_uuid
5710                            localStorage.removeItem(timestampKey); // 清除基准时间戳
5711                            localStorage.setItem(timestampKey, currentTimestamp.toString());
5712                        }
5713                        // 1小时内:不执行任何操作,保留数据
5714                    }
5715
5716
5717
5718                    /** 检查并生成 chat_uuid */
5719                    const storedUuid = localStorage.getItem('chat_uuid');
5720
5721
5722                    // 处理 focusInput 操作
5723                    if (event.data && event.data.action === 'focusInput') {
5724                        $('#userInput').focus();
5725                    }
5726
5727                    if (!storedUuid) {
5728                        // 如果 localStorage 中没有 chat_uuid,重新生成一个并存储
5729                        // const newUuid = generateSafeNumericId();
5730                        messagesContainer.children().not('.able-intro').remove();
5731                        // 【新增】清理审核加载状态
5732                        Object.keys(auditLoadingStates).forEach(key => delete auditLoadingStates[key]);
5733                        rebuildChatRoundIds.clear();
5734
5735                        // 【新增】清除localStorage中的uuid,生成新的uuid
5736                        // localStorage.removeItem('chat_uuid');
5737                        generateSafeNumericId()
5738                            .then(id => {
5739                                uuid = id
5740                                localStorage.setItem('chat_uuid', uuid);
5741                                // 后续逻辑
5742                            })
5743                        // 【新增】显示介绍页面
5744                        $(".able-intro").css("display", "block");
5745                    } else {
5746                        messagesContainer.children().not('.able-intro').remove();
5747                        // 如果需要加载历史记录
5748                        if (event.data && event.data.loadHistory && event.data.dialogId) {
5749                            const dialogId = event.data.dialogId;
5750                            const storedId = localStorage.getItem('chat_uuid') || dialogId;
5751                            // 开始显示加载提示
5752                            showConversationRestoreLoading();
5753                            loadConversationHistory(storedId);
5754                        }
5755                        // 兼容旧的消息格式(loadConversationHistory action)
5756                        if (event.data && event.data.action === 'loadConversationHistory') {
5757                            const dialogId = event.data.dialogId || event.data.dialog_id;
5758                            const storedId = localStorage.getItem('chat_uuid') || dialogId;
5759                            if (storedId) {
5760                                // 开始显示加载提示
5761                                showConversationRestoreLoading();
5762                                // 清理之前的消息元素,避免重复堆叠
5763                                loadConversationHistory(storedId);
5764                            }
5765                        }
5766                    }
5767                } else {
5768                    console.warn('拒绝处理来自未知域名的消息:', event.origin);
5769                }
5770            });
5771
5772            function isMobileDevice() {
5773                // 精准匹配手机设备的UA正则(排除平板/PC等)
5774                // 扩展UA正则:新增微信/QQ浏览器、安卓/苹果移动端标识、国产浏览器关键词
5775                const mobileUA = /Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|Windows Phone|Mobile|MicroMessenger|QQBrowser|MiuiBrowser|UCWEB/i.test(navigator.userAgent);
5776                // 检测iPad/平板(可选:如需包含平板,删除这行判断)
5777                const isTablet = /iPad|Macintosh/.test(navigator.userAgent) && 'ontouchend' in document;
5778                const result = mobileUA && !isTablet;
5779                return result;
5780            }
5781
5782            // 完整使用示例(保留原有样式插入逻辑)
5783            if (isMobileDevice()) {
5784                const style = document.createElement('style');
5785                style.type = 'text/css';
5786                const mobileStyles = `
5787                                                    #messagesContainer {
5788                                                        max-height: calc(100vh - 155px);
5789                                                    }
5790                                                    .mobilePadding {
5791                                                        padding: 0.35rem;
5792                                                    }
5793                                                    .content-body {
5794                                                        padding-left: 0.2rem;
5795                                                    }
5796                                                    .mobilePadding .segments-container {
5797                                                        font-size: 0.85rem;
5798                                                    }
5799                                                    .aifontSize {
5800                                                        font-size: 0.85rem;
5801                                                    }
5802                                                    .avatar-container {
5803                                                        margin-right: 6px !important;
5804                                                    }
5805                                                    .mobileUser {
5806                                                        margin-left: 6px !important;
5807                                                    }
5808                                                    .showThinkinglogo {
5809                                                        padding: 0px !important;
5810                                                    }
5811                                                    .able-intro{
5812                                                      margin-top:0px;
5813                                                    }
5814                                                `;
5815                style.innerHTML = mobileStyles;
5816                document.head.appendChild(style);
5817                console.log('mobile');
5818            }
5819
5820        });
5821
5822        /**
5823         * 恢复对话 SSE 流控制
5824         */
5825        let restoreStreamAbortController = null;
5826        let activeRestoreDialogId = null;
5827
5828        /**
5829         * 记录已渲染的最新消息ID,用于去重
5830         */
5831        const renderedMessageIds = new Set();
5832
5833        /**
5834         * 显示对话恢复中的加载提示并禁用发送按钮
5835         */
5836        function showConversationRestoreLoading() {
5837            // 检查是否已存在
5838            if ($('#restoreLoading').length > 0) {
5839                return;
5840            }
5841
5842            const loadingHtml = `
5843                                                                           <div id="restoreLoading" class="flex items-center justify-center py-4">
5844                                                                               <div class="flex flex-col items-center gap-2">
5845                                                                                   <i class="fa fa-spinner fa-spin" style="font-size: 24px; color: #00ae9e;"></i>
5846                                                                                   <p class="text-sm text-gray-500">Restoring conversation...</p>
5847                                                                               </div>
5848                                                                           </div>
5849                                                                       `;
5850            messagesContainer.append(loadingHtml);
5851
5852            // 禁用发送按钮
5853            $('#sendBtn').prop('disabled', true);
5854        }
5855
5856        /**
5857         * 隐藏对话恢复中的加载提示并启用发送按钮
5858         */
5859        function hideConversationRestoreLoading() {
5860            $('#restoreLoading').remove();
5861
5862            // 启用发送按钮
5863            $('#sendBtn').prop('disabled', false);
5864        }
5865
5866        /**
5867         * 加载对话历史记录
5868         * @param {string} dialogId - 对话ID
5869         */
5870        function loadConversationHistory(dialogId) {
5871            stopRestoreStream();
5872            fetch(`https://aipublic.ptglab.com/llm/v1/product/rebuild-chat/${dialogId}`, {
5873                method: 'GET',
5874                headers: {
5875                    'Content-Type': 'application/json'
5876                }
5877            })
5878                .then(response => {
5879                    if (!response.ok) {
5880                        throw new Error(`HTTP错误,状态码: ${response.status}`);
5881                    }
5882                    return response.json();
5883                })
5884                .then(data => {
5885                    if (data.dialog_expired == true) {
5886                        generateSafeNumericId()
5887                            .then(id => {
5888                                uuid = id
5889                                localStorage.setItem('chat_uuid', uuid);
5890                                // 后续逻辑
5891                            })
5892                        isLoading = false;
5893                        $('#sendBtn').removeClass('loading');
5894                        $('.loading-indicator, .load-more-indicator').remove();
5895                        updateSendButtonState(false); // 启用发送按钮
5896                        hideConversationRestoreLoading()
5897                        $(".able-intro").css("display", "block");
5898
5899                        $('#sendBtn').off('click').on('click', sendMessage);
5900                        return
5901                    } else {
5902
5903                        if (data && data.data && data.data.length == 0) {
5904                            // 如果 data 为空且 finished 为 true,展示介绍页面
5905                            const isEmpty = !data || !data.data || (Array.isArray(data.data) && data.data.length === 0);
5906                            if (isEmpty && data && data.finished === true) {
5907                                hideConversationRestoreLoading();
5908                                $(".able-intro").css("display", "block");
5909                            }
5910                            //return
5911                        }
5912
5913                        if (data && data.ok && data.data && Array.isArray(data.data) && data.data.length > 0) {
5914                            // 检查是否包含ReviewID的round_id,避免重复渲染
5915                            const hasReviewId = data.data.some(message => message.round_id == ReviewID);
5916
5917                            // 存储rebuild-chat接口返回的所有round_id
5918                            data.data.forEach(message => {
5919                                if (message.round_id) {
5920                                    rebuildChatRoundIds.add(message.round_id);
5921                                }
5922                            });
5923                            //    if (!hasReviewId) {
5924                            // 隐藏介绍区域
5925                            $(".able-intro").css("display", "none");
5926                            // 渲染消息
5927                            //renderConversationHistory(data.data);
5928
5929                            // 渲染消息(未完成时保留最后一轮由 SSE 输出,不渲染其 assistant 部分)
5930                            const willStreamLastRound = data.finished === false;
5931                            renderConversationHistory(data.data, {
5932                                skipLastRoundAssistant: willStreamLastRound
5933                            });
5934
5935
5936                            // 记录已渲染的消息ID
5937                            data.data.forEach(message => {
5938                                if (message.message_id) {
5939                                    renderedMessageIds.add(message.message_id);
5940                                }
5941                            });
5942                            //    } else {
5943                            //        console.log("检测到ReviewID,跳过重复渲染:", ReviewID);
5944                            //    }
5945                        } else if (data && data.ok && data.data && data.data.length === 0) {
5946                            // 如果 data 是空数组,保持介绍页面显示
5947                        }
5948
5949                        // 未完成时通过 SSE 继续输出最后一轮(历史轮次已由 rebuild-chat 渲染)
5950                        if (data && data.finished === false) {
5951                            updateSendButtonState(false);
5952                            const lastRoundId = (data.data && data.data.length > 0)
5953                                ? getLastRoundId(data.data)
5954                                : null;
5955                            startChatLastRoundStream(dialogId, lastRoundId);
5956                        } else {
5957                            // finished 为 true 或其他情况,隐藏加载提示并启用发送按钮
5958                            hideConversationRestoreLoading();
5959
5960                            // 如果 data 为空且 finished 为 true,展示介绍页面
5961                            const isEmpty = !data || !data.data || (Array.isArray(data.data) && data.data.length === 0);
5962                            if (isEmpty && data && data.finished === true) {
5963                                $(".able-intro").css("display", "block");
5964                            }
5965                        }
5966                    }
5967
5968                })
5969                .catch(error => {
5970                    console.error('加载对话历史失败:', error);
5971                    // 错误时也要隐藏加载提示
5972                    hideConversationRestoreLoading();
5973                });
5974        }
5975
5976
5977        /**
5978         * 取消息列表中最后一轮的 round_id
5979         * @param {Array} messages
5980         */
5981        function getLastRoundId(messages) {
5982            if (!messages || !messages.length) return null;
5983            const last = messages[messages.length - 1];
5984            return last.round_id != null ? String(last.round_id) : null;
5985        }
5986
5987        function initResponseBufferForMessage(messageId) {
5988            responseBuffers[messageId] = {
5989                segments: [],
5990                lastSegmentType: '',
5991                isLoading: false,
5992                lastType: '',
5993                toolContentBuffer: '',
5994                renderOrder: [],
5995                taskSegments: [],
5996                nonContentGroups: [],
5997                hasReviewed: false,
5998                afterTaskDoneNoValidContent: false,
5999                renderedMermaidCache: {},
6000                pendingMermaidKeys: {},
6001                pendingFormatGroupIds: {}
6002            };
6003            responseCompleted[messageId] = false;
6004            auditLoadingStates[messageId] = false;
6005        }
6006
6007        /**
6008         * 恢复未完成对话:调用 chat-last-round-sse 流式续写最后一轮
6009         * 在 rebuild-chat 已渲染的历史之后,单独追加一条 bot 消息用于流式输出
6010         * @param {string} dialogId - 对话ID
6011         * @param {string|null} lastRoundId - 未完成轮的 round_id
6012         */
6013        function startChatLastRoundStream(dialogId, lastRoundId) {
6014            if (activeRestoreDialogId === dialogId && restoreStreamAbortController) {
6015                return;
6016            }
6017            stopRestoreStream();
6018
6019            activeRestoreDialogId = dialogId;
6020            $('#restoreLoading').remove();
6021
6022            const messageId = addBotMessage(language === 'Chinese' ? '正在恢复回答...' : 'Resuming response...');
6023            if (lastRoundId) {
6024                $(`#${messageId}`).attr('data-round-id', lastRoundId);
6025                ReviewID = lastRoundId;
6026            }
6027
6028            initResponseBufferForMessage(messageId);
6029            record_MCP = '';
6030            isLoading = true;
6031            $('#sendBtn').addClass('loading');
6032            $('#sendBtn').off('click').on('click', function () {
6033                if (isLoading) {
6034                    stopRestoreStream();
6035                    abortRequest(true, dialogId);
6036                    isLoading = false;
6037                    $('#sendBtn').removeClass('loading');
6038                    hideConversationRestoreLoading();
6039                    $('#sendBtn').off('click').on('click', sendMessage);
6040                }
6041            });
6042
6043            restoreStreamAbortController = new AbortController();
6044            const signal = restoreStreamAbortController.signal;
6045            const apiUrl = 'https://aipublic.ptglab.com/llm/v2/product/chat-last-round-sse';
6046
6047            fetch(apiUrl, {
6048                method: 'POST',
6049                headers: { 'Content-Type': 'application/json' },
6050                body: JSON.stringify({ dialog_id: dialogId }),
6051                signal: signal
6052            })
6053                .then(response => {
6054                    if (!response.ok) {
6055                        throw new Error(`HTTP错误,状态码: ${response.status}`);
6056                    }
6057                    const reader = response.body.getReader();
6058                    const decoder = new TextDecoder();
6059
6060                    function read() {
6061                        reader.read().then(({ done, value }) => {
6062                            if (activeRestoreDialogId !== dialogId) {
6063                                return;
6064                            }
6065
6066                            if (done) {
6067                                try {
6068                                    isShowReview = 'Reviewed';
6069                                    const $messageDom = $(`#${messageId}`);
6070                                    updateStatusIndicator($messageDom, '');
6071                                    const buffer = responseBuffers[messageId];
6072                                    if (buffer) buffer.isLoading = false;
6073                                    $('.loading-indicator, .load-more-indicator').remove();
6074
6075                                    completeResponse(messageId);
6076                                    const hasExistingContainers = $messageDom.find('.pdb-3d-container').length > 0 ||
6077                                        $messageDom.data('has-3d-containers') === true;
6078                                    if (!hasExistingContainers) {
6079                                        checkAndProcessPdbLinks(messageId);
6080                                    }
6081                                    addCopyIconToContent(messageId);
6082                                    addResetIconToLastMessage(messageId);
6083                                    executeDoneLogic(messageId);
6084
6085                                    $messageDom.find('.thinkHtml').each(function () {
6086                                        const $thinkHtml = $(this);
6087                                        const $pdbContainers = $thinkHtml.find('.pdb-3d-container').detach();
6088                                        let htmlContent = $thinkHtml.html();
6089                                        htmlContent = htmlContent.replace(/&lt;end_message&gt;/gi, '<end_message>');
6090                                        htmlContent = htmlContent.replace(/&lt;\/end_message&gt;/gi, '</end_message>');
6091                                        htmlContent = htmlContent.replace(
6092                                            /<end_message>([\s\S]*?)<\/end_message>/gi,
6093                                            function (match, content) {
6094                                                const questionText = content.trim();
6095                                                if (!questionText) return '';
6096                                                return `<button class="end-message-btn" data-question="${questionText.replace(/"/g, '&quot;')}">${questionText}</button>`;
6097                                            }
6098                                        );
6099                                        $thinkHtml.html(htmlContent);
6100                                        if ($pdbContainers.length > 0) {
6101                                            $thinkHtml.append($pdbContainers);
6102                                        }
6103                                    });
6104
6105                                    const restoreDoneBuffer = responseBuffers[messageId];
6106                                    const $restoreSegments = $messageDom.find('.segments-container');
6107                                    if ($restoreSegments.length) {
6108                                        reinjectCachedMermaidInContainer(messageId, restoreDoneBuffer, $restoreSegments);
6109                                        formatAllNonContentGroupsAtStreamEnd(messageId, restoreDoneBuffer, $restoreSegments);
6110                                        formatOrphanSegmentGroups(messageId, $restoreSegments);
6111                                        sweepUnrenderedMermaid(messageId, $restoreSegments);
6112                                    }
6113
6114                                    $(".end-message-btn").addClass("visible");
6115                                    hideConversationRestoreLoading();
6116                                    stopRestoreStream();
6117                                    updateSendButtonState(false);
6118                                    setTimeout(() => scrollToBottom(true), 300);
6119                                } catch (doneError) {
6120                                    console.error('恢复流式输出结束处理失败:', doneError);
6121                                    hideConversationRestoreLoading();
6122                                    stopRestoreStream();
6123                                    updateSendButtonState(false);
6124                                } finally {
6125                                    responseCompleted[messageId] = true;
6126                                }
6127                                return;
6128                            }
6129
6130                            const chunk = decoder.decode(value, { stream: true });
6131
6132                            if (chunk.includes('/tool_call')) {
6133                                record_MCP += chunk;
6134                                const hasTag1 = record_MCP.includes('</think>') && record_MCP.includes('</MCP_HOST_TASK>');
6135                                if (hasTag1) {
6136                                    record_MCP = '';
6137                                    const $messageContainer = $(`#${messageId}`);
6138                                    if (!$messageContainer.length) return;
6139                                    const $taskTag = $messageContainer.find('mcp_host_task:has(span.incomplete-placeholder)');
6140                                    if ($taskTag.length) {
6141                                        const loaderId = `loader-${messageId}-${$taskTag.index()}`;
6142                                        const $existingLoading = $(`#${loaderId}`);
6143                                        if (!$existingLoading.length) {
6144                                            const $loading = $(`<div id="${loaderId}" class="loading-indicator ml-1 inline-block"></div>`)
6145                                                .css('color', 'dimgray')
6146                                                .html('<i class="fa fa-spinner fa-spin"></i>');
6147                                            $taskTag.after($loading);
6148                                        }
6149                                    }
6150                                }
6151                            }
6152
6153                            try {
6154                                const processedChunks = processSseChunk(chunk);
6155                                updateBotMessage(messageId, processedChunks);
6156                            } catch (codeError) {
6157                                console.error('恢复流式输出解析失败:', codeError);
6158                                hideConversationRestoreLoading();
6159                                stopRestoreStream();
6160                                updateSendButtonState(false);
6161                                return;
6162                            }
6163
6164                            read();
6165                        }).catch(error => {
6166                            if (error.name === 'AbortError') {
6167                                return;
6168                            }
6169                            console.error('读取恢复流式响应失败:', error);
6170                            isLoading = false;
6171                            $('#sendBtn').removeClass('loading');
6172                            $('#sendBtn').off('click').on('click', sendMessage);
6173                            hideConversationRestoreLoading();
6174                            stopRestoreStream();
6175                            updateSendButtonState(false);
6176                        });
6177                    }
6178                    read();
6179                })
6180                .catch(error => {
6181                    if (error.name === 'AbortError') {
6182                        return;
6183                    }
6184                    console.error('恢复流式请求失败:', error);
6185                    isLoading = false;
6186                    $('#sendBtn').removeClass('loading');
6187                    $('#sendBtn').off('click').on('click', sendMessage);
6188                    hideConversationRestoreLoading();
6189                    stopRestoreStream();
6190                    updateSendButtonState(false);
6191                });
6192        }
6193
6194        /**
6195         * 停止恢复对话的 SSE 流
6196         */
6197        function stopRestoreStream() {
6198            if (restoreStreamAbortController) {
6199                restoreStreamAbortController.abort();
6200                restoreStreamAbortController = null;
6201            }
6202            activeRestoreDialogId = null;
6203        }
6204
6205        // 页面卸载时中止恢复流
6206        window.addEventListener('beforeunload', function () {
6207            stopRestoreStream();
6208        })
6209
6210        /**
6211         * 追加对话历史(新增的消息)
6212         * @param {Array} messages - 消息数组
6213         */
6214        function appendConversationHistory(messages) {
6215            // 先移除加载提示(临时移除,稍后重新添加)
6216            const $restoreLoading = $('#restoreLoading');
6217            $restoreLoading.detach();
6218
6219            // 按round_id分组消息(包括user和assistant)
6220            const messagesByRound = {};
6221
6222            messages.forEach(message => {
6223                const roundId = message.round_id || 'default';
6224
6225                if (!messagesByRound[roundId]) {
6226                    messagesByRound[roundId] = {
6227                        userMessages: [],
6228                        assistantMessages: []
6229                    };
6230                }
6231
6232                if (message.role === 'user' || message.sender === 'ä½ ' || message.sender?.toLowerCase() === 'user') {
6233                    messagesByRound[roundId].userMessages.push(message);
6234                } else if (message.role === 'assistant' || message.sender === 'AI' || message.sender?.toLowerCase() === 'ai') {
6235                    messagesByRound[roundId].assistantMessages.push(message);
6236                }
6237            });
6238
6239            // 渲染每个round的消息(追加模式)
6240            Object.keys(messagesByRound).forEach(roundId => {
6241                const roundData = messagesByRound[roundId];
6242
6243                // 先渲染用户消息(如果有多个,合并显示)
6244                if (roundData.userMessages.length > 0) {
6245                    // 只显示第一个用户消息的内容
6246                    addUserMessageFromHistory(roundData.userMessages[0]);
6247                }
6248
6249                // 渲染AI消息组(合并成一个消息)
6250                if (roundData.assistantMessages.length > 0) {
6251                    addBotMessageGroupFromHistory(roundData.assistantMessages);
6252                }
6253            });
6254
6255            // 重新添加加载提示到底部
6256            if ($restoreLoading.length > 0) {
6257                messagesContainer.append($restoreLoading);
6258            }
6259
6260            // 执行后处理操作
6261            executeMessagePostProcessing();
6262
6263            // 为新追加的AI消息添加复制图标
6264            const $allBotMessages = messagesContainer.find('.bot-message');
6265            $allBotMessages.each(function () {
6266                const messageId = $(this).attr('id');
6267                if (messageId) {
6268                    // 先移除可能存在的重复图标
6269                    $(this).find('.copy-icon-container').remove();
6270                    addCopyIconToContent(messageId);
6271                }
6272            });
6273
6274            // 移除所有重置图标,只保留最后一个
6275            removeAllResetIcons();
6276            const $lastBotMessage = messagesContainer.find('.bot-message').last();
6277            if ($lastBotMessage.length > 0) {
6278                const lastMessageId = $lastBotMessage.attr('id');
6279                if (lastMessageId) {
6280                    $lastBotMessage.find('.end-message-btn').css('display', 'inline-block');
6281                    addResetIconToLastMessage(lastMessageId);
6282                }
6283            }
6284
6285            // 滚动到底部
6286            setTimeout(() => {
6287                scrollToBottom(true);
6288            }, 300);
6289        }
6290
6291        /**
6292         * 渲染对话历史
6293         * @param {Array} messages - 消息数组
6294         */
6295        function renderConversationHistory(messages, options) {
6296            options = options || {};
6297            const skipLastRoundAssistant = options.skipLastRoundAssistant === true;
6298            const lastRoundId = skipLastRoundAssistant ? getLastRoundId(messages) : null;
6299
6300            // 先清空现有消息(保留介绍页面和加载提示)
6301            messagesContainer.children().not('.able-intro, #restoreLoading').remove();
6302
6303            messagesContainer.find('.able-intro').hide();
6304
6305            // 按round_id分组消息(包括user和assistant)
6306            const messagesByRound = {};
6307
6308            messages.forEach(message => {
6309                const roundId = message.round_id || 'default';
6310
6311                if (!messagesByRound[roundId]) {
6312                    messagesByRound[roundId] = {
6313                        userMessages: [],
6314                        assistantMessages: []
6315                    };
6316                }
6317
6318                if (message.role === 'user' || message.sender === 'ä½ ' || message.sender?.toLowerCase() === 'user') {
6319                    messagesByRound[roundId].userMessages.push(message);
6320                } else if (message.role === 'assistant' || message.sender === 'AI' || message.sender?.toLowerCase() === 'ai') {
6321                    messagesByRound[roundId].assistantMessages.push(message);
6322                }
6323            });
6324
6325            // 渲染每个round的消息
6326            Object.keys(messagesByRound).forEach(roundId => {
6327                const roundData = messagesByRound[roundId];
6328                const isLastStreamingRound = skipLastRoundAssistant && lastRoundId != null
6329                    && String(roundId) === String(lastRoundId);
6330
6331                // 先渲染用户消息(如果有多个,合并显示)
6332                if (roundData.userMessages.length > 0) {
6333                    // 只显示第一个用户消息的内容
6334                    addUserMessageFromHistory(roundData.userMessages[0]);
6335                }
6336
6337                // 渲染AI消息组(最后一轮未完成时跳过,由 chat-last-round-sse 流式输出)
6338                if (roundData.assistantMessages.length > 0 && !isLastStreamingRound) {
6339                    addBotMessageGroupFromHistory(roundData.assistantMessages);
6340                }
6341            });
6342
6343            // 确保加载提示在最底部
6344            const $restoreLoading = $('#restoreLoading');
6345            if ($restoreLoading.length > 0) {
6346                $restoreLoading.appendTo(messagesContainer);
6347            }
6348
6349            // 执行后处理操作
6350            executeMessagePostProcessing();
6351
6352            // 为所有AI消息添加复制图标
6353            const $allBotMessages = messagesContainer.find('.bot-message');
6354            $allBotMessages.each(function () {
6355                const messageId = $(this).attr('id');
6356                if (messageId) {
6357                    addCopyIconToContent(messageId);
6358                }
6359            });
6360
6361            // 移除所有历史对话的重置图标,只保留最后一个
6362            removeAllResetIcons();
6363            const $lastBotMessage = messagesContainer.find('.bot-message').last();
6364            if ($lastBotMessage.length > 0) {
6365                const lastMessageId = $lastBotMessage.attr('id');
6366                if (lastMessageId) {
6367                    $lastBotMessage.find('.end-message-btn').css('display', 'inline-block');
6368                    addResetIconToLastMessage(lastMessageId);
6369                }
6370            }
6371
6372            // 滚动到底部
6373            setTimeout(() => {
6374                // 需要传入具体的DOM元素,因为initMermaidRender会检查HTMLElement类型
6375                initMermaidRender(messagesContainer);
6376                scrollToBottom(true);
6377            }, 300);
6378        }
6379
6380        /**
6381         * 从历史记录添加用户消息
6382         * @param {string} content - 消息内容
6383         */
6384        /**
6385         * 从历史记录添加用户消息
6386         * @param {Object} message - 消息对象(含 content、user_input_multi_content、dialog_id)
6387         */
6388        function addUserMessageFromHistory(message) {
6389            const content = message.content || '';
6390            const multiText = (message.user_input_multi_content || [])
6391                .filter(item => item.type === 'text' && item.text)
6392                .map(item => item.text)
6393                .join('');
6394            const allText = content + multiText;
6395
6396            // 检查是否包含tool_call标签,如果包含则跳过此消息
6397            if (allText && allText.includes('<tool_call>') && allText.includes('</tool_call>')) {
6398                return;
6399            }
6400
6401            const messageBodyHtml = buildUserHistoryMessageContent(message);
6402            if (!messageBodyHtml.trim()) {
6403                return;
6404            }
6405
6406            let spanText = language === 'Chinese' ? 'ä½ ' : 'U';
6407            const messageHtml = `
6408                                                                               <div class="message user-message message-appear">
6409                                                                                   <div class="flex items-start justify-end space-x-3" style="margin-left:45px">
6410                                                                                       <div class="bg-primary/10 rounded-lg shadow-sm p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-r-4 border-primary aifontSize">
6411                                                                                           <div class="prose max-w-none markdown-preview">
6412                                                                                               ${messageBodyHtml}
6413                                                                                           </div>
6414                                                                                       </div>
6415                                                                                       <div class="mobileUser w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 text-sm font-bold flex-shrink-0 shadow-sm">${spanText}</div>
6416                                                                                   </div>
6417                                                                               </div>
6418                                                                           `;
6419            messagesContainer.append(messageHtml);
6420        }
6421
6422        /**
6423         * 从历史记录添加AI消息组(合并同round_id的多个消息)
6424         * @param {Array} messageGroup - 同round_id的多个消息对象
6425         */
6426        function addBotMessageGroupFromHistory(messageGroup) {
6427            const messageId = generateUniqueId();
6428            let segmentsHtml = '';
6429            let segmentIndex = 0;
6430
6431            // 获取round_id(从第一个消息中获取)
6432            const roundId = messageGroup.length >
6432 0 ? messageGroup[0].round_id : '';
6433
6434            messageGroup.forEach(message => {
6435                const hasReasoning = message.reasoning_content && message.reasoning_content.trim() !== '';
6436                const hasContent = message.content && message.content.trim() !== '';
6437
6438                // 如果没有内容也没有思考内容,跳过
6439                if (!hasReasoning && !hasContent) {
6440                    return;
6441                }
6442
6443                // 检查content是否是JSON格式(审核结果)或包含tool_call
6444                let contentHtml = '';
6445                let isJsonContent = false;
6446                let isToolCall = false;
6447
6448                if (hasContent) {
6449                    // 检查是否包含tool_call标签
6450                    if (message.content.includes('<tool_call>') && message.content.includes('</tool_call>')) {
6451                        // 包含tool_call标签:标记为tool_call,但不设置contentHtml(不显示content)
6452                        isToolCall = true;
6453                        contentHtml = ''; // tool_call时不显示content
6454                    } else {
6455                        // 检查是否是纯JSON格式(审核结果)
6456                        try {
6457                            const parsed = JSON.parse(message.content);
6458                            if (typeof parsed === 'object') {
6459                                contentHtml = `<pre style="background: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;"><code>${message.content}</code></pre>`;
6460                                isJsonContent = true;
6461                            }
6462                        } catch (e) {
6463                            // 不是JSON,正常处理
6464                            contentHtml = mdParser.render(message.content);
6465                        }
6466                    }
6467                }
6468
6469                // 为每个segment创建HTML
6470                let segmentHtml = '';
6471
6472                if (hasReasoning && hasContent && !isJsonContent && !isToolCall) {
6473                    // 有思考过程和内容
6474                    segmentHtml = `
6475                                                                                   <div class="segment-group mb-1" data-segment-index="${segmentIndex}">
6476                                                                                       <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="${segmentIndex}">
6477                                                                                           <i class="fa fa-chevron-right think-toggle-icon mr-1"></i>
6478                                                                                           <span style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
6479                                                                                       </div>
6480                                                                                       <div class="think-content-body hidden pl-3 py-1 text-gray-600 text-sm aifontSize" style="">
6481                                                                                           ${mdParser.render(message.reasoning_content)}
6482                                                                                       </div>
6483                                                                                       <div class="content-body pl-3 mt-1">
6484                                                                                           ${contentHtml}
6485                                                                                       </div>
6486                                                                                   </div>
6487                                                                               `;
6488                } else if (hasReasoning && !hasContent) {
6489                    // 只有思考过程
6490                    segmentHtml = `
6491                                                                                   <div class="segment-group mb-1" data-segment-index="${segmentIndex}">
6492                                                                                       <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="${segmentIndex}">
6493                                                                                           <i class="fa fa-chevron-right think-toggle-icon mr-1"></i>
6494                                                                                           <sp
6494an style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
6495                                                                                       </div>
6496                                                                                       <div class="think-content-body hidden pl-3 py-1 text-gray-600 text-sm aifontSize" style="">
6497                                                                                           ${mdParser.render(message.reasoning_content)}
6498                                                                                       </div>
6499                                                                                   </div>
6500                                                                               `;
6501                } else if (hasReasoning && isToolCall) {
6502                    // 有思考过程且是tool_call:只显示thinking,不显示content
6503                    segmentHtml = `
6504                                                                                   <div class="segment-group mb-1" data-segment-index="${segmentIndex}">
6505                                                                                       <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="${segmentIndex}">
6506                                                                                           <i class="fa fa-chevron-right think-toggle-icon mr-1"></i>
6507                                                                                           <span style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
6508                                                                                       </div>
6509                                                                                       <div class="think-content-body hidden pl-3 py-1 text-gray-600 text-sm aifontSize" style="">
6510                                                                                           ${mdParser.render(message.reasoning_content)}
6511                                                                                       </div>
6512                                                                                   </div>
6513                                                                               `;
6514                } else if (hasContent && (isJsonContent || isToolCall)) {
6515                    // 只有内容且是JSON格式(tool_call的情况已在上面处理)
6516                    segmentHtml = `
6517                                                                                   ${hasReasoning ? `
6518                                                                                   <div class="segment-group mb-1" data-segment-index="${segmentIndex++}">
6519                                                                                       <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="${segmentIndex - 1}">
6520                                                                                           <i class="fa fa-chevron-right think-toggle-icon mr-1"></i>
6521                                                                                           <span style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
6522                                                                                       </div>
6523                                                                                       <div class="think-content-body hidden pl-3 py-1 text-gray-600 text-sm aifontSize" style="">
6524                                                                                           ${mdParser.render(message.reasoning_content)}
6525                                                                                       </div>
6526                                                                                   </div>
6527                                                                                   ` : ''}
6528                                                                                   <div class="segment-group mb-1" data-segment-index="${hasReasoning ? segmentIndex : segmentIndex}">
6529                                                                                       <div class="content-body pl-3 mt-1">
6530                                                                                           ${contentHtml}
6531                                                                                       </div>
6532                                                                                   </div>
6533                                                                               `;
6534                    if (hasReasoning) segmentIndex++;
6535                } else if (hasContent) {
6536                    // 只有内容
6537                    segmentHtml = `
6538                                                                                   <div class="segment-group mb-1" data-segment-index="${segmentIndex}">
6539                                                                                       <div class="content-body pl-3 mt-1">
6540                                                                                           ${contentHtml}
6541                                                                                       </div>
6542                                                                                   </div>
6543                                                                               `;
6544                }
6545
6546                segmentsHtml += segmentHtml;
6547                segmentIndex++;
6548            });
6549
6550            if (!segmentsHtml) {
6551                return; // 没有有效内容,不渲染
6552            }
6553
6554            const messageHtml = `
6555                                                                           <div class="message bot-message message-appear" id="${messageId}
6555" data-round-id="${roundId}">
6556                                                                               <div class="flex items-start">
6557                                                                                   <div class="avatar-container w-8 h-8 rounded-full overflow-hidden shadow-md flex-shrink-0">
6558                                                                                       <img src="/img/Able-Profile-Picture.jpg" alt="AI助手头像" class="w-full h-full object-cover">
6559                                                                                   </div>
6560                                                                                   <div class="mobilePadding bg-white rounded-lg shadow-md p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-l-4 border-primary" style="width:100%;overflow:auto">
6561                                                                                       <div class="prose max-w-none markdown-preview">
6562                                                                                           <div class="thinkHtml markdown-body">
6563                                                                                               <div class="segments-container">
6564                                                                                                   ${segmentsHtml}
6565                                                                                               </div>
6566                                                                                           </div>
6567                                                                                       </div>
6568                                                                                   </div>
6569                                                                               </div>
6570                                                                           </div>
6571                                                                       `;
6572
6573            messagesContainer.append(messageHtml);
6574
6575            // 绑定折叠事件
6576            $(`#${messageId} .think-header`).on('click', function (e) {
6577                e.stopPropagation();
6578                const $container = $(this).closest('.segment-group');
6579                const $contentBody = $container.find('.think-content-body');
6580                const $toggleIcon = $container.find('.think-toggle-icon');
6581                $contentBody.slideToggle(300, function () {
6582                    $toggleIcon.toggleClass('fa-chevron-down fa-chevron-right');
6583                });
6584            });
6585        }
6586
6587        /**
6588         * 从历史记录添加AI消息(已废弃,改用addBotMessageGroupFromHistory)
6589         * @param {Object} message - 消息对象,包含content和reasoning_content
6590         */
6591        function addBotMessageFromHistory(message) {
6592            const messageId = generateUniqueId();
6593            const hasReasoning = message.reasoning_content && message.reasoning_content.trim() !== '';
6594            const hasContent = message.content && message.content.trim() !== '';
6595
6596            // 获取round_id
6597            const roundId = message.round_id || '';
6598
6599            // 如果没有内容和思考内容,跳过
6600            if (!hasReasoning && !hasContent) {
6601                return;
6602            }
6603
6604            // 检查content是否是JSON格式(审核结果)或包含tool_call
6605            let contentHtml = '';
6606            let isJsonContent = false;
6607            let isToolCall = false;
6608
6609            if (hasContent) {
6610                // 检查是否包含tool_call标签
6611                if (message.content.includes('<tool_call>') && message.content.includes('</tool_call>')) {
6612                    // 包含tool_call标签:标记为tool_call,但不设置contentHtml(不显示content)
6613                    isToolCall = true;
6614                    contentHtml = ''; // tool_call时不显示content
6615                } else {
6616                    // 检查是否是纯JSON格式(审核结果)
6617                    try {
6618                        const parsed = JSON.parse(message.content);
6619                        if (typeof parsed === 'object') {
6620                            contentHtml = `<pre style="background: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;"><code>${message.content}</code></pre>`;
6621                            isJsonContent = true;
6622                        }
6623                    } catch (e) {
6624                        // 不是JSON,正常处理
6625                        contentHtml = mdParser.render(message.content);
6626                    }
6627                }
6628            }
6629
6630            // 构建消息HTML
6631            let segmentsHtml = '';
6632
6633            if (hasReasoning && hasContent && !isJsonContent && !isToolCall) {
6634                // 有思考过程和内容
6635                segmentsHtml = `
6636                                                                               <div class="segments-container">
6637                                                                                   <div class="segment-group mb-1" data-segment-index="0">
6638                                                                                       <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="0">
6639                                                                                           <i class="fa fa-chevron-right think-toggle-icon mr-1"></i>
6640                                                                                           <sp
6640an style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
6641                                                                                       </div>
6642                                                                                       <div class="think-content-body hidden pl-3 py-1 text-gray-600 text-sm aifontSize" style="">
6643                                                                                           ${mdParser.render(message.reasoning_content)}
6644                                                                                       </div>
6645                                                                                       <div class="content-body pl-3 mt-1">
6646                                                                                           ${contentHtml}
6647                                                                                       </div>
6648                                                                                   </div>
6649                                                                               </div>
6650                                                                           `;
6651            } else if (hasReasoning && !hasContent) {
6652                // 只有思考过程
6653                segmentsHtml = `
6654                                                                               <div class="segments-container">
6655                                                                                   <div class="segment-group mb-1" data-segment-index="0">
6656                                                                                       <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="0">
6657                                                                                           <i class="fa fa-chevron-right think-toggle-icon mr-1"></i>
6658                                                                                           <span style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
6659                                                                                       </div>
6660                                                                                       <div class="think-content-body hidden pl-3 py-1 text-gray-600 text-sm aifontSize" style="">
6661                                                                                           ${mdParser.render(message.reasoning_content)}
6662                                                                                       </div>
6663                                                                                   </div>
6664                                                                               </div>
6665                                                                           `;
6666            } else if (hasReasoning && isToolCall) {
6667                // 有思考过程且是tool_call:只显示thinking,不显示content
6668                segmentsHtml = `
6669                                                                               <div class="segments-container">
6670                                                                                   <div class="segment-group mb-1" data-segment-index="0">
6671                                                                                       <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="0">
6672                                                                                           <i class="fa fa-chevron-right think-toggle-icon mr-1"></i>
6673                                                                                           <span style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
6674                                                                                       </div>
6675                                                                                       <div class="think-content-body hidden pl-3 py-1 text-gray-600 text-sm aifontSize" style="">
6676                                                                                           ${mdParser.render(message.reasoning_content)}
6677                                                                                       </div>
6678                                                                                   </div>
6679                                                                               </div>
6680                                                                           `;
6681            } else if (hasContent && (isJsonContent || isToolCall)) {
6682                // 只有内容且是JSON格式(tool_call的情况已在上面处理)
6683                segmentsHtml = `
6684                                                                               <div class="segments-container">
6685                                                                                   ${hasReasoning ? `
6686                                                                                   <div class="segment-group mb-1" data-segment-index="0">
6687                                                                                       <div class="think-header hideThinking flex items-center cursor-pointer text-sm text-gray-700 mb-1" data-index="0">
6688                                                                                           <i class="fa fa-chevron-right think-toggle-icon mr-1"></i>
6689                                                                                           <sp
6689an style="font-weight:bold;">${language === 'Chinese' ? '思考过程' : 'Thinking'}</span>
6690                                                                                       </div>
6691                                                                                       <div class="think-content-body hidden pl-3 py-1 text-gray-600 text-sm aifontSize" style="">
6692                                                                                           ${mdParser.render(message.reasoning_content)}
6693                                                                                       </div>
6694                                                                                   </div>
6695                                                                                   ` : ''}
6696                                                                                   <div class="segment-group mb-1" data-segment-index="${hasReasoning ? '1' : '0'}">
6697                                                                                       <div class="content-body pl-3 mt-1">
6698                                                                                           ${contentHtml}
6699                                                                                       </div>
6700                                                                                   </div>
6701                                                                               </div>
6702                                                                           `;
6703            } else if (hasContent) {
6704                // 只有内容
6705                segmentsHtml = contentHtml;
6706            }
6707
6708            const messageHtml = `
6709                                                                           <div class="message bot-message message-appear" id="${messageId}" data-round-id="${roundId}">
6710                                                                               <div class="flex items-start">
6711                                                                                   <div class="w-8 h-8 rounded-full overflow-hidden shadow-md flex-shrink-0">
6712                                                                                       <img src="/img/Able-Profile-Picture.jpg" alt="AI助手头像" class="w-full h-full object-cover">
6713                                                                                   </div>
6714                                                                                   <div class="mobilePadding bg-white rounded-lg shadow-md p-4 max-w-[90%] sm:max-w-[80%] md:max-w-[75%] border-l-4 border-primary" style="width:100%;overflow:auto">
6715                                                                                       <div class="prose max-w-none markdown-preview">
6716                                                                                           <div class="thinkHtml markdown-body">
6717                                                                                               ${segmentsHtml}
6718                                                                                           </div>
6719                                                                                       </div>
6720                                                                                   </div>
6721                                                                               </div>
6722                                                                           </div>
6723                                                                       `;
6724
6725            messagesContainer.append(messageHtml);
6726
6727            // 绑定折叠事件
6728            $(`#${messageId} .think-header`).on('click', function (e) {
6729                e.stopPropagation();
6730                const $container = $(this).closest('.segment-group');
6731                const $contentBody = $container.find('.think-content-body');
6732                const $toggleIcon = $container.find('.think-toggle-icon');
6733                $contentBody.slideToggle(300, function () {
6734                    $toggleIcon.toggleClass('fa-chevron-down fa-chevron-right');
6735                });
6736            });
6737        }
6738
6739
6740        /**
6741         * 执行消息的后处理操作
6742         * 包括:处理链接、PDB、Live Chat、绑定事件等
6743         */
6744        function executeMessagePostProcessing() {
6745            // 处理链接属性
6746            $('.thinkHtml a').attr('target', '_blank');
6747            // 判断URL是否为图片
6748            function isImageUrl(url, extensions) {
6749                const lowerUrl = url.toLowerCase();
6750                return extensions.some(ext => lowerUrl.endsWith(ext));
6751            }
6752
6753            // 从URL提取文件名
6754            function getFileName(url) {
6755                const parts = url.split('/');
6756                return parts[parts.length - 1] || '图片';
6757            }
6758            const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
6759
6760            // 处理图片链接
6761            $('a:not(:has(img))').each(function () {
6762                const $link = $(this);
6763                if ($link.find('img').length > 0) {
6764                    return;
6765                }
6766                const href = $link.attr('href');
6767
6768                // 检查链接是否为图片
6769                if (href && isImageUrl(href, imageExtensions)) {
6770                    // 创建图片元素
6771                    const $img = $('<img>', {
6772                        'src': href,
6773                        'alt': '图片预览: ' + getFileName(href),
6774                        'data-jq-auto-added': 'true'
6775                    }).css({
6776                        'vertical-align': 'middle',
6777                        'border-radius': '3px'
6778                    });
6779
6780                    $link.append($img);
6781
6782                    $img.on('load error', function () {
6783                        setTimeout(() => {
6784                            scrollToBottom(true);
6785                        }, 300);
6786                    });
6787                }
6788            });
6789
6790            // 处理Live Chat链接
6791            $('.thinkHtml a[href="@ptglab-live-chat"]').each(function () {
6792                const $link = $(this);
6793                $link.removeAttr('href');
6794                $link.addClass('open-live-chat');
6795                $link.text('Live Chat');
6796            });
6797
6798            // 处理纯文本形式的 @ptglab-live-chat
6799            $('.thinkHtml').each(function () {
6800                const $thinkHtml = $(this);
6801
6802                // 保存3D容器,避免被清空
6803                const $pdbContainers = $thinkHtml.find('.pdb-3d-container').detach();
6804
6805                let htmlContent = $thinkHtml.html();
6806
6807                // 1. 先处理所有包含@ptglab-live-chat的a标签
6808                htmlContent = htmlContent.replace(
6809                    /<a\s+[^>]*?>([^<]*?)@ptglab-live-chat([^<]*?)<\/a>/gi,
6810                    function (match, prefix, suffix) {
6811                        return `<span class="open-live-chat">${prefix}Live Chat${suffix}</span>`;
6812                    }
6813                );
6814
6815                // 2. 处理纯文本形式的@ptglab-live-chat
6816                htmlContent = htmlContent.replace(
6817                    /([^<>\s])?@ptglab-live-chat([^<>\s])?/g,
6818                    function (match, prefix, suffix) {
6819                        const pre = prefix || '';
6820                        const suf = suffix || '';
6821                        return `${pre}<span class="open-live-chat">Live Chat</span>${suf}`;
6822                    }
6823                );
6824
6825                // 3. 清理可能残留的错误标签
6826                htmlContent = htmlContent.replace(/<a\s+[^>]*?><\/a>/gi, '');
6827
6828                // 更新内容
6829                $thinkHtml.html(htmlContent);
6830
6831                // 恢复3D容器
6832                if ($pdbContainers.length > 0) {
6833                    $thinkHtml.append($pdbContainers);
6834                }
6835            });
6836
6837            // 绑定Live Chat点击事件
6838            $('.open-live-chat')
6839                .off('click')
6840                .click(function () {
6841                    const data = {
6842                        type: 'fromChild',
6843                        message: 'msg',
6844                        status: 'success'
6845                    };
6846                    window.parent.postMessage(data, '*');
6847                });
6848
6849            // 绑定思考折叠事件
6850            $('#messagesContainer')
6851                .off('click', '.think-header')
6852                .on('click', '.think-header', function (e) {
6853                    e.stopPropagation();
6854                    const $container = $(this).closest('.segment-group');
6855                    const $contentBody = $container.find('.think-content-body');
6856                    const $toggleIcon = $container.find('.think-toggle-icon');
6857                    $contentBody.slideToggle(300, function () {
6858                        $toggleIcon.toggleClass('fa-chevron-down fa-chevron-right');
6859                    });
6860                });
6861
6862            const $pdbLinks = $('.content-body a[href*=".pdb"]');
6863            if ($pdbLinks.length > 0) {
6864                // 为每个PDB链接处理href属性
6865                $pdbLinks.each(function () {
6866                    const $link = $(this);
6867                    const originalHref = $link.attr('href');
6868
6869                    // 移除原始href属性,将其值保存到data-original-href中
6870                    $link.removeAttr('href');
6871                    $link.attr('data-original-href', originalHref);
6872                });
6873            }
6874            // 只查找当前消息的.content-body中的.pdb链接
6875            setTimeout(() => {
6876                initMermaidRender();
6877                $(document).off('click', 'a[data-original-href]', handleLinkClick);
6878                $(document).on('click', 'a[data-original-href]', handleLinkClick);
6879            }, 300)
6880
6881        }
6882
6883
6884
6885
6886
6887        function reportErrorToApi(dialogId, roundId, errorMsg) {
6888
6889            let topHref;
6890            try {
6891                // 尝试获取顶层窗口的属性(跨域时会抛错)
6892                topHref = window.top.location.href;
6893            } catch (e) {
6894                // 跨域报错时,降级为当前页面的属性
6895                topTitle = window.self.document.title; // 当前页面标题
6896                console.warn('跨域无法访问顶层窗口属性,已降级为当前页面值:', e);
6897            }
6898
6899            // 1. 构造 ErrorReport 格式的参数(严格匹配后端要求)
6900            const errorReport = {
6901                dialog_id: dialogId || '', // 对话ID(用你代码里的全局uuid)
6902                round_id: roundId || '',   // 轮次ID(聊天的messageId)
6903                msg: errorMsg || '未知错误', // 错误提示
6904                current_url: topHref
6905            };
6906
6907
6908            // 2. 调用错误上报接口(独立fetch,不影响原有逻辑)
6909            fetch('https://aipublic.ptglab.com/llm/v1/report-error', {
6910                method: 'POST',
6911                headers: { 'Content-Type': 'application/json' },
6912                body: JSON.stringify(errorReport) // 转JSON字符串
6913            }).catch(reportErr => {
6914                // 上报失败只打日志,不影响主流程
6915                console.error('错误上报接口调用失败:', reportErr);
6916            });
6917        }
6918
6919    </script>
6919
6920</body>
6921</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.