feat(frontend): U5 前端辩论可视化
前端展示辩论过程,专家交锋有独立气泡样式,裁决结果清晰可见。 类型 (api/types.ts): - WsServerMessage 新增 5 个辩论事件:debate_started / expert_argument / debate_round_summary / debate_resolved / team_intervention_ack - IChatMessage.message_type 新增 4 个辩论消息类型 - IChatMessage 新增 7 个可选辩论字段(topic/round/decision 等) - 新增 4 个数据接口 Chat Store (stores/chat.ts): - 新增 debateState ref(topic/participants/round/status) - WS switch 新增 5 个 case,复用 appendMessage/appendStep 模式 - 辩论结束 1s 后清空 debateState(与 board_concluded 一致) 渲染器 (useMessageRenderer.ts): - MessageViewType + resolveMessageType 新增 4 个辩论视图类型 - useMessageRenderer 新增 4 个 render spec 新组件 (messages/): - DebateBannerCard.vue — 辩论开始横幅(主题 + 参与专家 + 开场白) - DebateArgumentCard.vue — 专家论点卡片(专家色边框 + 轮次标签) - DebateSummaryCard.vue — 主持人轮次小结 - DebateConclusionCard.vue — 裁决卡片(按 decision 着色) 输入框 (ChatInput.vue): - 团队模式下显示「辩论」按钮,点击弹出 prompt 输入主题 - 发送 /debate <topic> 命令(U4 WS 干预通道处理) npm run typecheck 通过。
This commit is contained in:
parent
c831e925b6
commit
49b483b933
|
|
@ -16,6 +16,7 @@ declare module 'vue' {
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||||
|
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
||||||
ACol: typeof import('ant-design-vue/es')['Col']
|
ACol: typeof import('ant-design-vue/es')['Col']
|
||||||
ACollapse: typeof import('ant-design-vue/es')['Collapse']
|
ACollapse: typeof import('ant-design-vue/es')['Collapse']
|
||||||
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
|
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
|
||||||
|
|
@ -64,12 +65,18 @@ declare module 'vue' {
|
||||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
|
ATimePicker: typeof import('ant-design-vue/es/time-picker/dayjs')['TimePicker']
|
||||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||||
BoardBannerCard: typeof import('./src/components/chat/messages/BoardBannerCard.vue')['default']
|
BoardBannerCard: typeof import('./src/components/chat/messages/BoardBannerCard.vue')['default']
|
||||||
BoardConclusionCard: typeof import('./src/components/chat/messages/BoardConclusionCard.vue')['default']
|
BoardConclusionCard: typeof import('./src/components/chat/messages/BoardConclusionCard.vue')['default']
|
||||||
BoardMeetingModal: typeof import('./src/components/chat/BoardMeetingModal.vue')['default']
|
BoardMeetingModal: typeof import('./src/components/chat/BoardMeetingModal.vue')['default']
|
||||||
BoardRoundCard: typeof import('./src/components/chat/messages/BoardRoundCard.vue')['default']
|
BoardRoundCard: typeof import('./src/components/chat/messages/BoardRoundCard.vue')['default']
|
||||||
BoardStatusView: typeof import('./src/components/chat/BoardStatusView.vue')['default']
|
BoardStatusView: typeof import('./src/components/chat/BoardStatusView.vue')['default']
|
||||||
|
CalendarDrawer: typeof import('./src/components/calendar/CalendarDrawer.vue')['default']
|
||||||
|
CalendarGrid: typeof import('./src/components/calendar/CalendarGrid.vue')['default']
|
||||||
|
CalendarPanel: typeof import('./src/components/calendar/CalendarPanel.vue')['default']
|
||||||
|
CalendarTab: typeof import('./src/components/layout/tabs/CalendarTab.vue')['default']
|
||||||
|
CardView: typeof import('./src/components/calendar/CardView.vue')['default']
|
||||||
ChangePasswordPanel: typeof import('./src/components/settings/ChangePasswordPanel.vue')['default']
|
ChangePasswordPanel: typeof import('./src/components/settings/ChangePasswordPanel.vue')['default']
|
||||||
ChatInput: typeof import('./src/components/chat/ChatInput.vue')['default']
|
ChatInput: typeof import('./src/components/chat/ChatInput.vue')['default']
|
||||||
ChatMessage: typeof import('./src/components/chat/ChatMessage.vue')['default']
|
ChatMessage: typeof import('./src/components/chat/ChatMessage.vue')['default']
|
||||||
|
|
@ -80,8 +87,16 @@ declare module 'vue' {
|
||||||
ConditionNode: typeof import('./src/components/workflow/ConditionNode.vue')['default']
|
ConditionNode: typeof import('./src/components/workflow/ConditionNode.vue')['default']
|
||||||
ContextPill: typeof import('./src/components/chat/ContextPill.vue')['default']
|
ContextPill: typeof import('./src/components/chat/ContextPill.vue')['default']
|
||||||
DashboardOverview: typeof import('./src/components/evolution/DashboardOverview.vue')['default']
|
DashboardOverview: typeof import('./src/components/evolution/DashboardOverview.vue')['default']
|
||||||
|
DebateArgumentCard: typeof import('./src/components/chat/messages/DebateArgumentCard.vue')['default']
|
||||||
|
DebateBannerCard: typeof import('./src/components/chat/messages/DebateBannerCard.vue')['default']
|
||||||
|
DebateConclusionCard: typeof import('./src/components/chat/messages/DebateConclusionCard.vue')['default']
|
||||||
|
DebateSummaryCard: typeof import('./src/components/chat/messages/DebateSummaryCard.vue')['default']
|
||||||
|
DocumentCard: typeof import('./src/components/chat/messages/DocumentCard.vue')['default']
|
||||||
|
DocumentPanel: typeof import('./src/components/chat/DocumentPanel.vue')['default']
|
||||||
DocumentUpload: typeof import('./src/components/kb/DocumentUpload.vue')['default']
|
DocumentUpload: typeof import('./src/components/kb/DocumentUpload.vue')['default']
|
||||||
ErrorCard: typeof import('./src/components/chat/messages/ErrorCard.vue')['default']
|
ErrorCard: typeof import('./src/components/chat/messages/ErrorCard.vue')['default']
|
||||||
|
EventBadge: typeof import('./src/components/calendar/EventBadge.vue')['default']
|
||||||
|
EventEditor: typeof import('./src/components/calendar/EventEditor.vue')['default']
|
||||||
ExperiencePanel: typeof import('./src/components/evolution/ExperiencePanel.vue')['default']
|
ExperiencePanel: typeof import('./src/components/evolution/ExperiencePanel.vue')['default']
|
||||||
ExperienceTimeline: typeof import('./src/components/evolution/ExperienceTimeline.vue')['default']
|
ExperienceTimeline: typeof import('./src/components/evolution/ExperienceTimeline.vue')['default']
|
||||||
ExpertMessage: typeof import('./src/components/chat/ExpertMessage.vue')['default']
|
ExpertMessage: typeof import('./src/components/chat/ExpertMessage.vue')['default']
|
||||||
|
|
@ -91,7 +106,9 @@ declare module 'vue' {
|
||||||
FileTree: typeof import('./src/components/code/FileTree.vue')['default']
|
FileTree: typeof import('./src/components/code/FileTree.vue')['default']
|
||||||
FlowCanvas: typeof import('./src/components/workflow/FlowCanvas.vue')['default']
|
FlowCanvas: typeof import('./src/components/workflow/FlowCanvas.vue')['default']
|
||||||
IconNav: typeof import('./src/components/layout/IconNav.vue')['default']
|
IconNav: typeof import('./src/components/layout/IconNav.vue')['default']
|
||||||
|
InvitationManager: typeof import('./src/components/calendar/InvitationManager.vue')['default']
|
||||||
KnowledgeTab: typeof import('./src/components/layout/tabs/KnowledgeTab.vue')['default']
|
KnowledgeTab: typeof import('./src/components/layout/tabs/KnowledgeTab.vue')['default']
|
||||||
|
ListView: typeof import('./src/components/calendar/ListView.vue')['default']
|
||||||
MentionDropdown: typeof import('./src/components/chat/MentionDropdown.vue')['default']
|
MentionDropdown: typeof import('./src/components/chat/MentionDropdown.vue')['default']
|
||||||
MessageShell: typeof import('./src/components/chat/messages/MessageShell.vue')['default']
|
MessageShell: typeof import('./src/components/chat/messages/MessageShell.vue')['default']
|
||||||
MetricsChart: typeof import('./src/components/evolution/MetricsChart.vue')['default']
|
MetricsChart: typeof import('./src/components/evolution/MetricsChart.vue')['default']
|
||||||
|
|
@ -105,6 +122,7 @@ declare module 'vue' {
|
||||||
PlanVisualization: typeof import('./src/components/chat/PlanVisualization.vue')['default']
|
PlanVisualization: typeof import('./src/components/chat/PlanVisualization.vue')['default']
|
||||||
PropertyPanel: typeof import('./src/components/workflow/PropertyPanel.vue')['default']
|
PropertyPanel: typeof import('./src/components/workflow/PropertyPanel.vue')['default']
|
||||||
QuadrantPanel: typeof import('./src/components/layout/QuadrantPanel.vue')['default']
|
QuadrantPanel: typeof import('./src/components/layout/QuadrantPanel.vue')['default']
|
||||||
|
ReminderConfig: typeof import('./src/components/calendar/ReminderConfig.vue')['default']
|
||||||
RightPanel: typeof import('./src/components/layout/RightPanel.vue')['default']
|
RightPanel: typeof import('./src/components/layout/RightPanel.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
|
@ -123,6 +141,7 @@ declare module 'vue' {
|
||||||
SourceConfig: typeof import('./src/components/kb/SourceConfig.vue')['default']
|
SourceConfig: typeof import('./src/components/kb/SourceConfig.vue')['default']
|
||||||
SplashScreen: typeof import('./src/components/layout/SplashScreen.vue')['default']
|
SplashScreen: typeof import('./src/components/layout/SplashScreen.vue')['default']
|
||||||
SplitPane: typeof import('./src/components/layout/SplitPane.vue')['default']
|
SplitPane: typeof import('./src/components/layout/SplitPane.vue')['default']
|
||||||
|
SyncSettings: typeof import('./src/components/calendar/SyncSettings.vue')['default']
|
||||||
SystemMonitorPanel: typeof import('./src/components/layout/SystemMonitorPanel.vue')['default']
|
SystemMonitorPanel: typeof import('./src/components/layout/SystemMonitorPanel.vue')['default']
|
||||||
SystemTab: typeof import('./src/components/layout/tabs/SystemTab.vue')['default']
|
SystemTab: typeof import('./src/components/layout/tabs/SystemTab.vue')['default']
|
||||||
TeamModal: typeof import('./src/components/chat/TeamModal.vue')['default']
|
TeamModal: typeof import('./src/components/chat/TeamModal.vue')['default']
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ export interface IChatMessage {
|
||||||
| 'board_speech'
|
| 'board_speech'
|
||||||
| 'board_summary'
|
| 'board_summary'
|
||||||
| 'board_conclusion'
|
| 'board_conclusion'
|
||||||
|
| 'debate_started'
|
||||||
|
| 'debate_argument'
|
||||||
|
| 'debate_summary'
|
||||||
|
| 'debate_resolved'
|
||||||
| 'error'
|
| 'error'
|
||||||
board_round?: number
|
board_round?: number
|
||||||
board_role?: 'moderator' | 'expert' | 'user' | 'summary'
|
board_role?: 'moderator' | 'expert' | 'user' | 'summary'
|
||||||
|
|
@ -65,6 +69,13 @@ export interface IChatMessage {
|
||||||
error_detail?: string
|
error_detail?: string
|
||||||
board_started?: IBoardStartedData
|
board_started?: IBoardStartedData
|
||||||
board_conclusion?: IBoardConcludedData
|
board_conclusion?: IBoardConcludedData
|
||||||
|
debate_topic?: string
|
||||||
|
debate_round?: number
|
||||||
|
debate_decision?: string
|
||||||
|
debate_rationale?: string
|
||||||
|
debate_participants?: string[]
|
||||||
|
debate_opening?: string
|
||||||
|
debate_moderator?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Conversation with messages */
|
/** Conversation with messages */
|
||||||
|
|
@ -132,6 +143,12 @@ export type WsServerMessage =
|
||||||
| { type: 'round_summary'; data: IRoundSummaryData }
|
| { type: 'round_summary'; data: IRoundSummaryData }
|
||||||
| { type: 'user_intervention'; data: IUserInterventionData }
|
| { type: 'user_intervention'; data: IUserInterventionData }
|
||||||
| { type: 'board_concluded'; data: IBoardConcludedData }
|
| { type: 'board_concluded'; data: IBoardConcludedData }
|
||||||
|
// Debate (U5) 事件
|
||||||
|
| { type: 'debate_started'; data: IDebateStartedData }
|
||||||
|
| { type: 'expert_argument'; data: IDebateArgumentData }
|
||||||
|
| { type: 'debate_round_summary'; data: IDebateRoundSummaryData }
|
||||||
|
| { type: 'debate_resolved'; data: IDebateResolvedData }
|
||||||
|
| { type: 'team_intervention_ack'; data: { content: string } }
|
||||||
// Calendar 事件 (KTD-10 — piggyback on chat WS)
|
// Calendar 事件 (KTD-10 — piggyback on chat WS)
|
||||||
| { type: 'calendar_event_created'; data: ICalendarEventCreatedData }
|
| { type: 'calendar_event_created'; data: ICalendarEventCreatedData }
|
||||||
| { type: 'calendar_reminder'; data: ICalendarReminderData }
|
| { type: 'calendar_reminder'; data: ICalendarReminderData }
|
||||||
|
|
@ -225,6 +242,47 @@ export interface IBoardConcludedData {
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Debate (U5) 模式类型 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** debate_started event payload */
|
||||||
|
export interface IDebateStartedData {
|
||||||
|
phase_id: string
|
||||||
|
phase_name: string
|
||||||
|
topic: string
|
||||||
|
participants: string[]
|
||||||
|
max_rounds: number
|
||||||
|
opening: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** expert_argument event payload */
|
||||||
|
export interface IDebateArgumentData {
|
||||||
|
phase_id: string
|
||||||
|
expert_id: string
|
||||||
|
expert_name: string
|
||||||
|
expert_color: string
|
||||||
|
content: string
|
||||||
|
round: number
|
||||||
|
topic: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** debate_round_summary event payload */
|
||||||
|
export interface IDebateRoundSummaryData {
|
||||||
|
phase_id: string
|
||||||
|
moderator_name: string
|
||||||
|
content: string
|
||||||
|
round: number
|
||||||
|
continue: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** debate_resolved event payload */
|
||||||
|
export interface IDebateResolvedData {
|
||||||
|
phase_id: string
|
||||||
|
phase_name: string
|
||||||
|
decision: 'adopt' | 'compromise' | 'shelve' | 'inconclusive'
|
||||||
|
conclusion: string
|
||||||
|
rationale: string
|
||||||
|
}
|
||||||
|
|
||||||
/** Board meeting status (matches backend BoardStatus enum) */
|
/** Board meeting status (matches backend BoardStatus enum) */
|
||||||
export type BoardStatus = 'forming' | 'discussing' | 'concluding' | 'completed' | 'dissolved'
|
export type BoardStatus = 'forming' | 'discussing' | 'concluding' | 'completed' | 'dissolved'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,16 @@
|
||||||
<template #icon><TeamOutlined /></template>
|
<template #icon><TeamOutlined /></template>
|
||||||
私董会
|
私董会
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
v-if="teamStore?.isTeamMode"
|
||||||
|
size="small"
|
||||||
|
:disabled="disabled && !teamStore?.isTeamMode"
|
||||||
|
@click="handleDebateClick"
|
||||||
|
class="chat-input__action-btn chat-input__action-btn--debate"
|
||||||
|
title="发起辩论"
|
||||||
|
>
|
||||||
|
<template #icon><CommentOutlined /></template>辩论
|
||||||
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="disabled || fileUploading"
|
:disabled="disabled || fileUploading"
|
||||||
|
|
@ -129,12 +139,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, type Component } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, type Component } from 'vue'
|
||||||
import { Input as AInput, Button as AButton, Select as ASelect } from 'ant-design-vue'
|
import { Input as AInput, Button as AButton, Select as ASelect } from 'ant-design-vue'
|
||||||
import { SendOutlined, TeamOutlined, UsergroupAddOutlined, PaperClipOutlined, PoweroffOutlined } from '@ant-design/icons-vue'
|
import { SendOutlined, TeamOutlined, UsergroupAddOutlined, PaperClipOutlined, PoweroffOutlined, CommentOutlined } from '@ant-design/icons-vue'
|
||||||
import ContextPill from './ContextPill.vue'
|
import ContextPill from './ContextPill.vue'
|
||||||
import MentionDropdown from './MentionDropdown.vue'
|
import MentionDropdown from './MentionDropdown.vue'
|
||||||
import BoardMeetingModal from './BoardMeetingModal.vue'
|
import BoardMeetingModal from './BoardMeetingModal.vue'
|
||||||
import TeamModal from './TeamModal.vue'
|
import TeamModal from './TeamModal.vue'
|
||||||
import { useSkillsStore } from '@/stores/skills'
|
import { useSkillsStore } from '@/stores/skills'
|
||||||
|
import { useTeamStore } from '@/stores/team'
|
||||||
import type { ISkillInfo } from '@/api/skills'
|
import type { ISkillInfo } from '@/api/skills'
|
||||||
import { getDynamicBaseURL } from '@/api/base'
|
import { getDynamicBaseURL } from '@/api/base'
|
||||||
import { apiClient } from '@/api/client'
|
import { apiClient } from '@/api/client'
|
||||||
|
|
@ -230,6 +241,7 @@ const mentionQuery = ref('')
|
||||||
const mentionStartIndex = ref(-1)
|
const mentionStartIndex = ref(-1)
|
||||||
const mentionPosition = ref({ left: 0 })
|
const mentionPosition = ref({ left: 0 })
|
||||||
const skillsStore = useSkillsStore()
|
const skillsStore = useSkillsStore()
|
||||||
|
const teamStore = useTeamStore()
|
||||||
|
|
||||||
const skillSuggestions = computed<SkillSuggestion[]>(() => {
|
const skillSuggestions = computed<SkillSuggestion[]>(() => {
|
||||||
return (skillsStore.skills || []).map((s: ISkillInfo) => ({
|
return (skillsStore.skills || []).map((s: ISkillInfo) => ({
|
||||||
|
|
@ -318,6 +330,14 @@ function handleTeamSubmit(command: string): void {
|
||||||
emit('send', command, selectedModel.value)
|
emit('send', command, selectedModel.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDebateClick(): void {
|
||||||
|
// Prompt user for debate topic, then send as intervention
|
||||||
|
const topic = window.prompt('请输入辩论主题')
|
||||||
|
if (topic && topic.trim()) {
|
||||||
|
emit('send', `/debate ${topic.trim()}`, selectedModel.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openFilePicker(): void {
|
function openFilePicker(): void {
|
||||||
fileInputRef.value?.click()
|
fileInputRef.value?.click()
|
||||||
}
|
}
|
||||||
|
|
@ -563,6 +583,17 @@ function removePill(idx: number): void {
|
||||||
background: var(--accent-board-soft);
|
background: var(--accent-board-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-input__action-btn--debate {
|
||||||
|
color: #722ed1;
|
||||||
|
border-color: #d3adf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input__action-btn--debate:not(:disabled):hover {
|
||||||
|
color: #531dab;
|
||||||
|
border-color: #722ed1;
|
||||||
|
background: #f9f0ff;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-input__action-btn--clear {
|
.chat-input__action-btn--clear {
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import TeamPlanCard from '@/components/chat/messages/TeamPlanCard.vue'
|
||||||
import BoardBannerCard from '@/components/chat/messages/BoardBannerCard.vue'
|
import BoardBannerCard from '@/components/chat/messages/BoardBannerCard.vue'
|
||||||
import BoardRoundCard from '@/components/chat/messages/BoardRoundCard.vue'
|
import BoardRoundCard from '@/components/chat/messages/BoardRoundCard.vue'
|
||||||
import BoardConclusionCard from '@/components/chat/messages/BoardConclusionCard.vue'
|
import BoardConclusionCard from '@/components/chat/messages/BoardConclusionCard.vue'
|
||||||
|
import DebateBannerCard from '@/components/chat/messages/DebateBannerCard.vue'
|
||||||
|
import DebateArgumentCard from '@/components/chat/messages/DebateArgumentCard.vue'
|
||||||
|
import DebateSummaryCard from '@/components/chat/messages/DebateSummaryCard.vue'
|
||||||
|
import DebateConclusionCard from '@/components/chat/messages/DebateConclusionCard.vue'
|
||||||
import ErrorCard from '@/components/chat/messages/ErrorCard.vue'
|
import ErrorCard from '@/components/chat/messages/ErrorCard.vue'
|
||||||
|
|
||||||
export type MessageViewType =
|
export type MessageViewType =
|
||||||
|
|
@ -16,6 +20,10 @@ export type MessageViewType =
|
||||||
| 'board_speech'
|
| 'board_speech'
|
||||||
| 'board_summary'
|
| 'board_summary'
|
||||||
| 'board_conclusion'
|
| 'board_conclusion'
|
||||||
|
| 'debate_started'
|
||||||
|
| 'debate_argument'
|
||||||
|
| 'debate_summary'
|
||||||
|
| 'debate_resolved'
|
||||||
| 'milestone'
|
| 'milestone'
|
||||||
| 'error'
|
| 'error'
|
||||||
|
|
||||||
|
|
@ -48,6 +56,14 @@ export function resolveMessageType(message: IChatMessage): MessageViewType {
|
||||||
return 'board_summary'
|
return 'board_summary'
|
||||||
case 'board_conclusion':
|
case 'board_conclusion':
|
||||||
return 'board_conclusion'
|
return 'board_conclusion'
|
||||||
|
case 'debate_started':
|
||||||
|
return 'debate_started'
|
||||||
|
case 'debate_argument':
|
||||||
|
return 'debate_argument'
|
||||||
|
case 'debate_summary':
|
||||||
|
return 'debate_summary'
|
||||||
|
case 'debate_resolved':
|
||||||
|
return 'debate_resolved'
|
||||||
case 'milestone':
|
case 'milestone':
|
||||||
return 'milestone'
|
return 'milestone'
|
||||||
default:
|
default:
|
||||||
|
|
@ -168,6 +184,83 @@ export function useMessageRenderer(message: IChatMessage) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'debate_started':
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
shell: {
|
||||||
|
name: '辩论',
|
||||||
|
avatar: '⚖',
|
||||||
|
color: '#722ed1',
|
||||||
|
meta: message.debate_topic || '',
|
||||||
|
},
|
||||||
|
component: DebateBannerCard,
|
||||||
|
props: {
|
||||||
|
topic: message.debate_topic || '',
|
||||||
|
participants: message.debate_participants || [],
|
||||||
|
opening: message.debate_opening || message.content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'debate_argument':
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
shell: {
|
||||||
|
name: message.expert_name || '专家',
|
||||||
|
avatar: (message.expert_name || '?')[0],
|
||||||
|
color: message.expert_color || '#722ed1',
|
||||||
|
meta: `辩论第${message.debate_round || 1}轮`,
|
||||||
|
},
|
||||||
|
component: DebateArgumentCard,
|
||||||
|
props: {
|
||||||
|
content: message.content,
|
||||||
|
round: message.debate_round || 1,
|
||||||
|
expertName: message.expert_name || '',
|
||||||
|
expertColor: message.expert_color || '#722ed1',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'debate_summary':
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
shell: {
|
||||||
|
name: message.expert_name || 'Lead',
|
||||||
|
avatar: (message.expert_name || 'L')[0],
|
||||||
|
color: '#722ed1',
|
||||||
|
meta: `第${message.debate_round || 1}轮小结`,
|
||||||
|
},
|
||||||
|
component: DebateSummaryCard,
|
||||||
|
props: {
|
||||||
|
content: message.content,
|
||||||
|
round: message.debate_round || 1,
|
||||||
|
moderatorName: message.debate_moderator || message.expert_name || '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'debate_resolved': {
|
||||||
|
const decisionLabels: Record<string, string> = {
|
||||||
|
adopt: '采纳',
|
||||||
|
compromise: '折中',
|
||||||
|
shelve: '搁置',
|
||||||
|
inconclusive: '未决',
|
||||||
|
}
|
||||||
|
const decision = message.debate_decision || 'inconclusive'
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
shell: {
|
||||||
|
name: '辩论裁决',
|
||||||
|
avatar: '⚖',
|
||||||
|
color: '#fa8c16',
|
||||||
|
meta: decisionLabels[decision] || decision,
|
||||||
|
},
|
||||||
|
component: DebateConclusionCard,
|
||||||
|
props: {
|
||||||
|
conclusion: message.content,
|
||||||
|
decision,
|
||||||
|
rationale: message.debate_rationale || '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div class="debate-argument" :style="{ borderLeftColor: expertColor }">
|
||||||
|
<div class="debate-argument__header">
|
||||||
|
<span class="debate-argument__name" :style="{ color: expertColor }">{{ expertName }}</span>
|
||||||
|
<a-tag color="purple">第{{ round }}轮</a-tag>
|
||||||
|
</div>
|
||||||
|
<AssistantText :message="textMessage" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import AssistantText from './AssistantText.vue'
|
||||||
|
import type { IChatMessage } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
content: string
|
||||||
|
round: number
|
||||||
|
expertName: string
|
||||||
|
expertColor: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const textMessage = computed<IChatMessage>(() => ({
|
||||||
|
id: `debate-arg-${props.expertName}-${props.round}`,
|
||||||
|
role: 'assistant',
|
||||||
|
content: props.content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'completed',
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.debate-argument {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-left: 3px solid #722ed1;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.debate-argument__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.debate-argument__name { font-weight: 600; }
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<div class="debate-banner">
|
||||||
|
<div class="debate-banner__icon">⚖</div>
|
||||||
|
<div class="debate-banner__body">
|
||||||
|
<div class="debate-banner__title">辩论开始</div>
|
||||||
|
<div class="debate-banner__topic">{{ topic }}</div>
|
||||||
|
<div class="debate-banner__participants">
|
||||||
|
<span class="debate-banner__label">参与专家:</span>
|
||||||
|
<a-tag v-for="p in participants" :key="p" color="purple">{{ p }}</a-tag>
|
||||||
|
</div>
|
||||||
|
<div v-if="opening" class="debate-banner__opening">
|
||||||
|
<AssistantText :message="openingMessage" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import AssistantText from './AssistantText.vue'
|
||||||
|
import type { IChatMessage } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
topic: string
|
||||||
|
participants: string[]
|
||||||
|
opening: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const openingMessage = computed<IChatMessage>(() => ({
|
||||||
|
id: 'debate-opening',
|
||||||
|
role: 'assistant',
|
||||||
|
content: props.opening,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'completed',
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.debate-banner {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-left: 3px solid #722ed1;
|
||||||
|
background: #f9f0ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.debate-banner__icon { font-size: 24px; }
|
||||||
|
.debate-banner__title { font-weight: 600; color: #722ed1; margin-bottom: 4px; }
|
||||||
|
.debate-banner__topic { font-size: 14px; margin-bottom: 8px; }
|
||||||
|
.debate-banner__participants { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||||
|
.debate-banner__label { font-size: 12px; color: #666; }
|
||||||
|
.debate-banner__opening { margin-top: 8px; }
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="debate-conclusion" :class="`debate-conclusion--${decision}`">
|
||||||
|
<div class="debate-conclusion__header">
|
||||||
|
<span class="debate-conclusion__icon">{{ decisionIcon }}</span>
|
||||||
|
<span class="debate-conclusion__decision">{{ decisionLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debate-conclusion__body">
|
||||||
|
<AssistantText :message="textMessage" />
|
||||||
|
</div>
|
||||||
|
<div v-if="rationale" class="debate-conclusion__rationale">
|
||||||
|
<span class="debate-conclusion__rationale-label">理由:</span>
|
||||||
|
<span>{{ rationale }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import AssistantText from './AssistantText.vue'
|
||||||
|
import type { IChatMessage } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
conclusion: string
|
||||||
|
decision: string
|
||||||
|
rationale: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const decisionLabels: Record<string, string> = {
|
||||||
|
adopt: '采纳',
|
||||||
|
compromise: '折中',
|
||||||
|
shelve: '搁置',
|
||||||
|
inconclusive: '未决',
|
||||||
|
}
|
||||||
|
const decisionIcons: Record<string, string> = {
|
||||||
|
adopt: '✓',
|
||||||
|
compromise: '⇄',
|
||||||
|
shelve: '○',
|
||||||
|
inconclusive: '?',
|
||||||
|
}
|
||||||
|
|
||||||
|
const decisionLabel = computed(() => decisionLabels[props.decision] || props.decision)
|
||||||
|
const decisionIcon = computed(() => decisionIcons[props.decision] || '?')
|
||||||
|
|
||||||
|
const textMessage = computed<IChatMessage>(() => ({
|
||||||
|
id: 'debate-conclusion',
|
||||||
|
role: 'assistant',
|
||||||
|
content: props.conclusion,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'completed',
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.debate-conclusion {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-left: 3px solid #fa8c16;
|
||||||
|
background: #fff7e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.debate-conclusion--adopt { border-left-color: #52c41a; background: #f6ffed; }
|
||||||
|
.debate-conclusion--compromise { border-left-color: #faad14; background: #fffbe6; }
|
||||||
|
.debate-conclusion--shelve { border-left-color: #bfbfbf; background: #f5f5f5; }
|
||||||
|
.debate-conclusion--inconclusive { border-left-color: #ff4d4f; background: #fff2f0; }
|
||||||
|
|
||||||
|
.debate-conclusion__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.debate-conclusion__icon { font-size: 18px; font-weight: bold; }
|
||||||
|
.debate-conclusion__decision { font-weight: 600; font-size: 14px; }
|
||||||
|
.debate-conclusion__body { margin-bottom: 8px; }
|
||||||
|
.debate-conclusion__rationale {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px dashed #d9d9d9;
|
||||||
|
}
|
||||||
|
.debate-conclusion__rationale-label { font-weight: 600; }
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div class="debate-summary">
|
||||||
|
<div class="debate-summary__header">
|
||||||
|
<span class="debate-summary__moderator">{{ moderatorName }}</span>
|
||||||
|
<a-tag color="orange">第{{ round }}轮小结</a-tag>
|
||||||
|
</div>
|
||||||
|
<AssistantText :message="textMessage" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import AssistantText from './AssistantText.vue'
|
||||||
|
import type { IChatMessage } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
content: string
|
||||||
|
round: number
|
||||||
|
moderatorName: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const textMessage = computed<IChatMessage>(() => ({
|
||||||
|
id: `debate-summary-${props.round}`,
|
||||||
|
role: 'assistant',
|
||||||
|
content: props.content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'completed',
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.debate-summary {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-left: 3px solid #fa8c16;
|
||||||
|
background: #fff7e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
.debate-summary__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.debate-summary__moderator { font-weight: 600; color: #fa8c16; }
|
||||||
|
</style>
|
||||||
|
|
@ -5,5 +5,9 @@ export { default as TeamPlanCard } from './TeamPlanCard.vue'
|
||||||
export { default as BoardBannerCard } from './BoardBannerCard.vue'
|
export { default as BoardBannerCard } from './BoardBannerCard.vue'
|
||||||
export { default as BoardRoundCard } from './BoardRoundCard.vue'
|
export { default as BoardRoundCard } from './BoardRoundCard.vue'
|
||||||
export { default as BoardConclusionCard } from './BoardConclusionCard.vue'
|
export { default as BoardConclusionCard } from './BoardConclusionCard.vue'
|
||||||
|
export { default as DebateBannerCard } from './DebateBannerCard.vue'
|
||||||
|
export { default as DebateArgumentCard } from './DebateArgumentCard.vue'
|
||||||
|
export { default as DebateSummaryCard } from './DebateSummaryCard.vue'
|
||||||
|
export { default as DebateConclusionCard } from './DebateConclusionCard.vue'
|
||||||
export { default as ErrorCard } from './ErrorCard.vue'
|
export { default as ErrorCard } from './ErrorCard.vue'
|
||||||
export { default as FileAttachment } from './FileAttachment.vue'
|
export { default as FileAttachment } from './FileAttachment.vue'
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ import type {
|
||||||
IChatRequest,
|
IChatRequest,
|
||||||
WsClientMessage,
|
WsClientMessage,
|
||||||
WsServerMessage,
|
WsServerMessage,
|
||||||
|
IDebateStartedData,
|
||||||
|
IDebateArgumentData,
|
||||||
|
IDebateRoundSummaryData,
|
||||||
|
IDebateResolvedData,
|
||||||
} from '@/api/types'
|
} from '@/api/types'
|
||||||
|
|
||||||
function generateId(): string {
|
function generateId(): string {
|
||||||
|
|
@ -148,6 +152,15 @@ export const useChatStore = defineStore('chat', () => {
|
||||||
|
|
||||||
const isBoardMode = computed(() => boardState.value !== null && boardState.value.status === 'discussing')
|
const isBoardMode = computed(() => boardState.value !== null && boardState.value.status === 'discussing')
|
||||||
|
|
||||||
|
// Debate state (transient, only active during a debate collaboration)
|
||||||
|
const debateState = ref<{
|
||||||
|
topic: string
|
||||||
|
participants: string[]
|
||||||
|
current_round: number
|
||||||
|
max_rounds: number
|
||||||
|
status: 'debating' | 'resolved' | 'cancelled'
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
// --- Getters ---
|
// --- Getters ---
|
||||||
const currentConversation = computed<IConversation | undefined>(() => {
|
const currentConversation = computed<IConversation | undefined>(() => {
|
||||||
return conversations.value.find((c) => c.id === currentConversationId.value)
|
return conversations.value.find((c) => c.id === currentConversationId.value)
|
||||||
|
|
@ -1199,6 +1212,130 @@ export const useChatStore = defineStore('chat', () => {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Debate events (U5) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
case 'debate_started': {
|
||||||
|
const d = data.data as IDebateStartedData
|
||||||
|
debateState.value = {
|
||||||
|
topic: d.topic,
|
||||||
|
participants: d.participants,
|
||||||
|
current_round: 0,
|
||||||
|
max_rounds: d.max_rounds,
|
||||||
|
status: 'debating',
|
||||||
|
}
|
||||||
|
const sessionId = resolveIncomingConvId()
|
||||||
|
if (!sessionId) break
|
||||||
|
appendMessage(sessionId, {
|
||||||
|
id: generateId(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: d.opening,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'completed',
|
||||||
|
message_type: 'debate_started',
|
||||||
|
debate_topic: d.topic,
|
||||||
|
debate_participants: d.participants,
|
||||||
|
debate_opening: d.opening,
|
||||||
|
})
|
||||||
|
appendStep({ type: 'team_event', label: '辩论开始', detail: d.topic.slice(0, 40), status: 'success' }, sessionId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'expert_argument': {
|
||||||
|
const d = data.data as IDebateArgumentData
|
||||||
|
debateState.value = {
|
||||||
|
...(debateState.value as NonNullable<typeof debateState.value>),
|
||||||
|
current_round: d.round,
|
||||||
|
}
|
||||||
|
const sessionId = resolveIncomingConvId()
|
||||||
|
if (!sessionId) break
|
||||||
|
appendMessage(sessionId, {
|
||||||
|
id: generateId(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: d.content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'completed',
|
||||||
|
message_type: 'debate_argument',
|
||||||
|
expert_name: d.expert_name,
|
||||||
|
expert_color: d.expert_color,
|
||||||
|
debate_topic: d.topic,
|
||||||
|
debate_round: d.round,
|
||||||
|
})
|
||||||
|
appendStep({
|
||||||
|
type: 'team_event',
|
||||||
|
label: d.expert_name,
|
||||||
|
detail: `辩论第${d.round}轮`,
|
||||||
|
status: 'success',
|
||||||
|
}, sessionId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'debate_round_summary': {
|
||||||
|
const d = data.data as IDebateRoundSummaryData
|
||||||
|
const sessionId = resolveIncomingConvId()
|
||||||
|
if (!sessionId) break
|
||||||
|
appendMessage(sessionId, {
|
||||||
|
id: generateId(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: d.content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'completed',
|
||||||
|
message_type: 'debate_summary',
|
||||||
|
expert_name: d.moderator_name,
|
||||||
|
debate_round: d.round,
|
||||||
|
debate_moderator: d.moderator_name,
|
||||||
|
})
|
||||||
|
appendStep({
|
||||||
|
type: 'team_event',
|
||||||
|
label: d.moderator_name,
|
||||||
|
detail: `第${d.round}轮小结`,
|
||||||
|
status: 'success',
|
||||||
|
}, sessionId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'debate_resolved': {
|
||||||
|
const d = data.data as IDebateResolvedData
|
||||||
|
if (debateState.value) {
|
||||||
|
debateState.value = { ...debateState.value, status: 'resolved' }
|
||||||
|
}
|
||||||
|
const sessionId = resolveIncomingConvId()
|
||||||
|
if (!sessionId) break
|
||||||
|
appendMessage(sessionId, {
|
||||||
|
id: generateId(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: d.conclusion,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'completed',
|
||||||
|
message_type: 'debate_resolved',
|
||||||
|
debate_decision: d.decision,
|
||||||
|
debate_rationale: d.rationale,
|
||||||
|
})
|
||||||
|
appendStep({
|
||||||
|
type: 'team_event',
|
||||||
|
label: '辩论裁决',
|
||||||
|
detail: d.decision,
|
||||||
|
status: 'success',
|
||||||
|
}, sessionId)
|
||||||
|
// Clear debate state after 1 second (same pattern as board_concluded)
|
||||||
|
setTimeout(() => { debateState.value = null }, 1000)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'team_intervention_ack': {
|
||||||
|
// User intervention was accepted by the server — no message needed,
|
||||||
|
// just a step entry for visibility.
|
||||||
|
const d = data.data as { content: string }
|
||||||
|
const sessionId = resolveIncomingConvId()
|
||||||
|
if (!sessionId) break
|
||||||
|
appendStep({
|
||||||
|
type: 'team_event',
|
||||||
|
label: '用户干预',
|
||||||
|
detail: d.content.slice(0, 40),
|
||||||
|
status: 'success',
|
||||||
|
}, sessionId)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1251,6 +1388,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||||
pendingConversations,
|
pendingConversations,
|
||||||
streamingStepsByConv,
|
streamingStepsByConv,
|
||||||
boardState,
|
boardState,
|
||||||
|
debateState,
|
||||||
// Legacy aliases (derive from current conversation for backward compat).
|
// Legacy aliases (derive from current conversation for backward compat).
|
||||||
// New code should use `isCurrentLoading` / `currentStreamingSteps` instead.
|
// New code should use `isCurrentLoading` / `currentStreamingSteps` instead.
|
||||||
isLoading: isCurrentLoading,
|
isLoading: isCurrentLoading,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue