From 8636ff97bffec9f2130628bf09c9d0fbb371e2bc Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期二, 10 三月 2026 16:53:24 +0800
Subject: [PATCH] #
---
src/main/webapp/views/ai/diagnosis.html | 988 +++++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 849 insertions(+), 139 deletions(-)
diff --git a/src/main/webapp/views/ai/diagnosis.html b/src/main/webapp/views/ai/diagnosis.html
index b2dfd1e..70686ae 100644
--- a/src/main/webapp/views/ai/diagnosis.html
+++ b/src/main/webapp/views/ai/diagnosis.html
@@ -6,94 +6,640 @@
<title>WCS AI 鍔╂墜</title>
<link rel="stylesheet" href="../../static/vue/element/element.css" />
<style>
- body { background: #f5f7fa; }
- .container { max-width: 1100px; margin: 24px auto; }
- .actions { display: flex; gap: 12px; align-items: center; }
- .output { height: 60vh; }
- .markdown-body { font-size: 14px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
- .markdown-body p { margin: 4px 0; }
- .markdown-body ul, .markdown-body ol { margin: 4px 0 4px 16px; padding: 0; }
- .markdown-body h1, .markdown-body h2, .markdown-body h3 { margin-top: 8px; }
- .markdown-body pre { background: #f6f8fa; padding: 12px; border-radius: 6px; overflow: auto; }
- .status { color: #909399; }
- .chat { display: flex; flex-direction: column; gap: 10px; height: 100%; overflow-y: auto; padding-right: 8px; }
- .msg { display: flex; align-items: flex-start; }
- .msg.user { justify-content: flex-end; }
- .msg.assistant { justify-content: flex-start; }
- .bubble { max-width: 72%; padding: 10px 12px; border-radius: 16px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
- .assistant .bubble { background: #ffffff; border: 1px solid #ebeef5; color: #303133; }
- .user .bubble { background: #409EFF; color: #ffffff; }
- .composer { display: flex; gap: 10px; align-items: center; margin-top: 12px; }
- .avatar { width: 24px; height: 24px; display: flex; align-items: center; margin-right: 8px; }
- .time { font-size: 12px; color: #909399; text-align: right; margin-top: 6px; }
- .output .el-card__body { height: 100%; padding: 0; }
- .assistant-running { display: flex; align-items: center; gap: 8px; color: #909399; }
- details.think-block {
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- padding: 8px;
- margin: 8px 0;
- background-color: #fcfcfc;
+ html, body, #app {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
}
+
+ body {
+ background: #ffffff;
+ color: #303133;
+ font-family: "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
+ }
+
+ * {
+ box-sizing: border-box;
+ }
+
+ .drawer-shell {
+ height: 100%;
+ background: #fff;
+ }
+
+ .assistant-page {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 0;
+ }
+
+ .assistant-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 14px;
+ border-bottom: 1px solid #ebeef5;
+ background: #fff;
+ flex-shrink: 0;
+ }
+
+ .assistant-header-main {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+ }
+
+ .assistant-header-icon {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+ box-shadow: 0 4px 10px rgba(64, 158, 255, 0.18);
+ flex-shrink: 0;
+ }
+
+ .assistant-header-text {
+ min-width: 0;
+ }
+
+ .assistant-header-text strong {
+ display: block;
+ font-size: 18px;
+ line-height: 1.2;
+ color: #303133;
+ }
+
+ .assistant-header-text span {
+ display: block;
+ margin-top: 2px;
+ font-size: 12px;
+ color: #909399;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .assistant-header-side {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ }
+
+ .status-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 10px;
+ border-radius: 999px;
+ background: #f4f4f5;
+ color: #606266;
+ font-size: 12px;
+ font-weight: 600;
+ }
+
+ .status-chip::before {
+ content: "";
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #909399;
+ }
+
+ .status-chip.streaming {
+ background: #ecf5ff;
+ color: #409eff;
+ }
+
+ .status-chip.streaming::before {
+ background: #409eff;
+ }
+
+ .status-chip.loading {
+ background: #fdf6ec;
+ color: #e6a23c;
+ }
+
+ .status-chip.loading::before {
+ background: #e6a23c;
+ }
+
+ .toolbar-panel {
+ padding: 10px 14px;
+ border-bottom: 1px solid #ebeef5;
+ background: #fafbfd;
+ flex-shrink: 0;
+ }
+
+ .toolbar-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .toolbar-row + .toolbar-row {
+ margin-top: 8px;
+ }
+
+ .toolbar-tip {
+ flex: 1;
+ min-width: 0;
+ padding: 0 2px;
+ color: #606266;
+ font-size: 12px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .session-select {
+ flex: 1;
+ min-width: 220px;
+ }
+
+ .session-count {
+ color: #909399;
+ font-size: 12px;
+ white-space: nowrap;
+ }
+
+ .assistant-content {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 12px;
+ background: #f5f7fa;
+ }
+
+ .chat-panel {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ background: #fff;
+ border: 1px solid #ebeef5;
+ border-radius: 4px;
+ overflow: hidden;
+ }
+
+ .chat-scroll {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 14px;
+ }
+
+ .empty-state {
+ height: 100%;
+ min-height: 260px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ color: #909399;
+ padding: 20px;
+ }
+
+ .empty-state strong {
+ font-size: 16px;
+ color: #303133;
+ margin-bottom: 6px;
+ }
+
+ .empty-state p {
+ max-width: 420px;
+ margin: 0;
+ line-height: 1.7;
+ font-size: 13px;
+ }
+
+ .empty-presets {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 8px;
+ margin-top: 16px;
+ }
+
+ .empty-presets button,
+ .quick-actions button {
+ border: 1px solid #dcdfe6;
+ background: #fff;
+ color: #606266;
+ border-radius: 4px;
+ padding: 8px 12px;
+ cursor: pointer;
+ transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease;
+ font: inherit;
+ }
+
+ .empty-presets button:hover,
+ .quick-actions button:hover {
+ border-color: #409eff;
+ color: #409eff;
+ background: #ecf5ff;
+ }
+
+ .message-row {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 14px;
+ }
+
+ .message-row.user {
+ justify-content: flex-end;
+ }
+
+ .message-row.user .message-avatar {
+ order: 2;
+ }
+
+ .message-row.user .message-content {
+ order: 1;
+ align-items: flex-end;
+ }
+
+ .message-avatar {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-top: 2px;
+ }
+
+ .message-content {
+ display: flex;
+ flex-direction: column;
+ max-width: 82%;
+ min-width: 0;
+ }
+
+ .message-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 6px;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .message-meta strong {
+ font-weight: 600;
+ color: #606266;
+ }
+
+ .message-bubble {
+ padding: 12px 14px;
+ border-radius: 8px;
+ border: 1px solid #ebeef5;
+ background: #fff;
+ color: #303133;
+ line-height: 1.6;
+ word-break: break-word;
+ }
+
+ .message-bubble.user {
+ background: #409eff;
+ border-color: #409eff;
+ color: #fff;
+ }
+
+ .message-bubble.user .plain-text,
+ .message-bubble.user .markdown-body {
+ color: inherit;
+ }
+
+ .plain-text {
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 14px;
+ line-height: 1.6;
+ }
+
+ .message-time {
+ margin-top: 6px;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .assistant-running {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: #909399;
+ min-height: 24px;
+ }
+
+ .typing-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #409eff;
+ box-shadow: 12px 0 0 rgba(64, 158, 255, 0.6), 24px 0 0 rgba(64, 158, 255, 0.3);
+ margin-right: 24px;
+ animation: pulseDots 1.2s infinite linear;
+ }
+
+ @keyframes pulseDots {
+ 0% { transform: translateX(0); opacity: 0.6; }
+ 50% { transform: translateX(2px); opacity: 1; }
+ 100% { transform: translateX(0); opacity: 0.6; }
+ }
+
+ .markdown-body {
+ font-size: 14px;
+ line-height: 1.7;
+ white-space: normal;
+ word-break: break-word;
+ }
+
+ .markdown-body > :first-child {
+ margin-top: 0;
+ }
+
+ .markdown-body > :last-child {
+ margin-bottom: 0;
+ }
+
+ .markdown-body p {
+ margin: 0 0 10px;
+ }
+
+ .markdown-body ul,
+ .markdown-body ol {
+ margin: 0 0 10px 18px;
+ padding: 0;
+ }
+
+ .markdown-body h1,
+ .markdown-body h2,
+ .markdown-body h3,
+ .markdown-body h4 {
+ margin: 14px 0 8px;
+ line-height: 1.4;
+ }
+
+ .markdown-body pre {
+ margin: 10px 0;
+ padding: 12px;
+ border-radius: 4px;
+ overflow-x: auto;
+ background: #2b2f3a;
+ color: #eff5fb;
+ font-size: 13px;
+ }
+
+ .markdown-body code {
+ font-family: "SFMono-Regular", "Consolas", monospace;
+ }
+
+ .markdown-body p code,
+ .markdown-body li code {
+ background: #f5f7fa;
+ color: #337ecc;
+ padding: 2px 6px;
+ border-radius: 3px;
+ }
+
+ .markdown-body blockquote {
+ margin: 10px 0;
+ padding: 10px 12px;
+ border-left: 4px solid #409eff;
+ background: #ecf5ff;
+ color: #606266;
+ }
+
+ details.think-block {
+ border: 1px solid #d9ecff;
+ border-radius: 4px;
+ padding: 10px 12px;
+ margin: 10px 0;
+ background: #f5faff;
+ }
+
details.think-block summary {
cursor: pointer;
- color: #909399;
+ color: #409eff;
font-size: 13px;
- font-weight: bold;
+ font-weight: 600;
outline: none;
}
+
details.think-block .content {
margin-top: 8px;
color: #606266;
font-size: 13px;
white-space: pre-wrap;
+ line-height: 1.7;
+ }
+
+ .composer-panel {
+ background: #fff;
+ border: 1px solid #ebeef5;
+ border-radius: 4px;
+ padding: 12px;
+ flex-shrink: 0;
+ }
+
+ .composer-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 10px;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .composer-head strong {
+ color: #303133;
+ font-size: 14px;
+ }
+
+ .composer-panel .el-textarea__inner {
+ min-height: 92px !important;
+ border-radius: 4px;
+ resize: none;
+ font-size: 14px;
+ line-height: 1.6;
+ padding: 10px 12px;
+ font-family: inherit;
+ }
+
+ .composer-tools {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-top: 10px;
+ flex-wrap: wrap;
+ }
+
+ .quick-actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .composer-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-left: auto;
+ flex-wrap: wrap;
+ }
+
+ .composer-hint {
+ color: #909399;
+ font-size: 12px;
+ white-space: nowrap;
+ }
+
+ @media (max-width: 520px) {
+ .toolbar-tip,
+ .assistant-header-text span,
+ .composer-hint {
+ display: none;
+ }
+
+ .session-select {
+ min-width: 100%;
+ }
+
+ .message-content {
+ max-width: 100%;
+ }
}
</style>
</head>
<body>
- <div id="app" class="container">
- <el-card shadow="hover">
- <div slot="header" class="clearfix" style="display: flex; align-items: center;">
- <div v-html="headerIcon" style="margin-right: 10px; display: flex;"></div>
- <span>WCS AI 鍔╂墜</span>
- </div>
-
- <div class="actions" style="flex-wrap: wrap;">
- <el-button type="primary" :loading="loading" :disabled="streaming" @click="start">涓�閿瘖鏂郴缁�</el-button>
- <el-button type="warning" :disabled="!streaming" @click="stop">鍋滄</el-button>
- <!-- <el-button @click="clear">娓呯┖褰撳墠鑱婂ぉ</el-button> -->
- <span class="status" style="margin-right: 12px;">{{ statusText }}</span>
- <el-select v-model="currentChatId" placeholder="閫夋嫨浼氳瘽" style="min-width:240px;" @change="switchChat" :disabled="streaming">
- <el-option v-for="c in chats" :key="c.chatId" :label="(c.title||('浼氳瘽 '+c.chatId)) + '锛�'+(c.size||0)+'锛�'" :value="c.chatId" />
- </el-select>
- <el-button type="success" plain icon="el-icon-plus" @click="newChat" :disabled="streaming">鏂颁細璇�</el-button>
- <el-button type="danger" plain icon="el-icon-delete" @click="deleteChat" :disabled="!currentChatId || streaming">鍒犻櫎浼氳瘽</el-button>
- </div>
-
- <el-divider></el-divider>
-
- <el-card class="output" shadow="never">
- <div ref="chat" class="chat">
- <div v-for="(m,i) in messages" :key="i" class="msg" :class="m.role">
- <div class="avatar" v-html="m.role === 'assistant' ? assistantIcon : userIcon"></div>
- <div class="bubble">
- <div v-if="m.role === 'assistant' && m.html" class="markdown-body" v-html="m.html"></div>
- <div v-else-if="m.role === 'assistant' && streaming && i === messages.length - 1" class="assistant-running">
- <span v-html="assistantIcon"></span>
- <span>AI鍔╂墜姝e湪杩愯涓�</span>
- </div>
- <div v-else v-text="m.text"></div>
- <div class="time">{{ m.ts }}</div>
- </div>
+ <div id="app" class="drawer-shell">
+ <div class="assistant-page">
+ <header class="assistant-header">
+ <div class="assistant-header-main">
+ <div class="assistant-header-icon" v-html="headerIcon"></div>
+ <div class="assistant-header-text">
+ <strong>AI 鍔╂墜</strong>
+ <span>绯荤粺宸℃銆佸紓甯搁棶绛斻�佸巻鍙蹭細璇�</span>
</div>
</div>
- </el-card>
+ <div class="assistant-header-side">
+ <span class="status-chip" :class="{ streaming: streaming, loading: loading && !streaming }">{{ statusText }}</span>
+ <el-button v-if="embedded" size="mini" icon="el-icon-close" @click="closeDrawer">鍏抽棴</el-button>
+ </div>
+ </header>
- <div class="composer">
- <el-input v-model="userInput" placeholder="鍚� AI 鍔╂墜鎻愰棶" clearable :disabled="streaming" @keyup.enter.native="ask"></el-input>
- <el-button type="success" :disabled="sendDisabled" @click="ask">鍙戦��</el-button>
- </div>
- </el-card>
+ <section class="toolbar-panel">
+ <div class="toolbar-row">
+ <el-button type="primary" size="small" :disabled="streaming" @click="start">涓�閿贰妫�</el-button>
+ <el-button size="small" :disabled="streaming" @click="newChat">鏂颁細璇�</el-button>
+ <el-button size="small" type="danger" plain :disabled="!currentChatId || streaming" @click="deleteChat">鍒犻櫎</el-button>
+ <el-button size="small" type="warning" plain :disabled="!streaming" @click="stop">鍋滄</el-button>
+ </div>
+ <div class="toolbar-row">
+ <el-select
+ v-model="currentChatId"
+ class="session-select"
+ size="small"
+ filterable
+ placeholder="閫夋嫨鍘嗗彶浼氳瘽"
+ :disabled="streaming || (!sortedChats.length && !currentChatId)"
+ @change="switchChat"
+ >
+ <el-option
+ v-if="currentChatId && !findChat(currentChatId)"
+ :key="currentChatId"
+ :label="currentChatSummary"
+ :value="currentChatId"
+ ></el-option>
+ <el-option
+ v-for="chat in sortedChats"
+ :key="chat.chatId"
+ :label="chatOptionLabel(chat)"
+ :value="chat.chatId"
+ ></el-option>
+ </el-select>
+ <div class="toolbar-tip">{{ currentChatSummary }}</div>
+ <div class="session-count">{{ sortedChats.length }} 涓細璇�</div>
+ </div>
+ </section>
+
+ <main class="assistant-content">
+ <section class="chat-panel">
+ <div ref="chat" class="chat-scroll">
+ <div v-if="!messages.length" class="empty-state">
+ <strong>杈撳叆闂锛屾垨鍏堟墽琛屼竴娆″贰妫�</strong>
+ <p>鏀寔杩炵画杩介棶銆佸巻鍙蹭細璇濆垏鎹紝浠ュ強 AI 鎬濊�冭繃绋嬫姌鍙犲睍绀恒��</p>
+ <div class="empty-presets">
+ <button v-for="preset in promptPresets" :key="preset.title" @click="applyPreset(preset)">
+ {{ preset.title }}
+ </button>
+ </div>
+ </div>
+
+ <template v-else>
+ <div v-for="(m, i) in messages" :key="i" class="message-row" :class="m.role">
+ <div class="message-avatar" v-html="m.role === 'assistant' ? assistantIcon : userIcon"></div>
+ <div class="message-content">
+ <div class="message-meta">
+ <strong>{{ m.role === 'assistant' ? 'AI 鍔╂墜' : '鐢ㄦ埛' }}</strong>
+ <span>{{ m.role === 'assistant' ? 'WCS 璇婃柇鍥炲' : '闂杈撳叆' }}</span>
+ </div>
+ <div class="message-bubble" :class="m.role">
+ <div v-if="m.role === 'assistant' && m.html" class="markdown-body" v-html="m.html"></div>
+ <div v-else-if="m.role === 'assistant' && streaming && i === messages.length - 1" class="assistant-running">
+ <span class="typing-dot"></span>
+ <span>AI 姝e湪鐢熸垚鍥炲...</span>
+ </div>
+ <div v-else class="plain-text" v-text="m.role === 'assistant' ? (m.md || '') : (m.text || '')"></div>
+ </div>
+ <div class="message-time">{{ m.ts }}</div>
+ </div>
+ </div>
+ </template>
+ </div>
+ </section>
+
+ <footer class="composer-panel">
+ <div class="composer-head">
+ <div><strong>鍚� AI 鍔╂墜鎻愰棶</strong></div>
+ <div>{{ currentChatId ? '浼氳瘽宸茬粦瀹�' : '涓存椂浼氳瘽' }}</div>
+ </div>
+ <el-input
+ v-model="userInput"
+ type="textarea"
+ :autosize="{ minRows: 3, maxRows: 7 }"
+ placeholder="渚嬪锛氭渶杩戝摢涓澶囨渶鍊煎緱浼樺厛鎺掓煡锛熷紓甯告槸鍚﹀拰鍫嗗灈鏈轰换鍔°�佸伐浣嶅牭濉炴垨鏃ュ織娉㈠姩鏈夊叧锛�"
+ :disabled="streaming"
+ @keydown.native="handleComposerKeydown"
+ ></el-input>
+ <div class="composer-tools">
+ <div class="quick-actions" v-if="!streaming">
+ <button v-for="preset in inlinePrompts" :key="preset.title" @click="applyPreset(preset, true)">
+ {{ preset.title }}
+ </button>
+ </div>
+ <div class="composer-actions">
+ <span class="composer-hint">Enter 鍙戦�侊紝Shift+Enter 鎹㈣</span>
+ <el-button type="primary" size="small" :disabled="sendDisabled" @click="ask">鍙戦��</el-button>
+ </div>
+ </div>
+ </footer>
+ </main>
+ </div>
</div>
<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
@@ -108,10 +654,11 @@
});
function getUserIconHtml(width, height) {
- width = width || 24; height = height || 24;
- return '<svg width="'+width+'" height="'+height+'" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">\n'
- + '<circle cx="12" cy="7" r="4" fill="#909399"/>\n'
- + '<path d="M4 20c0-4 4-6 8-6s8 2 8 6" fill="#909399" opacity="0.35"/>\n'
+ width = width || 24;
+ height = height || 24;
+ return '<svg width="' + width + '" height="' + height + '" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">'
+ + '<circle cx="12" cy="7" r="4" fill="#6c7782"></circle>'
+ + '<path d="M4 20c0-4 4-6 8-6s8 2 8 6" fill="#6c7782" opacity="0.34"></path>'
+ '</svg>';
}
@@ -119,11 +666,12 @@
el: '#app',
data: function() {
return {
- headerIcon: getAiIconHtml(50, 50),
+ headerIcon: getAiIconHtml(42, 42),
assistantIcon: getAiIconHtml(24, 24),
userIcon: getUserIconHtml(24, 24),
loading: false,
streaming: false,
+ embedded: window !== window.top,
source: null,
messages: [],
pendingText: '',
@@ -135,67 +683,212 @@
autoScrollThreshold: 80,
chats: [],
currentChatId: '',
- resetting: false
+ resetting: false,
+ promptPresets: [
+ {
+ title: '宸℃褰撳墠绯荤粺',
+ description: '璁� AI 涓诲姩姊崇悊璁惧銆佷换鍔″拰鏃ュ織锛岀粰鍑轰竴杞畬鏁村贰妫�銆�',
+ prompt: ''
+ },
+ {
+ title: '瀹氫綅鍫嗗灈鏈哄紓甯�',
+ description: '缁撳悎杩戞湡鏃ュ織涓庝换鍔$姸鎬侊紝鍒ゆ柇鏄惁瀛樺湪鍫嗗灈鏈洪摼璺紓甯搞��',
+ prompt: '甯垜瀹氫綅褰撳墠鍫嗗灈鏈虹浉鍏崇殑寮傚父椋庨櫓锛屾寜鍙兘鎬т粠楂樺埌浣庡垪鍑恒��'
+ },
+ {
+ title: '鍒嗘瀽鍫靛涓庣Н鍘�',
+ description: '璁� AI 浼樺厛鍏虫敞宸ヤ綅鍫靛銆佷换鍔″爢绉拰鑺傛媿寮傚父銆�',
+ prompt: '璇烽噸鐐瑰垎鏋愬綋鍓嶆槸鍚﹀瓨鍦ㄥ伐浣嶅牭濉炪�佷换鍔$Н鍘嬫垨鑺傛媿寮傚父銆�'
+ },
+ {
+ title: '杩介棶鏈�杩戝憡璀�',
+ description: '鎶婃渶杩戝紓甯镐簨浠跺帇缂╂垚鍙墽琛屾帓鏌ュ缓璁��',
+ prompt: '甯垜鎬荤粨鏈�杩戞渶鍊煎緱鍏虫敞鐨勫紓甯革紝骞剁粰鍑轰笅涓�姝ユ帓鏌ュ姩浣溿��'
+ }
+ ]
};
},
computed: {
statusText: function() {
- if (this.streaming) return '璇婃柇杩涜涓�';
+ if (this.streaming) return '璇婃柇涓�';
if (this.loading) return '杩炴帴涓�';
return '绌洪棽';
},
sendDisabled: function() {
- var t = (this.userInput || '').trim();
- return this.streaming || t.length === 0;
+ var text = (this.userInput || '').trim();
+ return this.streaming || text.length === 0;
+ },
+ sortedChats: function() {
+ var arr = Array.isArray(this.chats) ? this.chats.slice() : [];
+ return arr.sort(function(a, b) {
+ var at = a && a.updatedAt ? Number(a.updatedAt) : 0;
+ var bt = b && b.updatedAt ? Number(b.updatedAt) : 0;
+ return bt - at;
+ });
+ },
+ currentChatSummary: function() {
+ if (!this.currentChatId) return '鏈粦瀹氬巻鍙蹭細璇�';
+ var current = this.findChat(this.currentChatId);
+ if (!current && this.resetting) return '鏂板缓浼氳瘽锛岀瓑寰呴鏉℃秷鎭�';
+ if (!current) return '浼氳瘽 ' + this.currentChatId;
+ return this.chatLabel(current);
+ },
+ inlinePrompts: function() {
+ return this.promptPresets.slice(1);
}
},
methods: {
+ authHeaders: function() {
+ var token = localStorage.getItem('token');
+ return token ? { token: token } : {};
+ },
+ notifyError: function(message) {
+ if (this.$message && message) {
+ this.$message.error(message);
+ }
+ },
+ closeDrawer: function() {
+ if (!this.embedded) return;
+ try {
+ if (window.parent && window.parent.layer) {
+ var index = window.parent.layer.getFrameIndex(window.name);
+ if (typeof index === 'number' && index >= 0) {
+ window.parent.layer.close(index);
+ return;
+ }
+ }
+ } catch (e) {}
+ },
renderMarkdown: function(md, streaming) {
if (!md) return '';
- var src = md.replace(/\\n/g, '\n');
+ var source = md.replace(/\\n/g, '\n');
var openAttr = streaming ? ' open' : '';
- src = src.replace(/<think>/g, '<details class="think-block"' + openAttr + '><summary>AI娣卞害鎬濊��</summary><div class="content">');
- src = src.replace(/<\/think>/g, '</div></details>');
- if (streaming && src.indexOf('<details class="think-block"') >= 0 && src.indexOf('</div></details>') < 0) {
- src += '</div></details>';
+ source = source.replace(/<think>/g, '<details class="think-block"' + openAttr + '><summary>AI 娣卞害鎬濊��</summary><div class="content">');
+ source = source.replace(/<\/think>/g, '</div></details>');
+ if (streaming && source.indexOf('<details class="think-block"') >= 0 && source.indexOf('</div></details>') < 0) {
+ source += '</div></details>';
}
- return DOMPurify.sanitize(marked.parse(src));
+ return DOMPurify.sanitize(marked.parse(source));
},
- loadChats: function() {
+ loadChats: function(preferKeepCurrent) {
var self = this;
- fetch(baseUrl + '/ai/diagnose/chats', { headers: { 'token': localStorage.getItem('token') } })
- .then(function(r){ return r.json(); })
- .then(function(arr){ if (Array.isArray(arr)) { self.chats = arr; } });
+ fetch(baseUrl + '/ai/diagnose/chats', { headers: self.authHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(arr) {
+ self.chats = Array.isArray(arr) ? arr : [];
+ if (preferKeepCurrent && self.currentChatId) return;
+ if (!self.currentChatId && self.sortedChats.length > 0) {
+ self.openChat(self.sortedChats[0].chatId);
+ return;
+ }
+ if (!self.currentChatId && self.sortedChats.length === 0) {
+ self.newChat();
+ }
+ })
+ .catch(function() {
+ self.chats = [];
+ if (!preferKeepCurrent) {
+ self.newChat();
+ }
+ self.notifyError('鍔犺浇 AI 浼氳瘽鍒楄〃澶辫触');
+ });
+ },
+ chatLabel: function(chat) {
+ if (!chat) return '鏈懡鍚嶄細璇�';
+ if (chat.title) return chat.title;
+ var id = chat.chatId || '';
+ return '浼氳瘽 ' + (id.length > 8 ? id.slice(-8) : id);
+ },
+ chatOptionLabel: function(chat) {
+ if (!chat) return '鏈懡鍚嶄細璇�';
+ return this.chatLabel(chat) + ' 路 ' + (chat.size || 0) + ' 鏉� 路 ' + this.chatUpdatedAt(chat);
+ },
+ chatUpdatedAt: function(chat) {
+ if (!chat || !chat.updatedAt) return '鍒氬垰鍒涘缓';
+ var time = Number(chat.updatedAt);
+ if (!time) return '鏈�杩戞洿鏂�';
+ var diff = Date.now() - time;
+ if (diff < 60000) return '鍒氬垰鏇存柊';
+ if (diff < 3600000) return Math.floor(diff / 60000) + ' 鍒嗛挓鍓�';
+ if (diff < 86400000) return Math.floor(diff / 3600000) + ' 灏忔椂鍓�';
+ var date = new Date(time);
+ return date.Format('MM-dd hh:mm');
+ },
+ findChat: function(chatId) {
+ for (var i = 0; i < this.chats.length; i++) {
+ if (this.chats[i] && this.chats[i].chatId === chatId) {
+ return this.chats[i];
+ }
+ }
+ return null;
+ },
+ openChat: function(chatId) {
+ if (!chatId || this.streaming) return;
+ this.currentChatId = chatId;
+ this.switchChat();
},
switchChat: function() {
var self = this;
- if (!self.currentChatId) { self.clear(); return; }
- fetch(baseUrl + '/ai/diagnose/chats/' + encodeURIComponent(self.currentChatId) + '/history', { headers: { 'token': localStorage.getItem('token') } })
- .then(function(r){ return r.json(); })
- .then(function(arr){
+ if (!self.currentChatId) {
+ self.clear();
+ return;
+ }
+ fetch(baseUrl + '/ai/diagnose/chats/' + encodeURIComponent(self.currentChatId) + '/history', { headers: self.authHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(arr) {
if (!Array.isArray(arr)) return;
var msgs = [];
- for (var i=0;i<arr.length;i++) {
- var m = arr[i];
- if (m.role === 'assistant') msgs.push({ role: 'assistant', md: m.content || '', html: self.renderMarkdown(m.content || '', false), ts: self.nowStr() });
- else msgs.push({ role: 'user', text: m.content || '', ts: self.nowStr() });
+ for (var i = 0; i < arr.length; i++) {
+ var m = arr[i] || {};
+ if (m.role === 'assistant') {
+ msgs.push({
+ role: 'assistant',
+ md: m.content || '',
+ html: self.renderMarkdown(m.content || '', false),
+ ts: self.nowStr()
+ });
+ } else {
+ msgs.push({
+ role: 'user',
+ text: m.content || '',
+ ts: self.nowStr()
+ });
+ }
}
self.messages = msgs;
- self.$nextTick(function(){ self.scrollToBottom(true); });
+ self.pendingText = '';
+ self.resetting = false;
+ self.$nextTick(function() { self.scrollToBottom(true); });
+ })
+ .catch(function() {
+ self.clear();
+ self.notifyError('鍔犺浇浼氳瘽鍘嗗彶澶辫触');
});
},
newChat: function() {
- var id = Date.now() + '_' + Math.random().toString(36).substr(2,8);
- this.currentChatId = id;
+ if (this.streaming) return;
+ this.currentChatId = Date.now() + '_' + Math.random().toString(36).substr(2, 8);
this.resetting = true;
this.clear();
},
deleteChat: function() {
var self = this;
- if (!self.currentChatId) return;
- fetch(baseUrl + '/ai/diagnose/chats/' + encodeURIComponent(self.currentChatId), { method: 'DELETE', headers: { 'token': localStorage.getItem('token') } })
- .then(function(r){ return r.json(); })
- .then(function(ok){ if (ok === true) { self.currentChatId = ''; self.clear(); self.loadChats(); self.newChat(); } });
+ if (!self.currentChatId || self.streaming) return;
+ fetch(baseUrl + '/ai/diagnose/chats/' + encodeURIComponent(self.currentChatId), {
+ method: 'DELETE',
+ headers: self.authHeaders()
+ })
+ .then(function(r) { return r.json(); })
+ .then(function(ok) {
+ if (ok === true) {
+ self.currentChatId = '';
+ self.clear();
+ self.loadChats(false);
+ }
+ })
+ .catch(function() {
+ self.notifyError('鍒犻櫎浼氳瘽澶辫触');
+ });
},
shouldAutoScroll: function() {
var el = this.$refs.chat;
@@ -210,28 +903,43 @@
}
},
nowStr: function() {
- var d = new Date();
- function pad(n) { return (n<10?'0':'') + n; }
- var y = d.getFullYear();
- var m = pad(d.getMonth() + 1);
- var day = pad(d.getDate());
- var hh = pad(d.getHours());
- var mm = pad(d.getMinutes());
- var ss = pad(d.getSeconds());
- return y + '-' + m + '-' + day + ' ' + hh + ':' + mm + ':' + ss;
+ return new Date().Format('yyyy-MM-dd hh:mm:ss');
+ },
+ handleComposerKeydown: function(e) {
+ if (!e) return;
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ this.ask();
+ }
+ },
+ applyPreset: function(preset, immediate) {
+ if (this.streaming || !preset) return;
+ if (!preset.prompt) {
+ this.start();
+ return;
+ }
+ this.userInput = preset.prompt;
+ if (immediate) {
+ this.ask();
+ }
+ },
+ appendAssistantPlaceholder: function() {
+ this.messages.push({ role: 'assistant', md: '', html: '', ts: this.nowStr() });
},
ask: function() {
if (this.streaming) return;
- var msg = (this.userInput || '').trim();
- if (!msg) return;
+ var message = (this.userInput || '').trim();
+ if (!message) return;
this.loading = true;
this.streaming = true;
- this.messages.push({ role: 'user', text: msg, ts: this.nowStr() });
- this.messages.push({ role: 'assistant', md: '', html: '', ts: this.nowStr() });
+ this.messages.push({ role: 'user', text: message, ts: this.nowStr() });
+ this.appendAssistantPlaceholder();
this.scrollToBottom(true);
- var url = baseUrl + '/ai/diagnose/askStream?prompt=' + encodeURIComponent(msg);
+
+ var url = baseUrl + '/ai/diagnose/askStream?prompt=' + encodeURIComponent(message);
if (this.currentChatId) url += '&chatId=' + encodeURIComponent(this.currentChatId);
if (this.resetting) url += '&reset=true';
+
this.source = new EventSource(url);
var self = this;
this.source.onopen = function() {
@@ -258,20 +966,18 @@
this.clear();
this.loading = true;
this.streaming = true;
- var url = baseUrl + '/ai/diagnose/runAiStream';
- this.source = new EventSource(url);
- this.messages.push({ role: 'assistant', md: '', html: '', ts: this.nowStr() });
+ this.appendAssistantPlaceholder();
this.scrollToBottom(true);
+
var self = this;
+ this.source = new EventSource(baseUrl + '/ai/diagnose/runAiStream');
this.source.onopen = function() {
self.loading = false;
};
this.source.onmessage = function(e) {
if (!e || !e.data) return;
- // 鍚庣鎶婃崲琛岃浆涔夋垚 \n锛岃繖閲岃繕鍘熶负鐪熸鐨勬崲琛�
var chunk = (e.data || '').replace(/\\n/g, '\n');
if (!chunk) return;
- // 濡傛灉浠呭寘鍚崲琛岀锛堝鍗曠嫭 \n 鎴� \n\n锛夛紝涓㈠純閬垮厤绌虹櫧琛�
var normalized = chunk.replace(/\r/g, '');
if (/^\n+$/.test(normalized)) return;
self.pendingText += chunk;
@@ -292,32 +998,32 @@
return;
}
if (self.pendingText.length > 0) {
- var n = Math.min(self.stepChars, self.pendingText.length);
- var part = self.pendingText.slice(0, n);
- self.pendingText = self.pendingText.slice(n);
- var last = self.messages.length > 0 ? self.messages[self.messages.length - 1] : null;
+ var count = Math.min(self.stepChars, self.pendingText.length);
+ var piece = self.pendingText.slice(0, count);
+ self.pendingText = self.pendingText.slice(count);
+ var last = self.messages.length ? self.messages[self.messages.length - 1] : null;
if (last && last.role === 'assistant') {
- last.md = (last.md || '') + part;
+ last.md = (last.md || '') + piece;
}
}
var now = Date.now();
if (now - self.lastRenderTs > self.renderIntervalMs) {
self.lastRenderTs = now;
- var last = self.messages.length > 0 ? self.messages[self.messages.length - 1] : null;
- if (last && last.role === 'assistant') {
- last.html = self.renderMarkdown(last.md || '', true);
+ var latest = self.messages.length ? self.messages[self.messages.length - 1] : null;
+ if (latest && latest.role === 'assistant') {
+ latest.html = self.renderMarkdown(latest.md || '', true);
self.$nextTick(function() { self.scrollToBottom(true); });
}
}
}, 50);
},
- stop: function() {
+ stop: function(skipReload) {
if (this.source) {
this.source.close();
this.source = null;
}
- var last = this.messages.length > 0 ? this.messages[this.messages.length - 1] : null;
- if (last && last.role === 'assistant' && this.pendingText && this.pendingText.length > 0) {
+ var last = this.messages.length ? this.messages[this.messages.length - 1] : null;
+ if (last && last.role === 'assistant' && this.pendingText) {
last.md = (last.md || '') + this.pendingText;
this.pendingText = '';
}
@@ -331,16 +1037,20 @@
last.html = this.renderMarkdown(last.md || '', false);
}
this.$nextTick(function() { this.scrollToBottom(true); }.bind(this));
- this.loadChats();
+ if (!skipReload) {
+ this.loadChats(true);
+ }
},
clear: function() {
this.messages = [];
this.pendingText = '';
}
- }
- ,mounted: function() {
- this.loadChats();
- this.newChat();
+ },
+ mounted: function() {
+ this.loadChats(false);
+ },
+ beforeDestroy: function() {
+ this.stop(true);
}
});
</script>
--
Gitblit v1.9.1