diff --git a/src/agentkit/server/frontend/src/api/types.ts b/src/agentkit/server/frontend/src/api/types.ts index 0678afb..e5121f2 100644 --- a/src/agentkit/server/frontend/src/api/types.ts +++ b/src/agentkit/server/frontend/src/api/types.ts @@ -62,6 +62,9 @@ export interface IChatMessage { | 'debate_argument' | 'debate_summary' | 'debate_resolved' + | 'collaboration_graph' + | 'review_result' + | 'risk_flagged' | 'error' board_round?: number board_role?: 'moderator' | 'expert' | 'user' | 'summary' @@ -76,6 +79,12 @@ export interface IChatMessage { debate_participants?: string[] debate_opening?: string debate_moderator?: string + /** U5: PM collaboration — aggregated graph data for CollaborationGraphCard */ + collaboration_graph?: ICollaborationGraphData + /** U5: PM collaboration — review result for ReviewResultCard */ + review_result?: IReviewResult + /** U5: PM collaboration — risk flag for RiskFlagCard */ + risk_flag?: IRiskFlag } /** Conversation with messages */ @@ -149,6 +158,11 @@ export type WsServerMessage = | { type: 'debate_round_summary'; data: IDebateRoundSummaryData } | { type: 'debate_resolved'; data: IDebateResolvedData } | { type: 'team_intervention_ack'; data: { content: string } } + // PM Collaboration (U5) 事件 + | { type: 'collaboration_contract_defined'; data: ICollaborationContractDefinedData } + | { type: 'collaboration_notice'; data: ICollaborationNotice } + | { type: 'review_result'; data: IReviewResult } + | { type: 'risk_flagged'; data: IRiskFlag } // Calendar 事件 (KTD-10 — piggyback on chat WS) | { type: 'calendar_event_created'; data: ICalendarEventCreatedData } | { type: 'calendar_reminder'; data: ICalendarReminderData } @@ -178,6 +192,12 @@ export interface ITeamPlanPhase { result?: string parallel_type?: 'serial' | 'subtask_parallel' | 'competitive_parallel' milestone?: string + /** U5: PM collaboration — contracts defined by Lead for this phase */ + collaboration_contracts?: ICollaborationContract[] + /** U5: PM collaboration — rework count after Lead review failures */ + rework_count?: number + /** U5: PM collaboration — Lead review feedback (modification requirements) */ + review_feedback?: string | null } /** Expert team state */ @@ -283,6 +303,64 @@ export interface IDebateResolvedData { rationale: string } +// ── PM Collaboration (U5) 模式类型 ────────────────────────────────── + +/** 协作契约 — 匹配后端 CollaborationContract.to_dict() */ +export interface ICollaborationContract { + from_expert: string + to_expert: string + content_description: string + status: 'pending' | 'delivered' | 'received' +} + +/** collaboration_contract_defined event payload + * (后端当前通过 plan_update 的 plan_phases[].collaboration_contracts 携带, + * 此类型用于可能的独立事件和类型完整性) */ +export interface ICollaborationContractDefinedData { + phase_id: string + phase_name: string + contracts: ICollaborationContract[] +} + +/** collaboration_notice event payload — 专家完成后按契约通知相关专家 */ +export interface ICollaborationNotice { + from_expert: string + to_expert: string + content_description: string + phase_id: string + phase_name: string + output_key: string + expert_color: string +} + +/** review_result event payload — Lead 验收阶段输出 */ +export interface IReviewResult { + phase_id: string + phase_name: string + passed: boolean + feedback: string + expert: string + rework_count?: number + final_status?: 'rework' | 'failed' +} + +/** risk_flagged event payload — 专家风险标记 */ +export interface IRiskFlag { + expert: string + expert_name: string + risk_description: string + phase_id: string + phase_name: string +} + +/** 协作关系图聚合数据 — 存储在 collaboration_graph 消息中,随事件实时更新 */ +export interface ICollaborationGraphData { + contracts: Array + notices: ICollaborationNotice[] + reviews: IReviewResult[] + risks: IRiskFlag[] +} + /** Board meeting status (matches backend BoardStatus enum) */ export type BoardStatus = 'forming' | 'discussing' | 'concluding' | 'completed' | 'dissolved' diff --git a/src/agentkit/server/frontend/src/components/chat/helpers/useMessageRenderer.ts b/src/agentkit/server/frontend/src/components/chat/helpers/useMessageRenderer.ts index 48846ce..5dd951b 100644 --- a/src/agentkit/server/frontend/src/components/chat/helpers/useMessageRenderer.ts +++ b/src/agentkit/server/frontend/src/components/chat/helpers/useMessageRenderer.ts @@ -10,6 +10,9 @@ 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 CollaborationGraphCard from '@/components/chat/messages/CollaborationGraphCard.vue' +import ReviewResultCard from '@/components/chat/messages/ReviewResultCard.vue' +import RiskFlagCard from '@/components/chat/messages/RiskFlagCard.vue' import ErrorCard from '@/components/chat/messages/ErrorCard.vue' export type MessageViewType = @@ -24,6 +27,9 @@ export type MessageViewType = | 'debate_argument' | 'debate_summary' | 'debate_resolved' + | 'collaboration_graph' + | 'review_result' + | 'risk_flagged' | 'milestone' | 'error' @@ -64,6 +70,12 @@ export function resolveMessageType(message: IChatMessage): MessageViewType { return 'debate_summary' case 'debate_resolved': return 'debate_resolved' + case 'collaboration_graph': + return 'collaboration_graph' + case 'review_result': + return 'review_result' + case 'risk_flagged': + return 'risk_flagged' case 'milestone': return 'milestone' default: @@ -261,6 +273,68 @@ export function useMessageRenderer(message: IChatMessage) { } } + case 'collaboration_graph': { + const graphData = message.collaboration_graph ?? { + contracts: [], + notices: [], + reviews: [], + risks: [], + } + return { + type, + shell: { + name: '协作关系图', + avatar: '◆', + color: '#1890ff', + meta: time, + }, + component: CollaborationGraphCard, + props: { graphData }, + } + } + + case 'review_result': { + const review = message.review_result ?? { + phase_id: '', + phase_name: '', + passed: false, + feedback: message.content, + expert: message.expert_name || '', + } + return { + type, + shell: { + name: '验收结果', + avatar: review.passed ? '\u2713' : '\u2717', + color: review.passed ? '#52c41a' : '#ff4d4f', + meta: review.phase_name || time, + }, + component: ReviewResultCard, + props: { review }, + } + } + + case 'risk_flagged': { + const risk = message.risk_flag ?? { + expert: message.expert_name || '', + expert_name: message.expert_name || '', + risk_description: message.content, + phase_id: '', + phase_name: '', + } + return { + type, + shell: { + name: '风险标记', + avatar: '!', + color: '#fa8c16', + meta: risk.phase_name || time, + }, + component: RiskFlagCard, + props: { risk }, + } + } + case 'error': return { type, diff --git a/src/agentkit/server/frontend/src/components/chat/messages/CollaborationGraphCard.vue b/src/agentkit/server/frontend/src/components/chat/messages/CollaborationGraphCard.vue new file mode 100644 index 0000000..2b14ad6 --- /dev/null +++ b/src/agentkit/server/frontend/src/components/chat/messages/CollaborationGraphCard.vue @@ -0,0 +1,440 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/chat/messages/ReviewResultCard.vue b/src/agentkit/server/frontend/src/components/chat/messages/ReviewResultCard.vue new file mode 100644 index 0000000..f756310 --- /dev/null +++ b/src/agentkit/server/frontend/src/components/chat/messages/ReviewResultCard.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/chat/messages/RiskFlagCard.vue b/src/agentkit/server/frontend/src/components/chat/messages/RiskFlagCard.vue new file mode 100644 index 0000000..1160bbe --- /dev/null +++ b/src/agentkit/server/frontend/src/components/chat/messages/RiskFlagCard.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/chat/messages/index.ts b/src/agentkit/server/frontend/src/components/chat/messages/index.ts index b67d448..7fe271e 100644 --- a/src/agentkit/server/frontend/src/components/chat/messages/index.ts +++ b/src/agentkit/server/frontend/src/components/chat/messages/index.ts @@ -9,5 +9,8 @@ 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 CollaborationGraphCard } from './CollaborationGraphCard.vue' +export { default as ReviewResultCard } from './ReviewResultCard.vue' +export { default as RiskFlagCard } from './RiskFlagCard.vue' export { default as ErrorCard } from './ErrorCard.vue' export { default as FileAttachment } from './FileAttachment.vue' diff --git a/src/agentkit/server/frontend/src/stores/chat.ts b/src/agentkit/server/frontend/src/stores/chat.ts index 5b5b142..67114f7 100644 --- a/src/agentkit/server/frontend/src/stores/chat.ts +++ b/src/agentkit/server/frontend/src/stores/chat.ts @@ -14,6 +14,12 @@ import type { IDebateArgumentData, IDebateRoundSummaryData, IDebateResolvedData, + ICollaborationContract, + ICollaborationContractDefinedData, + ICollaborationNotice, + IReviewResult, + IRiskFlag, + ICollaborationGraphData, } from '@/api/types' function generateId(): string { @@ -161,6 +167,10 @@ export const useChatStore = defineStore('chat', () => { status: 'debating' | 'resolved' | 'cancelled' } | null>(null) + // PM Collaboration state (transient, only active during a PM-mode team task) + // Tracks contracts, notices, reviews, and risks for the collaboration graph. + const collaborationState = ref(null) + // --- Getters --- const currentConversation = computed(() => { return conversations.value.find((c) => c.id === currentConversationId.value) @@ -596,6 +606,48 @@ export const useChatStore = defineStore('chat', () => { return _teamStore } + /** Ensure a collaboration_graph message exists in the conversation and update + * it with the latest graph data. Creates the message if absent. */ + function upsertCollaborationGraph(conversationId: string, graphData: ICollaborationGraphData): void { + const conv = conversations.value.find((c) => c.id === conversationId) + if (!conv) return + const existing = [...conv.messages].reverse().find((m) => m.message_type === 'collaboration_graph') + if (existing) { + updateMessage(conversationId, existing.id, { + collaboration_graph: { + contracts: [...graphData.contracts], + notices: [...graphData.notices], + reviews: [...graphData.reviews], + risks: [...graphData.risks], + }, + }) + } else { + const graphMsg: IChatMessage = { + id: generateId(), + role: 'assistant', + content: '', + timestamp: new Date().toISOString(), + status: 'completed', + message_type: 'collaboration_graph', + collaboration_graph: { + contracts: [...graphData.contracts], + notices: [...graphData.notices], + reviews: [...graphData.reviews], + risks: [...graphData.risks], + }, + } + appendMessage(conversationId, graphMsg) + } + } + + /** Ensure collaborationState is initialized; return the live data object. */ + function _ensureCollaborationState(): ICollaborationGraphData { + if (!collaborationState.value) { + collaborationState.value = { contracts: [], notices: [], reviews: [], risks: [] } + } + return collaborationState.value + } + function handleWsMessage(data: WsServerMessage): void { // Discriminated union narrowing: each `case` branch narrows `data` to a // specific variant of WsServerMessage, so typed fields can be accessed @@ -881,6 +933,8 @@ export const useChatStore = defineStore('chat', () => { } case 'team_formed': { + // Reset collaboration state for a fresh team + collaborationState.value = null const conversationId = resolveIncomingConvId() if (!conversationId) break const teamStore = _getTeamStore() @@ -980,6 +1034,36 @@ export const useChatStore = defineStore('chat', () => { } appendMessage(conversationId, planMsg) } + + // U5: Extract collaboration contracts from plan_phases and populate + // collaborationState + collaboration_graph message. The backend + // includes contracts inside plan_update (not as a separate event). + const extractedContracts: Array = [] + for (const phase of data.data.plan_phases) { + if (phase.collaboration_contracts && phase.collaboration_contracts.length > 0) { + for (const c of phase.collaboration_contracts) { + extractedContracts.push({ + from_expert: c.from_expert, + to_expert: c.to_expert, + content_description: c.content_description, + status: c.status, + phase_id: phase.id, + phase_name: phase.name, + }) + } + } + } + if (extractedContracts.length > 0) { + const collab = _ensureCollaborationState() + collab.contracts = extractedContracts + upsertCollaborationGraph(conversationId, collab) + appendStep({ + type: 'team_event', + label: '协作契约定义', + detail: `${extractedContracts.length} 项契约`, + status: 'success', + }, conversationId) + } break } @@ -1005,6 +1089,8 @@ export const useChatStore = defineStore('chat', () => { if (teamStore) { teamStore.clearTeam() } + // Clear collaboration state — team is done + collaborationState.value = null const cid = resolveIncomingConvId() if (cid) { appendStep({ @@ -1336,6 +1422,121 @@ export const useChatStore = defineStore('chat', () => { }, sessionId) break } + + // ── PM Collaboration (U5) 事件 ────────────────────────────────── + + case 'collaboration_contract_defined': { + const d = data.data as ICollaborationContractDefinedData + const collab = _ensureCollaborationState() + // Replace contracts for this phase, keep contracts from other phases + const others = collab.contracts.filter((c) => c.phase_id !== d.phase_id) + const newContracts: Array = + d.contracts.map((c) => ({ + from_expert: c.from_expert, + to_expert: c.to_expert, + content_description: c.content_description, + status: c.status, + phase_id: d.phase_id, + phase_name: d.phase_name, + })) + collab.contracts = [...others, ...newContracts] + const sessionId = resolveIncomingConvId() + if (sessionId) { + upsertCollaborationGraph(sessionId, collab) + appendStep({ + type: 'team_event', + label: '协作契约定义', + detail: `${newContracts.length} 项契约 · ${d.phase_name}`, + status: 'success', + }, sessionId) + } + break + } + + case 'collaboration_notice': { + const d = data.data as ICollaborationNotice + const collab = _ensureCollaborationState() + // Dedup by output_key to avoid duplicate notices on replay + if (!collab.notices.some((n) => n.output_key === d.output_key && n.from_expert === d.from_expert)) { + collab.notices.push(d) + } + const sessionId = resolveIncomingConvId() + if (sessionId) { + upsertCollaborationGraph(sessionId, collab) + appendStep({ + type: 'team_event', + label: '协作通知', + detail: `${d.from_expert} → ${d.to_expert}`, + status: 'success', + }, sessionId) + } + break + } + + case 'review_result': { + const d = data.data as IReviewResult + const collab = _ensureCollaborationState() + // Replace any existing review for the same phase to keep latest state + collab.reviews = collab.reviews.filter((r) => r.phase_id !== d.phase_id) + collab.reviews.push(d) + const sessionId = resolveIncomingConvId() + if (sessionId) { + // Update the collaboration graph with new review status (node colors) + upsertCollaborationGraph(sessionId, collab) + // Create a dedicated ReviewResultCard message + appendMessage(sessionId, { + id: generateId(), + role: 'assistant', + content: d.feedback || (d.passed ? '验收通过' : '验收未通过'), + timestamp: new Date().toISOString(), + status: 'completed', + message_type: 'review_result', + review_result: d, + expert_name: d.expert, + }) + appendStep({ + type: 'team_event', + label: d.passed ? '验收通过' : (d.final_status === 'failed' ? '验收失败' : '要求返工'), + detail: d.phase_name, + status: d.passed ? 'success' : 'error', + }, sessionId) + } + break + } + + case 'risk_flagged': { + const d = data.data as IRiskFlag + const collab = _ensureCollaborationState() + // Dedup by expert + phase + description to avoid duplicates on replay + if (!collab.risks.some( + (r) => r.expert === d.expert && r.phase_id === d.phase_id && r.risk_description === d.risk_description, + )) { + collab.risks.push(d) + } + const sessionId = resolveIncomingConvId() + if (sessionId) { + // Update the collaboration graph to show risk marker on the node + upsertCollaborationGraph(sessionId, collab) + // Create a dedicated RiskFlagCard message + appendMessage(sessionId, { + id: generateId(), + role: 'assistant', + content: d.risk_description, + timestamp: new Date().toISOString(), + status: 'completed', + message_type: 'risk_flagged', + risk_flag: d, + expert_name: d.expert_name || d.expert, + }) + appendStep({ + type: 'team_event', + label: '风险标记', + detail: `${d.expert_name || d.expert}: ${d.risk_description.slice(0, 30)}`, + status: 'error', + }, sessionId) + } + break + } } } @@ -1389,6 +1590,7 @@ export const useChatStore = defineStore('chat', () => { streamingStepsByConv, boardState, debateState, + collaborationState, // Legacy aliases (derive from current conversation for backward compat). // New code should use `isCurrentLoading` / `currentStreamingSteps` instead. isLoading: isCurrentLoading,