feat: 联调流式对话

This commit is contained in:
王天骄
2026-06-11 17:48:46 +08:00
parent cdbe16dde3
commit 37716f200b
29 changed files with 473 additions and 219 deletions
+80 -36
View File
@@ -104,7 +104,7 @@
class="message-input"
v-model="draft"
auto-height
maxlength="500"
maxlength="1000"
placeholder="请输入您的问题..."
placeholder-class="input-placeholder"
@confirm="handleSend"
@@ -113,7 +113,13 @@
<button class="attach-button" aria-label="附件" @click="showToast('附件上传即将开放')">
<view class="attach-icon"></view>
</button>
<button class="send-button" aria-label="发送" @click="handleSend">
<button
class="send-button"
:class="{ disabled: sending }"
:disabled="sending"
aria-label="发送"
@click="handleSend"
>
<view class="send-icon"></view>
</button>
</view>
@@ -167,6 +173,11 @@
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import {
createLearningAssistantSession,
streamLearningAssistantChat,
type LearningAssistantSession
} from '../../api/learning-assistant'
import { createHomeNavigator } from '../../api/navigation'
type AssistantMessage = {
@@ -192,19 +203,7 @@ const pathwaySteps = [
{ index: '3', title: '再灌注策略', description: 'STEMI需紧急PCI。' }
]
const messages = ref<AssistantMessage[]>([
{
id: 'sample-user',
role: 'user',
content: '你能解释一下急性冠脉综合征(ACS)的最新临床路径吗?'
},
{
id: 'sample-ai',
role: 'assistant',
content: '',
variant: 'acs-pathway'
}
])
const messages = ref<AssistantMessage[]>([])
const draft = ref('')
const modalVisible = ref(false)
@@ -212,10 +211,11 @@ const typingVisible = ref(false)
const toastMessage = ref('')
const toastVisible = ref(false)
const scrollTop = ref(0)
const assistantSession = ref<LearningAssistantSession | null>(null)
const sending = ref(false)
let typingTimer: ReturnType<typeof setTimeout> | null = null
let pulseTimer: ReturnType<typeof setInterval> | null = null
let toastTimer: ReturnType<typeof setTimeout> | null = null
let streamAbortController: AbortController | null = null
function useQuickAction(action: string) {
const prompts: Record<string, string> = {
@@ -227,12 +227,13 @@ function useQuickAction(action: string) {
draft.value = prompts[action] || action
}
function handleSend() {
async function handleSend() {
const value = draft.value.trim()
if (!value) {
showToast('请输入问题')
return
}
if (sending.value) return
messages.value.push({
id: `user-${Date.now()}`,
@@ -241,19 +242,65 @@ function handleSend() {
})
draft.value = ''
typingVisible.value = true
sending.value = true
scrollToBottom()
if (typingTimer) clearTimeout(typingTimer)
typingTimer = setTimeout(() => {
const assistantMessageIndex = messages.value.length
messages.value.push({
id: `assistant-${Date.now()}`,
role: 'assistant',
variant: 'simple',
content: ''
})
try {
const session = await ensureAssistantSession(value)
streamAbortController?.abort()
streamAbortController = new AbortController()
await streamLearningAssistantChat(
session.assistant_session_id,
{ question: value },
{
onDelta: delta => {
messages.value[assistantMessageIndex].content += delta
scrollToBottom()
}
},
streamAbortController.signal
)
if (!messages.value[assistantMessageIndex].content.trim()) {
messages.value[assistantMessageIndex].content = '暂未生成回复,请稍后重试。'
}
} catch (error) {
messages.value[assistantMessageIndex].content = error instanceof Error ? error.message : 'AI 学习助手回复失败'
showToast(messages.value[assistantMessageIndex].content)
} finally {
typingVisible.value = false
messages.value.push({
id: `assistant-${Date.now()}`,
role: 'assistant',
variant: 'simple',
content: '已收到。我会结合医院知识库、临床路径和指南证据,为你整理成可用于带教复盘的要点。'
})
sending.value = false
scrollToBottom()
}, 900)
}
}
async function ensureAssistantSession(title: string) {
if (assistantSession.value) return assistantSession.value
const session = await createLearningAssistantSession(title)
assistantSession.value = session
uni.setStorageSync('clinical-thinking-learning-assistant-session', session)
return session
}
async function initializeAssistantSession() {
try {
const storedSession = uni.getStorageSync('clinical-thinking-learning-assistant-session')
if (storedSession && typeof storedSession === 'object') {
assistantSession.value = storedSession as LearningAssistantSession
return
}
assistantSession.value = await createLearningAssistantSession('AI 学习助手')
uni.setStorageSync('clinical-thinking-learning-assistant-session', assistantSession.value)
} catch (error) {
showToast(error instanceof Error ? error.message : '新建会话失败')
}
}
function scrollToBottom() {
@@ -272,18 +319,11 @@ function showToast(message: string) {
}
onMounted(() => {
pulseTimer = setInterval(() => {
if (typingVisible.value) return
typingVisible.value = true
setTimeout(() => {
typingVisible.value = false
}, 2400)
}, 12000)
void initializeAssistantSession()
})
onUnmounted(() => {
if (typingTimer) clearTimeout(typingTimer)
if (pulseTimer) clearInterval(pulseTimer)
streamAbortController?.abort()
if (toastTimer) clearTimeout(toastTimer)
})
</script>
@@ -754,6 +794,10 @@ page {
background: #00478d;
}
.send-button.disabled {
opacity: 0.5;
}
.send-button:active {
transform: scale(0.95);
}