|
|
@@ -24,19 +24,17 @@ const DEFAULT_AI_QA_CONFIG = Object.freeze({
|
|
|
maxInputLength: Infinity,
|
|
|
acceptedFileExtensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx'],
|
|
|
uploadHint: '',
|
|
|
- inputHint: '发消息,或上传附件...',
|
|
|
+ inputHint: '请输入或粘贴学员拜访记录文字稿…',
|
|
|
enableFileUpload: false,
|
|
|
enableContext: true,
|
|
|
- enableVoicePlayback: true,
|
|
|
+ enableVoicePlayback: false,//语音
|
|
|
typewriterDelay: 28,
|
|
|
fastTypewriterDelay: 8,
|
|
|
maxInputHeight: 180,
|
|
|
voiceLanguage: 'zh-CN',
|
|
|
voiceMaxDuration: 30,
|
|
|
suggestions: [
|
|
|
- '开始陪练',
|
|
|
- '你都知道什么',
|
|
|
- '我需要知识点....'
|
|
|
+ '请根据标准评分规则对以下学员拜访记录进行评分'
|
|
|
]
|
|
|
})
|
|
|
|
|
|
@@ -147,7 +145,7 @@ const loadConversationList = () => {
|
|
|
conversations.value = Array.isArray(stored)
|
|
|
? stored.filter((item) => item?.id).map((item) => ({
|
|
|
id: item.id,
|
|
|
- title: item.title || '未命名对话',
|
|
|
+ title: item.title || '未命名评分记录',
|
|
|
updatedAt: Number(item.updatedAt) || Date.now(),
|
|
|
messages: Array.isArray(item.messages) ? item.messages : []
|
|
|
})).sort((a, b) => b.updatedAt - a.updatedAt)
|
|
|
@@ -161,7 +159,7 @@ const loadConversationList = () => {
|
|
|
const getConversationTitle = () => {
|
|
|
const firstQuestion = messages.value.find((message) => message.type === 'user')
|
|
|
if (!firstQuestion) return ''
|
|
|
- return firstQuestion.content?.trim() || firstQuestion.attachments?.[0]?.name || '附件问答'
|
|
|
+ return firstQuestion.content?.trim() || firstQuestion.attachments?.[0]?.name || '拜访记录评分'
|
|
|
}
|
|
|
|
|
|
const persistConversation = () => {
|
|
|
@@ -473,14 +471,14 @@ const copyMessageContent = async (content, event) => {
|
|
|
} else if (navigator.clipboard?.writeText) {
|
|
|
await navigator.clipboard.writeText(content)
|
|
|
}
|
|
|
- Message().success('已复制 AI 回复', true)
|
|
|
+ Message().success('已复制评分结果', true)
|
|
|
} catch (error) {
|
|
|
- console.error('复制 AI 回复失败', error)
|
|
|
+ console.error('复制评分结果失败', error)
|
|
|
Message().error('复制失败,请手动选择内容复制', true)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-const getRequestContent = (content, script) => script ? `评分参考话术:\n${script}\n\n用户问题:\n${content}` : content
|
|
|
+const getRequestContent = (content, script) => script ? `补充评分规则:\n${script}\n\n学员拜访记录:\n${content}` : content
|
|
|
const selectFile = () => fileInputRef.value?.click()
|
|
|
const formatSize = (size) => size < 1024 * 1024 ? `${Math.max(1, Math.round(size / 1024))} KB` : `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
|
const getFileExtension = (fileName) => fileName.split('.').pop()?.toLowerCase() || ''
|
|
|
@@ -504,7 +502,7 @@ const removeAttachment = (index) => attachments.value.splice(index, 1)
|
|
|
|
|
|
const sendMessage = async () => {
|
|
|
if (!canSend.value) return
|
|
|
- if (hasInputLengthLimit.value && input.value.length > AI_QA_CONFIG.maxInputLength) return Message().warning(`单次提问最多输入 ${AI_QA_CONFIG.maxInputLength.toLocaleString()} 个字符`, true)
|
|
|
+ if (hasInputLengthLimit.value && input.value.length > AI_QA_CONFIG.maxInputLength) return Message().warning(`单次最多输入 ${AI_QA_CONFIG.maxInputLength.toLocaleString()} 个字符`, true)
|
|
|
stopPlayback()
|
|
|
stopTypewriter()
|
|
|
const content = input.value.trim()
|
|
|
@@ -515,7 +513,7 @@ const sendMessage = async () => {
|
|
|
input.value = ''
|
|
|
attachments.value = []
|
|
|
userHasScrolled.value = false
|
|
|
- messages.value.push({ type: 'user', content: content || '请查看我上传的附件。', attachments: files, referenceScript: script })
|
|
|
+ messages.value.push({ type: 'user', content: content || '请根据标准评分规则对我上传的学员拜访记录进行评分。', attachments: files, referenceScript: script })
|
|
|
messages.value.push({
|
|
|
id: null,
|
|
|
type: 'assistant',
|
|
|
@@ -592,7 +590,7 @@ const sendMessage = async () => {
|
|
|
assistantMessage.pending = false
|
|
|
throw error
|
|
|
}, () => {
|
|
|
- assistantMessage.content ||= '暂未收到有效回复,请换一种方式提问。'
|
|
|
+ assistantMessage.content ||= '暂未收到有效评分结果,请稍后重试。'
|
|
|
assistantMessage.pending = false
|
|
|
assistantMessage.audioStreamEnded = true
|
|
|
if (!assistantMessage.hasAudio || !getIsPlaying()) {
|
|
|
@@ -683,11 +681,11 @@ onUnmounted(() => {
|
|
|
<span>非临 AI 助教</span>
|
|
|
</div>
|
|
|
<div class="header-actions">
|
|
|
- <button class="header-toggle" :class="{ active: voicePlaybackEnabled }" type="button" @click="toggleVoicePlayback">
|
|
|
- <span class="speaker-dot"></span>{{ voicePlaybackEnabled ? '语音播报已开启' : '语音播报已关闭' }}
|
|
|
- </button>
|
|
|
+<!-- <button class="header-toggle" :class="{ active: voicePlaybackEnabled }" type="button" @click="toggleVoicePlayback">-->
|
|
|
+<!-- <span class="speaker-dot"></span>{{ voicePlaybackEnabled ? '语音播报已开启' : '语音播报已关闭' }}-->
|
|
|
+<!-- </button>-->
|
|
|
<button class="new-conversation" type="button" @click="startNewConversation">
|
|
|
- <el-icon><RefreshRight /></el-icon><span>新建对话</span>
|
|
|
+ <el-icon><RefreshRight /></el-icon><span>新建评分</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
</header>
|
|
|
@@ -695,39 +693,49 @@ onUnmounted(() => {
|
|
|
<section class="qa-workspace" :class="{ 'history-collapsed': !historyDrawerOpen }">
|
|
|
<aside class="conversation-history">
|
|
|
<div class="history-heading">
|
|
|
- <div><span class="history-overline">HISTORY</span><h2>历史问答</h2></div>
|
|
|
+ <div><span class="history-overline">HISTORY</span><h2>历史评分</h2></div>
|
|
|
<button class="history-drawer-toggle" type="button" :title="historyDrawerOpen ? '收起历史记录' : '展开历史记录'" @click="historyDrawerOpen = !historyDrawerOpen"><el-icon><ArrowLeft v-if="historyDrawerOpen" /><ArrowRight v-else /></el-icon></button>
|
|
|
</div>
|
|
|
- <button class="history-new" type="button" @click="startNewConversation"><el-icon><Plus /></el-icon>发起新的问答</button>
|
|
|
+ <button class="history-new" type="button" @click="startNewConversation"><el-icon><Plus /></el-icon>发起新的评分</button>
|
|
|
<div v-if="conversations.length" class="history-list">
|
|
|
<button v-for="conversation in conversations" :key="conversation.id" class="history-item" :class="{ active: conversation.id === conversationId }" type="button" @click="selectConversation(conversation)">
|
|
|
<span class="history-item-title">{{ conversation.title }}</span>
|
|
|
<span class="history-item-time">{{ formatHistoryTime(conversation.updatedAt) }}</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
- <div v-else class="history-empty"><span>暂无历史记录</span><small>你的问答将在此处保存</small></div>
|
|
|
+ <div v-else class="history-empty"><span>暂无历史记录</span><small>你的评分将在此处保存</small></div>
|
|
|
</aside>
|
|
|
|
|
|
<section class="right-workspace">
|
|
|
<aside class="digital-human-panel" :class="{ speaking: isAudioSpeaking }">
|
|
|
- <div class="digital-human-copy"><span>HOSPITAL AI</span><h2>非临 AI 助教</h2><p>随时为您解答健康与服务问题</p></div>
|
|
|
+ <div class="digital-human-copy"><span>AI ASSISTANT</span><h2>非临 AI 助教</h2><p>我将依据标准评分规则,对学员拜访记录文字稿进行客观评分。</p></div>
|
|
|
<div class="digital-human-stage">
|
|
|
<span class="stage-orbit orbit-one"></span><span class="stage-orbit orbit-two"></span>
|
|
|
- <img class="digital-human-image" src="@/assets/images/ai-qa/doctor-avatar-cropped.png" alt="非临 AI 助教" />
|
|
|
+ <video
|
|
|
+ ref="digitalHumanVideoRef"
|
|
|
+ class="digital-human-video"
|
|
|
+ src="@/assets/images/ai-qa/ai-qa.mp4"
|
|
|
+ poster="@/assets/images/ai-qa/ai-qa-first-frame.png"
|
|
|
+ muted
|
|
|
+ playsinline
|
|
|
+ preload="auto"
|
|
|
+ @loadedmetadata="handleDigitalHumanVideoReady"
|
|
|
+ @ended="resetDigitalHumanVideo"
|
|
|
+ ></video>
|
|
|
</div>
|
|
|
<div class="digital-human-status"><i></i>{{ isAudioSpeaking ? '正在为您播报' : '在线为您服务' }}</div>
|
|
|
</aside>
|
|
|
|
|
|
<section class="chat-panel">
|
|
|
<div v-if="!hasConversation" class="welcome empty-conversation">
|
|
|
- <p class="eyebrow">AI MEDICAL ASSISTANT</p><h1>开始一次新的问答</h1>
|
|
|
- <p class="welcome-copy">向非临 AI 助教描述您的问题,获得及时的智能解答。</p>
|
|
|
- <button class="initial-conversation-button" type="button" :disabled="isCreatingConversation" @click="makeConversation"><el-icon><Plus /></el-icon>智能问答</button>
|
|
|
+ <p class="eyebrow">AI ASSISTANT</p><h1>开始一次新的评分</h1>
|
|
|
+ <p class="welcome-copy">提交或上传学员拜访记录文字稿,AI 将依据标准评分规则生成评分结果。</p>
|
|
|
+ <button class="initial-conversation-button" type="button" :disabled="isCreatingConversation" @click="makeConversation"><el-icon><Plus /></el-icon>开始评分</button>
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="!hasMessages" class="welcome">
|
|
|
- <p class="eyebrow">AI MEDICAL ASSISTANT</p><h1>有什么可以帮您?</h1>
|
|
|
- <p class="welcome-copy">请输入问题,您的本次对话将自动保存到左侧历史记录。</p>
|
|
|
+ <p class="eyebrow">AI ASSISTANT</p><h1>提交拜访记录进行评分</h1>
|
|
|
+ <p class="welcome-copy">请输入或上传学员拜访记录文字稿;本次评分将自动保存到左侧历史记录。</p>
|
|
|
<div class="suggestion-list"><button v-for="suggestion in AI_QA_CONFIG.suggestions" :key="suggestion" type="button" @click="chooseSuggestion(suggestion)"><span>{{ suggestion }}</span><el-icon><ArrowUp /></el-icon></button></div>
|
|
|
</div>
|
|
|
|
|
|
@@ -743,7 +751,7 @@ onUnmounted(() => {
|
|
|
:citation-ids="message.segments?.map((item) => item.id) || []"
|
|
|
@citation-click="openCitationById(message, $event)"
|
|
|
/>
|
|
|
- <div v-if="message.segments?.length" class="citation-list">
|
|
|
+<!-- <div v-if="message.segments?.length" class="citation-list">
|
|
|
<button class="citation-summary" type="button" @click="toggleCitationList(message)">
|
|
|
<span class="citation-summary-name">《{{ message.segments[0]?.documentName || '知识库文档' }}》</span>
|
|
|
<span class="citation-summary-toggle">{{ getCitationSummaryAction(message) }}</span>
|
|
|
@@ -761,13 +769,13 @@ onUnmounted(() => {
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </div>-->
|
|
|
</template>
|
|
|
<p v-else class="user-content">{{ message.content }}</p>
|
|
|
<div v-if="message.attachments?.length" class="attachment-list"><div v-for="file in message.attachments" :key="file.id" class="attachment-card"><el-icon><Paperclip /></el-icon><span class="attachment-name">{{ file.name }}</span><small>{{ formatSize(file.size) }}</small></div></div>
|
|
|
- <div v-if="message.referenceScript" class="reference-script-summary"><span>评分参考话术</span><p>{{ message.referenceScript }}</p></div>
|
|
|
+ <div v-if="message.referenceScript" class="reference-script-summary"><span>补充评分规则</span><p>{{ message.referenceScript }}</p></div>
|
|
|
<button v-if="voicePlaybackEnabled && message.type === 'assistant' && message.showVoiceStop" class="stop-voice-button" type="button" @click="stopVoicePlayback(message)"><el-icon><VideoPause /></el-icon>停止播报</button>
|
|
|
- <div v-if="message.type === 'assistant' && !message.pending && getAnswerBody(message)" class="assistant-message-actions"><button class="copy-message-button" type="button" title="复制回复" @click="copyMessageContent(getAnswerBody(message), $event)"><el-icon><DocumentCopy /></el-icon></button></div>
|
|
|
+ <div v-if="message.type === 'assistant' && !message.pending && getAnswerBody(message)" class="assistant-message-actions"><button class="copy-message-button" type="button" title="复制评分结果" @click="copyMessageContent(getAnswerBody(message), $event)"><el-icon><DocumentCopy /></el-icon></button></div>
|
|
|
</div>
|
|
|
</article>
|
|
|
</div>
|
|
|
@@ -776,9 +784,9 @@ onUnmounted(() => {
|
|
|
<p v-if="uploadEnabled && AI_QA_CONFIG.uploadHint" class="composer-limit-hint">{{ AI_QA_CONFIG.uploadHint }}</p>
|
|
|
<div v-if="uploadEnabled && attachments.length" class="pending-files"><div v-for="(file, index) in attachments" :key="file.id" class="pending-file"><el-icon><Paperclip /></el-icon><span>{{ file.name }}</span><button type="button" aria-label="移除附件" @click="removeAttachment(index)">×</button></div></div>
|
|
|
<div class="composer" :class="{ recording: isVoiceRecording, 'reference-script-open': referenceScriptEnabled && referenceScriptVisible }">
|
|
|
- <label v-if="referenceScriptEnabled && referenceScriptVisible" class="reference-script-field"><span class="reference-script-label">评分参考话术 <em>选填</em></span><textarea v-model="referenceScript" class="reference-script-textarea" rows="4" :maxlength="AI_QA_CONFIG.referenceScriptMaxLength" :disabled="isSending" placeholder="可填写评分规则中指定项的参考话术,AI 将据此辅助评分"></textarea><small>{{ referenceScript.length }}/{{ AI_QA_CONFIG.referenceScriptMaxLength }}</small></label>
|
|
|
+ <label v-if="referenceScriptEnabled && referenceScriptVisible" class="reference-script-field"><span class="reference-script-label">补充评分规则 <em>选填</em></span><textarea v-model="referenceScript" class="reference-script-textarea" rows="4" :maxlength="AI_QA_CONFIG.referenceScriptMaxLength" :disabled="isSending" placeholder="可补充本次评分所适用的标准、维度或要求,AI 将据此评分"></textarea><small>{{ referenceScript.length }}/{{ AI_QA_CONFIG.referenceScriptMaxLength }}</small></label>
|
|
|
<textarea ref="textareaRef" v-model="input" class="ai-qa-textarea" :maxlength="hasInputLengthLimit ? AI_QA_CONFIG.maxInputLength : undefined" :placeholder="AI_QA_CONFIG.inputHint" :disabled="isSending" @keydown="handleKeydown"></textarea>
|
|
|
- <div class="composer-toolbar"><div class="composer-tools"><input ref="fileInputRef" class="file-input" type="file" accept="image/*,.pdf,.doc,.docx,.xls,.xlsx" @change="handleFileChange" /><button v-if="uploadEnabled" class="add-button" type="button" title="上传附件" :disabled="isSending" @click="selectFile"><el-icon><Plus /></el-icon></button><VoiceInputDoubao input-selector=".ai-qa-textarea" :lang="AI_QA_CONFIG.voiceLanguage" :max-duration="AI_QA_CONFIG.voiceMaxDuration" :disabled="isSending" @voice-recognized="onVoiceRecognized" @recording-status-changed="isVoiceRecording = $event" /><button v-if="referenceScriptEnabled" class="reference-script-toggle" :class="{ active: referenceScriptVisible }" type="button" :disabled="isSending" @click="referenceScriptVisible = !referenceScriptVisible">评分话术</button></div><button v-if="isSending || isTyping" class="send-button stop-button" type="button" @click="stopResponse"><el-icon><VideoPause /></el-icon></button><button v-else class="send-button" type="button" :disabled="!canSend" @click="sendMessage"><el-icon><ArrowUp /></el-icon></button></div>
|
|
|
+ <div class="composer-toolbar"><div class="composer-tools"><input ref="fileInputRef" class="file-input" type="file" accept="image/*,.pdf,.doc,.docx,.xls,.xlsx" @change="handleFileChange" /><button v-if="uploadEnabled" class="add-button" type="button" title="上传拜访记录附件" :disabled="isSending" @click="selectFile"><el-icon><Plus /></el-icon></button><VoiceInputDoubao input-selector=".ai-qa-textarea" :lang="AI_QA_CONFIG.voiceLanguage" :max-duration="AI_QA_CONFIG.voiceMaxDuration" :disabled="isSending" @voice-recognized="onVoiceRecognized" @recording-status-changed="isVoiceRecording = $event" /><button v-if="referenceScriptEnabled" class="reference-script-toggle" :class="{ active: referenceScriptVisible }" type="button" :disabled="isSending" @click="referenceScriptVisible = !referenceScriptVisible">评分规则</button></div><button v-if="isSending || isTyping" class="send-button stop-button" type="button" @click="stopResponse"><el-icon><VideoPause /></el-icon></button><button v-else class="send-button" type="button" :disabled="!canSend" @click="sendMessage"><el-icon><ArrowUp /></el-icon></button></div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</section>
|
|
|
@@ -860,6 +868,10 @@ onUnmounted(() => {
|
|
|
.history-list::-webkit-scrollbar-thumb:hover, .reference-script-textarea::-webkit-scrollbar-thumb:hover { background-color: rgba(180, 216, 255, .92); }
|
|
|
.mouth-sync { top: 34.2%; left: 49.5%; width: 12.5%; height: 1.15%; min-height: 5px; border-radius: 0 0 999px 999px; background: linear-gradient(180deg, #632b37, #963d4d); box-shadow: 0 1px 1px rgba(255, 209, 207, .22) inset; transform: translateX(-50%) scale(.72, .58); transform-origin: center top; }
|
|
|
.digital-human-video { position: relative; z-index: 2; display: block; width: min(100%, 330px); max-height: 430px; object-fit: contain; filter: drop-shadow(0 22px 22px rgba(0, 0, 0, .28)); }
|
|
|
+.digital-human-panel { --digital-human-panel-padding: clamp(20px, 2vw, 30px); background: linear-gradient(180deg, #103261, #0a2148); backdrop-filter: none; }
|
|
|
+.digital-human-stage { margin-inline: calc(0px - var(--digital-human-panel-padding)); overflow: hidden; background: #0e2e59; }
|
|
|
+.digital-human-video { position: absolute; inset: 0; width: 100%; height: 100%; max-height: none; object-fit: cover; filter: none; }
|
|
|
+.digital-human-stage .stage-orbit { display: none; }
|
|
|
.digital-human-panel.speaking .digital-human-image { animation: none; }
|
|
|
.digital-human-panel.speaking .mouth-sync { opacity: .78; animation: medical-mouth-talk .3s ease-in-out infinite alternate; }
|
|
|
.digital-human-panel.speaking .stage-orbit { border-color: rgba(139, 204, 255, .72); box-shadow: inset 0 0 44px rgba(84, 158, 255, .28), 0 0 38px rgba(71, 151, 255, .4); }
|
|
|
@@ -869,5 +881,7 @@ onUnmounted(() => {
|
|
|
@keyframes orbit { to { transform: rotate(360deg); } } @keyframes person-talk { to { transform: translateY(-3px) rotate(-.5deg); } } @keyframes mouth-talk { to { height: 4.2%; width: 9%; } } @keyframes medical-mouth-talk { from { transform: translateX(-50%) scale(.72, .58); } to { transform: translateX(-50%) scale(1, 1.7); } } @keyframes halo-pulse { to { transform: scale(1.1); opacity: .72; } } @keyframes halo-glow { to { opacity: .98; filter: brightness(1.35); } } @keyframes pulse { 50% { transform: translateY(-3px); opacity: .45; } }
|
|
|
@media (max-width: 1100px) { .digital-human-panel { min-width: 205px; padding: 18px; }.digital-human-copy h2 { font-size: 20px; }.suggestion-list { grid-template-columns: 1fr; width: min(100%, 380px); }.suggestion-list button { min-height: 54px; }.header-toggle { display: none; } }
|
|
|
@media (max-width: 900px) { .conversation-history { width: 200px; padding-right: 10px; padding-left: 10px; }.right-workspace { padding: 12px; gap: 12px; }.digital-human-panel { min-width: 185px; padding: 14px; }.digital-human-copy p { font-size: 12px; }.messages { padding-right: 20px; padding-left: 20px; } }
|
|
|
-@media (max-width: 760px) { .page-header { height: 62px; padding: 0 15px; }.brand { font-size: 16px; }.new-conversation span { display: none; }.new-conversation { width: 36px; justify-content: center; padding: 0; }.qa-workspace { height: calc(100dvh - 62px); min-height: 0; }.conversation-history { width: 72px; padding: 15px 8px; }.history-heading { justify-content: center; padding: 0 0 14px; }.history-heading > div, .history-new { display: none; }.history-list { padding: 0; }.history-item { display: block; padding: 10px 4px; text-align: center; }.history-item-title { display: block; font-size: 0; }.history-item-title::before { color: #b7d4ff; content: '问答'; font-size: 11px; }.history-item-time { display: none; }.right-workspace { padding: 9px; gap: 0; }.digital-human-panel { display: none; }.chat-panel { border-radius: 14px; }.welcome { padding: 20px; }.welcome h1 { font-size: 28px; }.messages { padding: 20px 15px; }.message-card { max-width: 86%; }.composer-wrap { padding: 9px 10px 12px; } }
|
|
|
+@media (max-width: 760px) { .page-header { height: 62px; padding: 0 15px; }.brand { font-size: 16px; }.new-conversation span { display: none; }.new-conversation { width: 36px; justify-content: center; padding: 0; }.qa-workspace { height: calc(100dvh - 62px); min-height: 0; }.conversation-history { width: 72px; padding: 15px 8px; }.history-heading { justify-content: center; padding: 0 0 14px; }.history-heading > div, .history-new { display: none; }.history-list { padding: 0; }.history-item { display: block; padding: 10px 4px; text-align: center; }.history-item-title { display: block; font-size: 0; }.history-item-title::before { color: #b7d4ff; content: '评分'; font-size: 11px; }.history-item-time { display: none; }.right-workspace { padding: 9px; gap: 0; }.digital-human-panel { display: none; }.chat-panel { border-radius: 14px; }.welcome { padding: 20px; }.welcome h1 { font-size: 28px; }.messages { padding: 20px 15px; }.message-card { max-width: 86%; }.composer-wrap { padding: 9px 10px 12px; } }
|
|
|
+@media (max-width: 1100px) { .digital-human-panel { --digital-human-panel-padding: 18px; } }
|
|
|
+@media (max-width: 900px) { .digital-human-panel { --digital-human-panel-padding: 14px; } }
|
|
|
</style>
|