feat: 联调流式对话
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user