You've already forked agentic-coding-workflow
主流程
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<NCard
|
||||
:title="`AI 对话 · ${PHASE_TITLE[phase]}`"
|
||||
class="flex flex-col h-full"
|
||||
content-class="flex flex-col flex-1 min-h-0"
|
||||
>
|
||||
<!-- 会话选择条 -->
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<NSelect
|
||||
:value="session.currentConversation.value?.id ?? null"
|
||||
:options="conversationOptions"
|
||||
placeholder="选择历史会话"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
:disabled="creating"
|
||||
@update:value="onSelectConversation"
|
||||
/>
|
||||
<NButton
|
||||
size="small"
|
||||
type="primary"
|
||||
secondary
|
||||
@click="openCreateForm"
|
||||
>
|
||||
新建会话
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<!-- 新建会话表单 -->
|
||||
<div
|
||||
v-if="showCreateForm"
|
||||
class="space-y-2 mb-3 border rounded p-3"
|
||||
>
|
||||
<ModelSelector v-model:model-value="newModelId" />
|
||||
<NInput
|
||||
v-model:value="newSystemPrompt"
|
||||
type="textarea"
|
||||
placeholder="system prompt(已自动注入需求上下文,可修改)"
|
||||
:autosize="{ minRows: 4, maxRows: 10 }"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<NButton
|
||||
size="small"
|
||||
@click="showCreateForm = false"
|
||||
>
|
||||
取消
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="creating"
|
||||
:disabled="!newModelId"
|
||||
@click="onCreateConversation"
|
||||
>
|
||||
创建并开始
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息区 -->
|
||||
<div
|
||||
v-if="session.currentConversation.value"
|
||||
ref="scrollEl"
|
||||
class="flex-1 min-h-0 overflow-y-auto space-y-3 py-2"
|
||||
>
|
||||
<MessageBubble
|
||||
v-for="m in session.messages.value"
|
||||
:key="m.id"
|
||||
:message="m"
|
||||
:savable="!readonly"
|
||||
@retry="session.retryMessage"
|
||||
@save="onSaveMessage"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex-1 flex items-center justify-center text-neutral-400 text-sm"
|
||||
>
|
||||
选择历史会话,或新建会话开始与 AI 讨论本阶段内容
|
||||
</div>
|
||||
|
||||
<!-- 输入区 -->
|
||||
<div
|
||||
v-if="session.currentConversation.value"
|
||||
class="border-t pt-3 flex gap-2"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="draft"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 1, maxRows: 6 }"
|
||||
placeholder="发送消息(Enter 发送,Shift+Enter 换行)"
|
||||
:disabled="session.isStreaming.value"
|
||||
@keydown.enter.exact.prevent="send"
|
||||
/>
|
||||
<NButton
|
||||
v-if="!session.isStreaming.value"
|
||||
type="primary"
|
||||
:disabled="!draft.trim()"
|
||||
@click="send"
|
||||
>
|
||||
发送
|
||||
</NButton>
|
||||
<NButton
|
||||
v-else
|
||||
type="warning"
|
||||
@click="session.cancelMessage"
|
||||
>
|
||||
中断
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<SaveArtifactModal
|
||||
v-model:show="showSaveModal"
|
||||
:slug="slug"
|
||||
:number="requirement.number"
|
||||
:phase="phase"
|
||||
:source-message="saveSource"
|
||||
@saved="emit('artifact-saved')"
|
||||
/>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { NButton, NCard, NInput, NSelect } from 'naive-ui'
|
||||
|
||||
import { chatApi, type ChatMessage, type Conversation } from '@/api/chat'
|
||||
import type { Artifact, ArtifactPhase, Requirement } from '@/api/projects'
|
||||
import { useConversationSession } from '@/composables/useConversationSession'
|
||||
import { buildRequirementSystemPrompt } from '@/constants/requirementPhasePrompts'
|
||||
import MessageBubble from '@/views/chat/components/MessageBubble.vue'
|
||||
import ModelSelector from '@/views/chat/components/ModelSelector.vue'
|
||||
import SaveArtifactModal from './SaveArtifactModal.vue'
|
||||
|
||||
const PHASE_TITLE: Record<ArtifactPhase, string> = {
|
||||
planning: '规划',
|
||||
prototyping: '原型设计',
|
||||
auditing: '评审',
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
slug: string
|
||||
requirement: Requirement
|
||||
phase: ArtifactPhase
|
||||
// 上一阶段产物最新版(planning 无),注入新会话 system prompt。
|
||||
prevArtifact?: Artifact | null
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ 'artifact-saved': [] }>()
|
||||
|
||||
// 自有会话实例,与全局 /chat 页完全隔离。
|
||||
const session = useConversationSession()
|
||||
|
||||
const conversations = ref<Conversation[]>([])
|
||||
const showCreateForm = ref(false)
|
||||
const newModelId = ref<string | undefined>(undefined)
|
||||
const newSystemPrompt = ref('')
|
||||
const creating = ref(false)
|
||||
const draft = ref('')
|
||||
const scrollEl = ref<HTMLElement | null>(null)
|
||||
const showSaveModal = ref(false)
|
||||
const saveSource = ref<ChatMessage | null>(null)
|
||||
|
||||
const conversationOptions = computed(() =>
|
||||
conversations.value.map((c) => ({
|
||||
label: `${c.title || '(未命名)'} · ${new Date(c.updated_at).toLocaleString('zh-CN', { hour12: false })}`,
|
||||
value: c.id,
|
||||
})),
|
||||
)
|
||||
|
||||
async function loadConversations() {
|
||||
conversations.value = await chatApi.listConversations({
|
||||
requirement_id: props.requirement.id,
|
||||
})
|
||||
}
|
||||
|
||||
function openCreateForm() {
|
||||
newSystemPrompt.value = buildRequirementSystemPrompt(
|
||||
props.phase,
|
||||
props.requirement,
|
||||
props.prevArtifact,
|
||||
)
|
||||
showCreateForm.value = true
|
||||
}
|
||||
|
||||
async function onCreateConversation() {
|
||||
if (!newModelId.value) return
|
||||
creating.value = true
|
||||
try {
|
||||
const conv = await chatApi.createConversation({
|
||||
model_id: newModelId.value,
|
||||
system_prompt: newSystemPrompt.value,
|
||||
project_id: props.requirement.project_id,
|
||||
requirement_id: props.requirement.id,
|
||||
})
|
||||
showCreateForm.value = false
|
||||
await loadConversations()
|
||||
await session.openConversation(conv.id)
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSelectConversation(id: string | null) {
|
||||
if (!id) return
|
||||
await session.openConversation(id)
|
||||
}
|
||||
|
||||
async function send() {
|
||||
if (!draft.value.trim() || session.isStreaming.value) return
|
||||
const body = draft.value
|
||||
draft.value = ''
|
||||
await session.sendMessage(body)
|
||||
}
|
||||
|
||||
function onSaveMessage(id: number) {
|
||||
const msg = session.messages.value.find((m) => m.id === id)
|
||||
if (!msg) return
|
||||
saveSource.value = msg
|
||||
showSaveModal.value = true
|
||||
}
|
||||
|
||||
// 流式输出时自动滚到底部(同 ChatDetailView 的策略)。
|
||||
watch(
|
||||
() => [session.messages.value.length, session.messages.value.at(-1)?.content?.length ?? 0],
|
||||
() => {
|
||||
nextTick(() => {
|
||||
const el = scrollEl.value
|
||||
if (el) el.scrollTo(0, el.scrollHeight)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// 阶段切换(父组件未卸载本组件时)重置会话面板状态。
|
||||
watch(
|
||||
() => props.phase,
|
||||
() => {
|
||||
session.closeCurrent()
|
||||
showCreateForm.value = false
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(loadConversations)
|
||||
onUnmounted(() => session.closeCurrent())
|
||||
</script>
|
||||
Reference in New Issue
Block a user