feat: 增加前端监督关系
This commit is contained in:
parent
897e55e672
commit
c36563f968
File diff suppressed because it is too large
Load Diff
|
|
@ -5,6 +5,7 @@ import { parallelStreamManager } from '../utils/parallelStreamManager.js'
|
||||||
import { useParallelStreamStore } from '../utils/parallelStreamStore.js'
|
import { useParallelStreamStore } from '../utils/parallelStreamStore.js'
|
||||||
import MessageBubble from '../components/MessageBubble.vue'
|
import MessageBubble from '../components/MessageBubble.vue'
|
||||||
import ParallelMessages from '../components/ParallelMessages.vue'
|
import ParallelMessages from '../components/ParallelMessages.vue'
|
||||||
|
import SupervisionGraphEditor from '../components/SupervisionGraphEditor.vue'
|
||||||
|
|
||||||
const store = useParallelStreamStore()
|
const store = useParallelStreamStore()
|
||||||
|
|
||||||
|
|
@ -52,6 +53,133 @@ const creating = ref(false)
|
||||||
const newRoom = ref({ title: '', task: '', max_rounds: 5, agent_ids: [], execution_mode: 'sequential' })
|
const newRoom = ref({ title: '', task: '', max_rounds: 5, agent_ids: [], execution_mode: 'sequential' })
|
||||||
const showAddToRoom = ref(false)
|
const showAddToRoom = ref(false)
|
||||||
|
|
||||||
|
// ============ Wizard state (Supervision Graph) ============
|
||||||
|
const showCreateWizard = ref(false)
|
||||||
|
const wizardStep = ref(1)
|
||||||
|
const wizardData = ref({
|
||||||
|
title: '',
|
||||||
|
task: '',
|
||||||
|
max_rounds: 5,
|
||||||
|
execution_mode: 'sequential',
|
||||||
|
agent_ids: [],
|
||||||
|
agent_roles: {},
|
||||||
|
supervision_edges: []
|
||||||
|
})
|
||||||
|
const wizardRef = ref(null) // 用于参考 SuperGraphEditor 组件
|
||||||
|
|
||||||
|
// Wizard navigation
|
||||||
|
function openWizard() {
|
||||||
|
wizardStep.value = 1
|
||||||
|
wizardData.value = {
|
||||||
|
title: '',
|
||||||
|
task: '',
|
||||||
|
max_rounds: 5,
|
||||||
|
execution_mode: 'sequential',
|
||||||
|
agent_ids: [],
|
||||||
|
agent_roles: {},
|
||||||
|
supervision_edges: []
|
||||||
|
}
|
||||||
|
showCreateWizard.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWizard() {
|
||||||
|
showCreateWizard.value = false
|
||||||
|
wizardStep.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextWizardStep() {
|
||||||
|
if (wizardStep.value < 4) wizardStep.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevWizardStep() {
|
||||||
|
if (wizardStep.value > 1) wizardStep.value--
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectWizardTemplate(templateId) {
|
||||||
|
// 根据任务模板填充默认的 task 描述
|
||||||
|
const templates = {
|
||||||
|
custom: '请描述需要 Agent 团队完成的任务...',
|
||||||
|
code_review: '请对以下代码进行全面的审查和改进:\n\n```\n\n```',
|
||||||
|
architecture: '请设计一个系统架构方案,涵盖:\n1. 系统架构概览\n2. 核心模块设计\n3. 数据流设计\n4. 技术选型',
|
||||||
|
debugging: '请协助排查以下问题:\n\n问题描述:\n\n复现步骤:\n\n错误日志:'
|
||||||
|
}
|
||||||
|
if (templates[templateId] && !wizardData.value.task) {
|
||||||
|
wizardData.value.task = templates[templateId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWizardAgent(agentId) {
|
||||||
|
const idx = wizardData.value.agent_ids.indexOf(agentId)
|
||||||
|
if (idx >= 0) {
|
||||||
|
wizardData.value.agent_ids.splice(idx, 1)
|
||||||
|
delete wizardData.value.agent_roles[agentId]
|
||||||
|
} else {
|
||||||
|
wizardData.value.agent_ids.push(agentId)
|
||||||
|
wizardData.value.agent_roles[agentId] = 'producer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSupervisionChange(config) {
|
||||||
|
if (config) {
|
||||||
|
wizardData.value.agent_roles = config.roles || {}
|
||||||
|
wizardData.value.supervision_edges = config.edges || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRoomFromWizard() {
|
||||||
|
const data = wizardData.value
|
||||||
|
if (!data.title || !data.task || data.agent_ids.length === 0) return
|
||||||
|
|
||||||
|
creating.value = true
|
||||||
|
try {
|
||||||
|
// 获取 SupervisionGraphEditor 的完整配置
|
||||||
|
let supervisionConfig = null
|
||||||
|
if (wizardRef.value && data.execution_mode === 'review_loop') {
|
||||||
|
supervisionConfig = wizardRef.value.getSupervisionConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 agent 列表
|
||||||
|
const agents = data.agent_ids
|
||||||
|
.map(id => agentPool.value.find(a => a.id === id))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((a, idx) => {
|
||||||
|
const base = { agent_id: a.id }
|
||||||
|
if (data.execution_mode === 'review_loop') {
|
||||||
|
// 添加监督角色和边配置
|
||||||
|
const role = data.agent_roles[a.id] || 'producer'
|
||||||
|
const edgeConfig = supervisionConfig?.edges?.filter(e => e.source === a.id || e.target === a.id) || []
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
agent_type: role,
|
||||||
|
reviews_for: JSON.stringify(
|
||||||
|
edgeConfig.filter(e => e.source === a.id).map(e => e.target)
|
||||||
|
),
|
||||||
|
reviewed_by: JSON.stringify(
|
||||||
|
edgeConfig.filter(e => e.target === a.id).map(e => e.source)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await chatRoomsAPI.create({
|
||||||
|
title: data.title,
|
||||||
|
task: data.task,
|
||||||
|
max_rounds: data.max_rounds,
|
||||||
|
execution_mode: data.execution_mode,
|
||||||
|
agents
|
||||||
|
})
|
||||||
|
closeWizard()
|
||||||
|
await loadRooms()
|
||||||
|
const created = res.data
|
||||||
|
if (created?.id) selectRoom(created.id)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create room:', e)
|
||||||
|
} finally {
|
||||||
|
creating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Selected room state ============
|
// ============ Selected room state ============
|
||||||
const selectedId = ref(null)
|
const selectedId = ref(null)
|
||||||
const room = ref(null)
|
const room = ref(null)
|
||||||
|
|
@ -581,6 +709,33 @@ function randomColor() {
|
||||||
return colors[Math.floor(Math.random() * colors.length)]
|
return colors[Math.floor(Math.random() * colors.length)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Wizard helpers ============
|
||||||
|
function getAgentColor(id) {
|
||||||
|
const agent = agentPool.value.find(a => a.id === id)
|
||||||
|
return agent?.color || '#3b82f6'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgentName(id) {
|
||||||
|
const agent = agentPool.value.find(a => a.id === id)
|
||||||
|
return agent?.name || `Agent ${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedAgents() {
|
||||||
|
return wizardData.value.agent_ids
|
||||||
|
.map(id => agentPool.value.find(a => a.id === id))
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoleLabelText(role) {
|
||||||
|
const labels = {
|
||||||
|
producer: '🎨 Producer - 方案提出者',
|
||||||
|
reviewer: '🔍 Reviewer - 审查员',
|
||||||
|
executor: '⚙️ Executor - 执行者',
|
||||||
|
observer: '👁️ Observer - 观察员'
|
||||||
|
}
|
||||||
|
return labels[role] || role
|
||||||
|
}
|
||||||
|
|
||||||
watch(messages, () => { nextTick(scrollToBottom) }, { deep: true })
|
watch(messages, () => { nextTick(scrollToBottom) }, { deep: true })
|
||||||
|
|
||||||
// 启动聊天室时强制滚动
|
// 启动聊天室时强制滚动
|
||||||
|
|
@ -639,7 +794,7 @@ onUnmounted(() => {
|
||||||
<!-- Room list tab -->
|
<!-- Room list tab -->
|
||||||
<div v-if="sidebarTab === 'rooms'" class="sidebar-tab-content">
|
<div v-if="sidebarTab === 'rooms'" class="sidebar-tab-content">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<button class="btn-new-conv" @click="showCreate = true">+ 新建聊天室</button>
|
<button class="btn-new-conv" @click="openWizard">+ 新建聊天室</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="roomsLoading" class="sidebar-loading"><div class="spinner-small"></div></div>
|
<div v-if="roomsLoading" class="sidebar-loading"><div class="spinner-small"></div></div>
|
||||||
<div v-else-if="rooms.length === 0" class="sidebar-empty">暂无聊天室</div>
|
<div v-else-if="rooms.length === 0" class="sidebar-empty">暂无聊天室</div>
|
||||||
|
|
@ -742,7 +897,8 @@ onUnmounted(() => {
|
||||||
<div class="toolbar-badges">
|
<div class="toolbar-badges">
|
||||||
<span class="status-badge" :class="statusMap[room.status]?.class">{{ statusMap[room.status]?.label }}</span>
|
<span class="status-badge" :class="statusMap[room.status]?.class">{{ statusMap[room.status]?.label }}</span>
|
||||||
<span class="round-badge" v-if="room.current_round > 0">R{{ room.current_round }}/{{ room.max_rounds }}</span>
|
<span class="round-badge" v-if="room.current_round > 0">R{{ room.current_round }}/{{ room.max_rounds }}</span>
|
||||||
<span v-if="isParallelMode" class="mode-badge parallel">⚡ Parallel</span>
|
<span v-if="executionMode === 'parallel'" class="mode-badge parallel">⚡ Parallel</span>
|
||||||
|
<span v-else-if="executionMode === 'review_loop'" class="mode-badge review-loop">🔄 Review Loop</span>
|
||||||
<span v-else class="mode-badge sequential">📋 Sequential</span>
|
<span v-else class="mode-badge sequential">📋 Sequential</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -755,6 +911,7 @@ onUnmounted(() => {
|
||||||
>
|
>
|
||||||
<option value="sequential">📋 Sequential</option>
|
<option value="sequential">📋 Sequential</option>
|
||||||
<option value="parallel">⚡ Parallel</option>
|
<option value="parallel">⚡ Parallel</option>
|
||||||
|
<option value="review_loop">🔄 Review Loop</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<!-- Parallel status indicator -->
|
<!-- Parallel status indicator -->
|
||||||
|
|
@ -886,6 +1043,7 @@ onUnmounted(() => {
|
||||||
<select v-model="newRoom.execution_mode">
|
<select v-model="newRoom.execution_mode">
|
||||||
<option value="sequential">📋 串行 - Agent按顺序发言</option>
|
<option value="sequential">📋 串行 - Agent按顺序发言</option>
|
||||||
<option value="parallel">⚡ 并行 - Agent同时发言</option>
|
<option value="parallel">⚡ 并行 - Agent同时发言</option>
|
||||||
|
<option value="review_loop">🔄 监督循环 - Agent相互审查</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -966,6 +1124,258 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== Wizard: Create Room with Supervision Graph ===== -->
|
||||||
|
<div v-if="showCreateWizard" class="wizard-overlay" @click.self="closeWizard">
|
||||||
|
<div class="wizard-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="wizard-header">
|
||||||
|
<h2>{{ wizardData.execution_mode === 'review_loop' ? '🔄 创建监督型聊天室' : '🤖 创建聊天室' }}</h2>
|
||||||
|
<button class="btn-close" @click="closeWizard">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step indicators -->
|
||||||
|
<div class="wizard-steps">
|
||||||
|
<div class="step-indicator" :class="{ active: wizardStep >= 1, current: wizardStep === 1 }">
|
||||||
|
<span class="step-num">1</span>
|
||||||
|
<span class="step-label">基本信息</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-line"></div>
|
||||||
|
<div class="step-indicator" :class="{ active: wizardStep >= 2, current: wizardStep === 2 }">
|
||||||
|
<span class="step-num">2</span>
|
||||||
|
<span class="step-label">选择 Agent</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-line"></div>
|
||||||
|
<div class="step-indicator" :class="{ active: wizardStep >= 3, current: wizardStep === 3 }">
|
||||||
|
<span class="step-num">3</span>
|
||||||
|
<span class="step-label">{{ wizardData.execution_mode === 'review_loop' ? '监督关系' : '确认' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-line"></div>
|
||||||
|
<div class="step-indicator" :class="{ active: wizardStep >= 4, current: wizardStep === 4 }">
|
||||||
|
<span class="step-num">4</span>
|
||||||
|
<span class="step-label">确认创建</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 1: Basic Info -->
|
||||||
|
<div class="wizard-body">
|
||||||
|
<div v-if="wizardStep === 1" class="wizard-step">
|
||||||
|
<div class="step-title">📋 基本设置</div>
|
||||||
|
<div class="fg"><label>聊天室标题</label><input v-model="wizardData.title" placeholder="项目架构设计讨论" /></div>
|
||||||
|
<div class="fg"><label>任务描述</label><textarea v-model="wizardData.task" rows="4" placeholder="描述需要 Agent 团队讨论的问题..."></textarea></div>
|
||||||
|
<div class="fg-row">
|
||||||
|
<div class="fg" style="flex: 1;">
|
||||||
|
<label>最大轮次</label>
|
||||||
|
<input v-model.number="wizardData.max_rounds" type="number" min="1" max="20" />
|
||||||
|
</div>
|
||||||
|
<div class="fg" style="flex: 1;">
|
||||||
|
<label>执行模式</label>
|
||||||
|
<select v-model="wizardData.execution_mode">
|
||||||
|
<option value="sequential">📋 串行 - Agent按顺序发言</option>
|
||||||
|
<option value="parallel">⚡ 并行 - Agent同时发言</option>
|
||||||
|
<option value="review_loop">🔄 监督循环 - Agent相互审查</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 任务模板快捷选择 -->
|
||||||
|
<div class="template-grid">
|
||||||
|
<div class="template-card" :class="{ selected: wizardData.task.includes('代码') }" @click="selectWizardTemplate('code_review')">
|
||||||
|
<span class="template-icon">🔍</span>
|
||||||
|
<span class="template-name">代码审查</span>
|
||||||
|
</div>
|
||||||
|
<div class="template-card" @click="selectWizardTemplate('architecture')">
|
||||||
|
<span class="template-icon">🏗️</span>
|
||||||
|
<span class="template-name">架构设计</span>
|
||||||
|
</div>
|
||||||
|
<div class="template-card" @click="selectWizardTemplate('debugging')">
|
||||||
|
<span class="template-icon">🐛</span>
|
||||||
|
<span class="template-name">调试排查</span>
|
||||||
|
</div>
|
||||||
|
<div class="template-card" @click="selectWizardTemplate('custom')">
|
||||||
|
<span class="template-icon">📝</span>
|
||||||
|
<span class="template-name">自定义</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Select Agents -->
|
||||||
|
<div v-if="wizardStep === 2" class="wizard-step">
|
||||||
|
<div class="step-title">🤖 选择参与讨论的 Agent</div>
|
||||||
|
<div v-if="agentPool.length === 0" class="empty-hint">
|
||||||
|
<p>暂无可用 Agent,请先在 Agent 池中创建</p>
|
||||||
|
<button class="btn-link" @click="closeWizard(); startCreateAgent()">前往创建</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="agent-selection">
|
||||||
|
<div
|
||||||
|
v-for="agent in agentPool"
|
||||||
|
:key="agent.id"
|
||||||
|
class="agent-option"
|
||||||
|
:class="{ selected: wizardData.agent_ids.includes(agent.id) }"
|
||||||
|
@click="toggleWizardAgent(agent.id)"
|
||||||
|
>
|
||||||
|
<input type="checkbox" :checked="wizardData.agent_ids.includes(agent.id)" />
|
||||||
|
<span class="agent-dot" :style="{ background: agent.color }">{{ agent.name.charAt(0) }}</span>
|
||||||
|
<div class="agent-option-info">
|
||||||
|
<span class="agent-option-name">{{ agent.name }}</span>
|
||||||
|
<span class="agent-option-role">{{ agent.role || '通用' }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="agent-option-model">{{ agent.model || 'default' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="selected-info" v-if="wizardData.agent_ids.length > 0">
|
||||||
|
已选择 <strong>{{ wizardData.agent_ids.length }}</strong> 个 Agent
|
||||||
|
</div>
|
||||||
|
<div class="selected-info" v-else>请选择至少 1 个 Agent</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Supervision Graph (only for review_loop mode) -->
|
||||||
|
<div v-if="wizardStep === 3" class="wizard-step">
|
||||||
|
<template v-if="wizardData.execution_mode === 'review_loop'">
|
||||||
|
<div class="step-title">🔗 配置监督关系</div>
|
||||||
|
<div class="supervision-hint">
|
||||||
|
<p>设置每个 Agent 的角色类型,以及它们之间的监督关系。</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>🎨 Producer</strong> - 方案提出者</li>
|
||||||
|
<li><strong>🔍 Reviewer</strong> - 审查员(可指定能力标签)</li>
|
||||||
|
<li><strong>⚙️ Executor</strong> - 执行者</li>
|
||||||
|
<li><strong>👁️ Observer</strong> - 观察员</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 角色分配 -->
|
||||||
|
<div class="role-assignment-section">
|
||||||
|
<div class="sub-title">Agent 角色分配</div>
|
||||||
|
<div class="role-assignment-list">
|
||||||
|
<div
|
||||||
|
v-for="agentId in wizardData.agent_ids"
|
||||||
|
:key="agentId"
|
||||||
|
class="role-assignment-row"
|
||||||
|
>
|
||||||
|
<div class="role-agent-badge" :style="{ background: getAgentColor(agentId) }">
|
||||||
|
{{ getAgentName(agentId).charAt(0) }}
|
||||||
|
</div>
|
||||||
|
<span class="role-agent-name">{{ getAgentName(agentId) }}</span>
|
||||||
|
<select
|
||||||
|
v-model="wizardData.agent_roles[agentId]"
|
||||||
|
class="role-select"
|
||||||
|
@change="onSupervisionChange"
|
||||||
|
>
|
||||||
|
<option value="producer">🎨 Producer - 方案提出者</option>
|
||||||
|
<option value="reviewer">🔍 Reviewer - 审查员</option>
|
||||||
|
<option value="executor">⚙️ Executor - 执行者</option>
|
||||||
|
<option value="observer">👁️ Observer - 观察员</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Supervision Graph Editor -->
|
||||||
|
<div class="sub-title" style="margin-top: 1rem;">监督关系图</div>
|
||||||
|
<div class="graph-hint">拖拽节点移动 · 双击节点连线 · 点击边配置 · 按 Delete 删除边</div>
|
||||||
|
<SupervisionGraphEditor
|
||||||
|
ref="wizardRef"
|
||||||
|
:agents="getSelectedAgents()"
|
||||||
|
:initial-roles="wizardData.agent_roles"
|
||||||
|
:initial-edges="wizardData.supervision_edges"
|
||||||
|
:container-height="420"
|
||||||
|
@change="onSupervisionChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<!-- For non-review_loop modes, skip to confirmation -->
|
||||||
|
<div class="step-title">✅ 确认 Agent 选择</div>
|
||||||
|
<div v-if="wizardData.agent_ids.length === 0" class="empty-hint">请先选择 Agent</div>
|
||||||
|
<div v-else class="confirm-agents-list">
|
||||||
|
<div
|
||||||
|
v-for="agentId in wizardData.agent_ids"
|
||||||
|
:key="agentId"
|
||||||
|
class="confirm-agent-item"
|
||||||
|
>
|
||||||
|
<span class="agent-dot" :style="{ background: getAgentColor(agentId) }">
|
||||||
|
{{ getAgentName(agentId).charAt(0) }}
|
||||||
|
</span>
|
||||||
|
<span class="confirm-agent-name">{{ getAgentName(agentId) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: Confirmation -->
|
||||||
|
<div v-if="wizardStep === 4" class="wizard-step">
|
||||||
|
<div class="step-title">📋 确认配置</div>
|
||||||
|
<div class="confirm-summary">
|
||||||
|
<div class="summary-section">
|
||||||
|
<h4>基本信息</h4>
|
||||||
|
<div class="summary-item"><strong>标题:</strong> {{ wizardData.title }}</div>
|
||||||
|
<div class="summary-item"><strong>执行模式:</strong> {{ wizardData.execution_mode === 'review_loop' ? '🔄 监督循环' : wizardData.execution_mode === 'parallel' ? '⚡ 并行' : '📋 串行' }}</div>
|
||||||
|
<div class="summary-item"><strong>最大轮次:</strong> {{ wizardData.max_rounds }}</div>
|
||||||
|
<div class="summary-task"><strong>任务:</strong> {{ wizardData.task }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-section">
|
||||||
|
<h4>Agent 列表</h4>
|
||||||
|
<div class="summary-agents">
|
||||||
|
<div
|
||||||
|
v-for="agentId in wizardData.agent_ids"
|
||||||
|
:key="agentId"
|
||||||
|
class="summary-agent"
|
||||||
|
>
|
||||||
|
<span class="agent-dot-sm" :style="{ background: getAgentColor(agentId) }">
|
||||||
|
{{ getAgentName(agentId).charAt(0) }}
|
||||||
|
</span>
|
||||||
|
<div class="summary-agent-info">
|
||||||
|
<span class="summary-agent-name">{{ getAgentName(agentId) }}</span>
|
||||||
|
<span v-if="wizardData.execution_mode === 'review_loop'" class="summary-agent-role">
|
||||||
|
{{ getRoleLabelText(wizardData.agent_roles[agentId]) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="wizardData.execution_mode === 'review_loop' && wizardData.supervision_edges.length > 0" class="summary-section">
|
||||||
|
<h4>监督关系 ({{ wizardData.supervision_edges.length }} 条)</h4>
|
||||||
|
<div class="summary-edges">
|
||||||
|
<div
|
||||||
|
v-for="edge in wizardData.supervision_edges"
|
||||||
|
:key="edge.id"
|
||||||
|
class="summary-edge-item"
|
||||||
|
>
|
||||||
|
<span class="edge-src">{{ getAgentName(edge.source) }}</span>
|
||||||
|
<span class="edge-arrow-summary">→</span>
|
||||||
|
<span class="edge-tgt">{{ getAgentName(edge.target) }}</span>
|
||||||
|
<span class="edge-strictness">★{{ edge.strictness }}</span>
|
||||||
|
<span v-if="edge.focus" class="edge-focus">{{ edge.focus }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="wizard-footer">
|
||||||
|
<button v-if="wizardStep > 1" class="btn-secondary" @click="prevWizardStep">← 上一步</button>
|
||||||
|
<div class="flex-spacer"></div>
|
||||||
|
<button class="btn-secondary" @click="closeWizard">取消</button>
|
||||||
|
<button
|
||||||
|
v-if="wizardStep < 4"
|
||||||
|
class="btn-primary"
|
||||||
|
@click="nextWizardStep"
|
||||||
|
:disabled="(wizardStep === 1 && (!wizardData.title || !wizardData.task)) || (wizardStep === 2 && wizardData.agent_ids.length === 0)"
|
||||||
|
>
|
||||||
|
下一步 →
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="wizardStep === 4"
|
||||||
|
class="btn-primary"
|
||||||
|
@click="createRoomFromWizard"
|
||||||
|
:disabled="creating || wizardData.agent_ids.length === 0"
|
||||||
|
>
|
||||||
|
{{ creating ? '创建中...' : '✅ 确认创建' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -1080,6 +1490,7 @@ textarea.fi { resize: vertical; min-height: 50px; }
|
||||||
.mode-badge { font-size: 0.6rem; padding: 0.1rem 0.4rem; border-radius: 8px; font-weight: 600; }
|
.mode-badge { font-size: 0.6rem; padding: 0.1rem 0.4rem; border-radius: 8px; font-weight: 600; }
|
||||||
.mode-badge.parallel { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
|
.mode-badge.parallel { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
|
||||||
.mode-badge.sequential { background: rgba(107, 114, 128, 0.1); color: #6b7280; }
|
.mode-badge.sequential { background: rgba(107, 114, 128, 0.1); color: #6b7280; }
|
||||||
|
.mode-badge.review-loop { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
|
||||||
|
|
||||||
.mode-selector { margin-left: auto; }
|
.mode-selector { margin-left: auto; }
|
||||||
.mode-select { padding: 0.25rem 0.5rem; font-size: 0.75rem; border: 1px solid var(--border-light); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); cursor: pointer; }
|
.mode-select { padding: 0.25rem 0.5rem; font-size: 0.75rem; border: 1px solid var(--border-light); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); cursor: pointer; }
|
||||||
|
|
@ -1303,4 +1714,63 @@ textarea.fi { resize: vertical; min-height: 50px; }
|
||||||
/* Agent pick row in add-to-room modal */
|
/* Agent pick row in add-to-room modal */
|
||||||
.agent-pick-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.7rem 0.8rem; border-radius: 10px; cursor: pointer; transition: all 0.2s ease; border: 1px solid transparent; }
|
.agent-pick-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.7rem 0.8rem; border-radius: 10px; cursor: pointer; transition: all 0.2s ease; border: 1px solid transparent; }
|
||||||
.agent-pick-row:hover { background: var(--bg-secondary); border-color: var(--border-light); box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
.agent-pick-row:hover { background: var(--bg-secondary); border-color: var(--border-light); box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
||||||
|
|
||||||
|
/* ===== Wizard Styles ===== */
|
||||||
|
.wizard-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 2000; }
|
||||||
|
.wizard-container { background: var(--bg-primary); border-radius: 16px; width: 640px; max-width: 95vw; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; }
|
||||||
|
.wizard-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-light); }
|
||||||
|
.wizard-header h2 { margin: 0; font-size: 1.1rem; }
|
||||||
|
|
||||||
|
.wizard-steps { display: flex; align-items: center; justify-content: center; padding: 1.25rem; background: var(--bg-secondary); gap: 0.5rem; }
|
||||||
|
.step-indicator { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; opacity: 0.4; transition: all 0.2s; }
|
||||||
|
.step-indicator.active { opacity: 1; }
|
||||||
|
.step-indicator.current .step-num { background: var(--accent-primary); color: white; transform: scale(1.1); }
|
||||||
|
.step-num { width: 28px; height: 28px; border-radius: 50%; background: var(--border-light); color: var(--text-secondary); display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.8rem; transition: all 0.2s; }
|
||||||
|
.step-label { font-size: 0.7rem; color: var(--text-secondary); white-space: nowrap; }
|
||||||
|
.step-line { width: 40px; height: 2px; background: var(--border-light); flex-shrink: 0; }
|
||||||
|
|
||||||
|
.wizard-body { flex: 1; overflow-y: auto; padding: 1.5rem; }
|
||||||
|
.wizard-step { animation: fadeIn 0.3s ease; }
|
||||||
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
|
||||||
|
.step-title { font-size: 0.9rem; font-weight: 600; color: var(--text-primary); margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.template-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-top: 0.5rem; }
|
||||||
|
.template-card { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; padding: 0.75rem; border: 2px solid var(--border-light); border-radius: 10px; cursor: pointer; transition: all 0.2s; text-align: center; }
|
||||||
|
.template-card:hover { border-color: var(--accent-primary); background: var(--accent-primary-light); }
|
||||||
|
.template-card.selected { border-color: var(--accent-primary); background: var(--accent-primary-light); }
|
||||||
|
.template-icon { font-size: 1.5rem; }
|
||||||
|
.template-name { font-size: 0.75rem; font-weight: 600; color: var(--text-primary); }
|
||||||
|
|
||||||
|
.mode-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; }
|
||||||
|
.mode-card { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; padding: 1rem; border: 2px solid var(--border-light); border-radius: 10px; cursor: pointer; transition: all 0.2s; text-align: center; }
|
||||||
|
.mode-card:hover { border-color: var(--accent-primary); }
|
||||||
|
.mode-card.selected { border-color: var(--accent-primary); background: var(--accent-primary-light); }
|
||||||
|
.mode-icon { font-size: 1.5rem; }
|
||||||
|
.mode-title { font-size: 0.85rem; font-weight: 600; color: var(--text-primary); }
|
||||||
|
.mode-desc { font-size: 0.7rem; color: var(--text-secondary); }
|
||||||
|
|
||||||
|
.agent-selection { display: flex; flex-direction: column; gap: 0.5rem; max-height: 300px; overflow-y: auto; }
|
||||||
|
.agent-option { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 2px solid var(--border-light); border-radius: 10px; cursor: pointer; transition: all 0.2s; }
|
||||||
|
.agent-option:hover { border-color: var(--accent-primary); background: var(--bg-secondary); }
|
||||||
|
.agent-option.selected { border-color: var(--accent-primary); background: var(--accent-primary-light); }
|
||||||
|
.agent-option input[type="checkbox"] { display: none; }
|
||||||
|
.selected-info { margin-top: 1rem; padding: 0.75rem; background: var(--bg-secondary); border-radius: 8px; font-size: 0.85rem; color: var(--text-secondary); text-align: center; }
|
||||||
|
|
||||||
|
.confirm-summary { display: flex; flex-direction: column; gap: 1.25rem; }
|
||||||
|
.summary-section h4 { font-size: 0.85rem; font-weight: 600; color: var(--text-primary); margin: 0 0 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-light); }
|
||||||
|
.summary-item { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.4rem; }
|
||||||
|
.summary-item strong { color: var(--text-primary); }
|
||||||
|
.summary-agents { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||||
|
.summary-agent { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; background: var(--bg-secondary); border-radius: 8px; font-size: 0.85rem; }
|
||||||
|
.agent-order { width: 20px; height: 20px; background: var(--accent-primary); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 600; }
|
||||||
|
.agent-dot-sm { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.7rem; font-weight: 600; }
|
||||||
|
.summary-task { padding: 0.75rem; background: var(--bg-secondary); border-radius: 8px; font-size: 0.85rem; color: var(--text-secondary); white-space: pre-wrap; max-height: 150px; overflow-y: auto; }
|
||||||
|
|
||||||
|
.wizard-footer { display: flex; align-items: center; gap: 0.75rem; padding: 1rem 1.5rem; border-top: 1px solid var(--border-light); }
|
||||||
|
.flex-spacer { flex: 1; }
|
||||||
|
.empty-hint { text-align: center; padding: 2rem; color: var(--text-secondary); }
|
||||||
|
.empty-hint p { margin: 0 0 1rem; }
|
||||||
|
.btn-link { background: none; border: none; color: var(--accent-primary); cursor: pointer; text-decoration: underline; font-size: 0.85rem; }
|
||||||
|
.btn-link:hover { color: var(--accent-primary-hover); }
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,19 @@ class AgentConfig(BaseModel):
|
||||||
model: str = ""
|
model: str = ""
|
||||||
system_prompt: str = "You are a helpful AI assistant."
|
system_prompt: str = "You are a helpful AI assistant."
|
||||||
color: str = "#2563eb"
|
color: str = "#2563eb"
|
||||||
|
# Supervision fields
|
||||||
|
agent_type: str = "producer" # producer | reviewer | executor | observer
|
||||||
|
reviews_for: Optional[str] = None # JSON: [agent_id_1, agent_id_2]
|
||||||
|
reviewed_by: Optional[str] = None # JSON: [agent_id_1, agent_id_2]
|
||||||
|
review_strictness: int = 3
|
||||||
|
capability_tags: Optional[str] = None # JSON: ["security", "performance"]
|
||||||
|
|
||||||
|
|
||||||
class ChatRoomCreate(BaseModel):
|
class ChatRoomCreate(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
task: str
|
task: str
|
||||||
max_rounds: int = 5
|
max_rounds: int = 5
|
||||||
|
execution_mode: str = "sequential" # sequential | parallel | review_loop
|
||||||
agents: List[AgentConfig] = []
|
agents: List[AgentConfig] = []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,6 +46,7 @@ class ChatRoomUpdate(BaseModel):
|
||||||
task: Optional[str] = None
|
task: Optional[str] = None
|
||||||
max_rounds: Optional[int] = None
|
max_rounds: Optional[int] = None
|
||||||
status: Optional[str] = None
|
status: Optional[str] = None
|
||||||
|
execution_mode: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class AgentCreate(BaseModel):
|
class AgentCreate(BaseModel):
|
||||||
|
|
@ -93,7 +101,8 @@ def create_room(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
title=data.title,
|
title=data.title,
|
||||||
task=data.task,
|
task=data.task,
|
||||||
max_rounds=data.max_rounds
|
max_rounds=data.max_rounds,
|
||||||
|
execution_mode=data.execution_mode
|
||||||
)
|
)
|
||||||
db.add(room)
|
db.add(room)
|
||||||
db.flush()
|
db.flush()
|
||||||
|
|
@ -152,7 +161,12 @@ def create_room(
|
||||||
model=model,
|
model=model,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
color=color,
|
color=color,
|
||||||
turn_order=i
|
turn_order=i,
|
||||||
|
agent_type=agent_cfg.agent_type,
|
||||||
|
reviews_for=agent_cfg.reviews_for,
|
||||||
|
reviewed_by=agent_cfg.reviewed_by,
|
||||||
|
review_strictness=agent_cfg.review_strictness,
|
||||||
|
capability_tags=agent_cfg.capability_tags
|
||||||
)
|
)
|
||||||
db.add(agent)
|
db.add(agent)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue