Files
agentic-coding-workflow/web/src/views/chat/ChatDetailView.vue
T

197 lines
5.3 KiB
Vue

<template>
<AppShell>
<div class="flex h-[calc(100vh-8rem)] -m-6">
<!-- 左侧会话列表 -->
<div class="w-72 border-r overflow-y-auto p-2 flex flex-col gap-1">
<NButton
block
class="mb-2"
@click="showCreate = true"
>
+ 新建对话
</NButton>
<div
v-for="c in chat.conversations"
:key="c.id"
:class="[
'p-2 rounded cursor-pointer hover:bg-neutral-100',
chat.currentConversation?.id === c.id ? 'bg-blue-50' : '',
]"
@click="open(c.id)"
>
<div class="truncate text-sm">
{{ c.title || '(未命名)' }}
</div>
<div class="text-xs text-neutral-400">
{{ formatTime(c.updated_at) }}
</div>
</div>
<div
v-if="chat.conversations.length === 0"
class="text-xs text-neutral-400 px-2"
>
暂无对话
</div>
</div>
<!-- 右侧消息流 -->
<div class="flex-1 flex flex-col">
<div
v-if="!chat.currentConversation"
class="flex-1 flex items-center justify-center text-neutral-400"
>
选择或新建会话
</div>
<template v-else>
<div
ref="scrollEl"
class="flex-1 overflow-y-auto p-4 space-y-3"
>
<MessageBubble
v-for="m in chat.messages"
:key="m.id"
:message="m"
@retry="onRetry"
/>
</div>
<div class="border-t p-3 space-y-2">
<AttachmentUploader
ref="uploaderRef"
:model-capabilities="currentModelCaps"
@change="onAttachmentChange"
/>
<div class="flex gap-2">
<NInput
v-model:value="draft"
type="textarea"
:autosize="{ minRows: 1, maxRows: 6 }"
placeholder="发送消息(Enter 发送,Shift+Enter 换行)"
:disabled="chat.isStreaming"
@keydown.enter.exact.prevent="send"
/>
<NButton
v-if="!chat.isStreaming"
type="primary"
:disabled="!draft.trim()"
@click="send"
>
发送
</NButton>
<NButton
v-else
type="warning"
@click="chat.cancelMessage"
>
中断
</NButton>
</div>
</div>
</template>
</div>
<ConversationCreateModal
v-model:show="showCreate"
:project-id="projectId"
@created="onCreated"
/>
</div>
</AppShell>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { NButton, NInput } from 'naive-ui'
import AppShell from '@/layouts/AppShell.vue'
import { useChatStore } from '@/stores/chat'
import MessageBubble from './components/MessageBubble.vue'
import AttachmentUploader from './components/AttachmentUploader.vue'
import ConversationCreateModal from './ConversationCreateModal.vue'
import { llmAdminApi, type LLMModel, type ModelCapabilities } from '@/api/llmAdmin'
const chat = useChatStore()
const route = useRoute()
const router = useRouter()
const draft = ref('')
const attachmentIds = ref<string[]>([])
const showCreate = ref(false)
const uploaderRef = ref<InstanceType<typeof AttachmentUploader> | null>(null)
const scrollEl = ref<HTMLElement | null>(null)
const allModels = ref<LLMModel[]>([])
const projectId = computed(() => {
const v = route.params.pid
return typeof v === 'string' && v !== '' ? v : undefined
})
const defaultCaps: ModelCapabilities = {
image: false,
pdf: false,
audio: false,
reasoning: false,
tools: false,
}
const currentModelCaps = computed<ModelCapabilities>(() => {
const id = chat.currentConversation?.model_id
if (!id) return defaultCaps
const m = allModels.value.find((x) => x.id === id)
return m?.capabilities ?? defaultCaps
})
onMounted(async () => {
allModels.value = await llmAdminApi.listModels(true)
await chat.loadConversations({ project_id: projectId.value })
const cid = route.params.conversationId
if (typeof cid === 'string' && cid !== '') {
await open(cid)
}
})
watch(
() => chat.messages.length,
() => {
nextTick(() => {
const el = scrollEl.value
if (el) el.scrollTo(0, el.scrollHeight)
})
},
)
async function open(id: string) {
await chat.openConversation(id)
await router.replace({
path: projectId.value ? `/projects/${projectId.value}/chat/${id}` : `/chat/${id}`,
})
}
async function send() {
if (!draft.value.trim() || chat.isStreaming) return
const body = draft.value
const ids = [...attachmentIds.value]
draft.value = ''
uploaderRef.value?.clear()
attachmentIds.value = []
await chat.sendMessage(body, ids)
}
function onAttachmentChange(ids: string[]) {
attachmentIds.value = ids
}
function onRetry(id: number) {
chat.retryMessage(id)
}
async function onCreated(id: string) {
await chat.loadConversations({ project_id: projectId.value })
await open(id)
}
function formatTime(iso: string) {
return new Date(iso).toLocaleString('zh-CN', { hour12: false })
}
</script>