feat: 增加前端监督关系

This commit is contained in:
ViperEkura 2026-04-28 23:19:47 +08:00
parent 897e55e672
commit c36563f968
3 changed files with 1647 additions and 4 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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">&times;</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>

View File

@ -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)