refactor: 优化前端文件结构
This commit is contained in:
parent
c36563f968
commit
f948dfc45f
|
|
@ -0,0 +1,166 @@
|
|||
<script setup>
|
||||
// Props
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
agentPool: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
'add'
|
||||
])
|
||||
|
||||
// Handlers
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleAdd(agent) {
|
||||
emit('add', agent)
|
||||
handleClose()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="show" class="modal-overlay" @click.self="handleClose">
|
||||
<div class="modal">
|
||||
<!-- Header -->
|
||||
<div class="modal-head">
|
||||
<h3>添加 Agent 到房间</h3>
|
||||
<button class="btn-close" @click="handleClose">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
<div v-if="agentPool.length === 0" class="no-agents-hint">
|
||||
请先在左侧 Agent 池中创建 Agent
|
||||
</div>
|
||||
<div v-else class="agents-list">
|
||||
<div
|
||||
v-for="a in agentPool"
|
||||
:key="a.id"
|
||||
class="agent-pick-row"
|
||||
@click="handleAdd(a)"
|
||||
>
|
||||
<span class="agent-dot" :style="{ background: a.color }">
|
||||
{{ a.name.charAt(0) }}
|
||||
</span>
|
||||
<span class="agent-checkbox-name" :style="{ color: a.color }">
|
||||
{{ a.name }}
|
||||
</span>
|
||||
<span class="agent-checkbox-model">{{ a.model || 'default' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 16px;
|
||||
width: 400px;
|
||||
max-width: 95vw;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.modal-head h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.no-agents-hint {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.agents-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.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-dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-checkbox-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.agent-checkbox-model {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editingAgent: {
|
||||
type: [Object, String],
|
||||
default: null // null | 'new' | agent object
|
||||
},
|
||||
agentForm: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
providers: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
availableModels: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
saving: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
'save',
|
||||
'delete',
|
||||
'providerChange',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
// Computed
|
||||
const isNew = computed(() => props.editingAgent === 'new')
|
||||
const title = computed(() => isNew.value ? '新建 Agent' : '编辑 Agent')
|
||||
const canSave = computed(() => props.agentForm.name && !props.saving)
|
||||
const canDelete = computed(() => !isNew.value)
|
||||
|
||||
// Handlers
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (canSave.value) {
|
||||
emit('save')
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
emit('delete', props.editingAgent)
|
||||
}
|
||||
|
||||
function handleProviderChange() {
|
||||
emit('providerChange')
|
||||
}
|
||||
|
||||
function handleModelChange(e) {
|
||||
emit('update:modelValue', e.target.value)
|
||||
}
|
||||
|
||||
// Color picker
|
||||
function randomColor() {
|
||||
const colors = ['#2563eb', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="show" class="modal-overlay" @click.self="handleClose">
|
||||
<div class="modal">
|
||||
<!-- Header -->
|
||||
<div class="modal-head">
|
||||
<h3>{{ title }}</h3>
|
||||
<button class="btn-close" @click="handleClose">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
<!-- Name -->
|
||||
<div class="fg">
|
||||
<label>名称</label>
|
||||
<input
|
||||
v-model="agentForm.name"
|
||||
placeholder="如: 架构师"
|
||||
class="fi"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Role -->
|
||||
<div class="fg">
|
||||
<label>角色</label>
|
||||
<input
|
||||
v-model="agentForm.role"
|
||||
placeholder="如: 架构师、开发者、审查员"
|
||||
class="fi"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Provider & Color -->
|
||||
<div class="fg-row">
|
||||
<div class="fg" style="flex: 1;">
|
||||
<label>Provider</label>
|
||||
<select
|
||||
v-model="agentForm.provider_id"
|
||||
@change="handleProviderChange"
|
||||
class="fi"
|
||||
>
|
||||
<option :value="null">默认 Provider</option>
|
||||
<option
|
||||
v-for="p in providers"
|
||||
:key="p.id"
|
||||
:value="p.id"
|
||||
>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg fg-color">
|
||||
<label>颜色</label>
|
||||
<div class="color-picker-wrap">
|
||||
<input
|
||||
type="color"
|
||||
v-model="agentForm.color"
|
||||
class="color-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model -->
|
||||
<div class="fg">
|
||||
<label>模型</label>
|
||||
<select
|
||||
:value="agentForm.model"
|
||||
@change="handleModelChange"
|
||||
class="fi"
|
||||
>
|
||||
<option value="">自动选择</option>
|
||||
<option
|
||||
v-for="m in availableModels"
|
||||
:key="m"
|
||||
:value="m"
|
||||
>
|
||||
{{ m }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- System Prompt -->
|
||||
<div class="fg">
|
||||
<label>系统提示词</label>
|
||||
<textarea
|
||||
v-model="agentForm.system_prompt"
|
||||
rows="3"
|
||||
placeholder="定义 Agent 的角色和行为..."
|
||||
class="fi"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-foot">
|
||||
<button
|
||||
v-if="canDelete"
|
||||
class="btn-danger"
|
||||
@click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
<div style="flex: 1;"></div>
|
||||
<button class="btn-secondary" @click="handleClose">取消</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="handleSave"
|
||||
:disabled="!canSave"
|
||||
>
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 16px;
|
||||
width: 480px;
|
||||
max-width: 95vw;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.modal-head h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.fg {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.fg label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.4rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.fg input,
|
||||
.fg textarea,
|
||||
.fg select {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.875rem;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.fg input:focus,
|
||||
.fg textarea:focus,
|
||||
.fg select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.fg textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.fg-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.fg-color {
|
||||
width: 70px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fg-color label {
|
||||
margin-bottom: 0.4rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.color-picker-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 42px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-secondary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.color-picker-wrap:hover {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.color-input {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--danger-bg, rgba(239, 68, 68, 0.1));
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
agentPool: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
creating: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
'create'
|
||||
])
|
||||
|
||||
// Local state
|
||||
const newRoom = ref({
|
||||
title: '',
|
||||
task: '',
|
||||
max_rounds: 5,
|
||||
agent_ids: [],
|
||||
execution_mode: 'sequential'
|
||||
})
|
||||
|
||||
// Computed
|
||||
const canCreate = computed(() =>
|
||||
newRoom.value.title &&
|
||||
newRoom.value.task &&
|
||||
newRoom.value.agent_ids.length > 0 &&
|
||||
!props.creating
|
||||
)
|
||||
|
||||
// Watch show to reset
|
||||
function resetForm() {
|
||||
newRoom.value = {
|
||||
title: '',
|
||||
task: '',
|
||||
max_rounds: 5,
|
||||
agent_ids: [],
|
||||
execution_mode: 'sequential'
|
||||
}
|
||||
}
|
||||
|
||||
// Handlers
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
resetForm()
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
if (canCreate.value) {
|
||||
emit('create', { ...newRoom.value })
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAgent(agentId) {
|
||||
const idx = newRoom.value.agent_ids.indexOf(agentId)
|
||||
if (idx >= 0) {
|
||||
newRoom.value.agent_ids.splice(idx, 1)
|
||||
} else {
|
||||
newRoom.value.agent_ids.push(agentId)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="show" class="modal-overlay" @click.self="handleClose">
|
||||
<div class="modal">
|
||||
<!-- Header -->
|
||||
<div class="modal-head">
|
||||
<h3>新建聊天室</h3>
|
||||
<button class="btn-close" @click="handleClose">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
<!-- Title -->
|
||||
<div class="fg">
|
||||
<label>标题</label>
|
||||
<input
|
||||
v-model="newRoom.title"
|
||||
placeholder="项目架构设计讨论"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Task -->
|
||||
<div class="fg">
|
||||
<label>任务描述</label>
|
||||
<textarea
|
||||
v-model="newRoom.task"
|
||||
rows="3"
|
||||
placeholder="描述需要 Agent 讨论的问题..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Max rounds & Execution mode -->
|
||||
<div class="fg-row">
|
||||
<div class="fg" style="flex: 1;">
|
||||
<label>最大轮次</label>
|
||||
<input
|
||||
v-model.number="newRoom.max_rounds"
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
/>
|
||||
</div>
|
||||
<div class="fg" style="flex: 1;">
|
||||
<label>执行模式</label>
|
||||
<select v-model="newRoom.execution_mode">
|
||||
<option value="sequential">📋 串行</option>
|
||||
<option value="parallel">⚡ 并行</option>
|
||||
<option value="review_loop">🔄 监督循环</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent selection -->
|
||||
<div class="agents-select">
|
||||
<label class="agents-select-label">选择 Agent</label>
|
||||
<div v-if="agentPool.length === 0" class="no-agents-hint">
|
||||
请先在左侧 Agent 池中创建 Agent
|
||||
</div>
|
||||
<div v-else class="agents-checkboxes">
|
||||
<label
|
||||
v-for="a in agentPool"
|
||||
:key="a.id"
|
||||
class="agent-checkbox"
|
||||
:class="{ checked: newRoom.agent_ids.includes(a.id) }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="a.id"
|
||||
v-model="newRoom.agent_ids"
|
||||
/>
|
||||
<span class="agent-dot" :style="{ background: a.color }">
|
||||
{{ a.name.charAt(0) }}
|
||||
</span>
|
||||
<span class="agent-checkbox-name" :style="{ color: a.color }">
|
||||
{{ a.name }}
|
||||
</span>
|
||||
<span class="agent-checkbox-model">{{ a.model || 'default' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-foot">
|
||||
<button class="btn-secondary" @click="handleClose">取消</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="handleCreate"
|
||||
:disabled="!canCreate"
|
||||
>
|
||||
{{ creating ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 16px;
|
||||
width: 480px;
|
||||
max-width: 95vw;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.modal-head h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.fg {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.fg label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.4rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.fg input,
|
||||
.fg textarea,
|
||||
.fg select {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.875rem;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.fg input:focus,
|
||||
.fg textarea:focus,
|
||||
.fg select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.fg textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.fg-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.agents-select {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.agents-select-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.no-agents-hint {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.agents-checkboxes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.agent-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.agent-checkbox:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-light);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.agent-checkbox.checked {
|
||||
background: var(--accent-primary-light);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.agent-checkbox input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-dot {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-checkbox-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-checkbox-model {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,399 @@
|
|||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import MessageBubble from '../MessageBubble.vue'
|
||||
import ParallelMessages from '../ParallelMessages.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
messages: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
streamingMessages: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
streaming: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
messagesLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isParallelMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
roomId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// Refs
|
||||
const messagesContainer = ref(null)
|
||||
const isNearBottom = ref(true)
|
||||
|
||||
// Group messages by round number
|
||||
const groupedMessages = computed(() => {
|
||||
const groups = []
|
||||
let currentGroup = []
|
||||
let currentRound = null
|
||||
|
||||
for (const msg of props.messages) {
|
||||
const round = msg.round_number || 0
|
||||
if (round !== currentRound) {
|
||||
if (currentGroup.length > 0) {
|
||||
groups.push(currentGroup)
|
||||
}
|
||||
currentGroup = [msg]
|
||||
currentRound = round
|
||||
} else {
|
||||
currentGroup.push(msg)
|
||||
}
|
||||
}
|
||||
if (currentGroup.length > 0) {
|
||||
groups.push(currentGroup)
|
||||
}
|
||||
return groups
|
||||
})
|
||||
|
||||
const hasMessages = computed(() =>
|
||||
props.messages.length > 0 || Object.keys(props.streamingMessages).length > 0
|
||||
)
|
||||
|
||||
const streamingMessageList = computed(() => Object.values(props.streamingMessages))
|
||||
|
||||
// Scroll functions
|
||||
function scrollToBottom() {
|
||||
if (!isNearBottom.value) return
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
function checkNearBottom() {
|
||||
if (!messagesContainer.value) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = messagesContainer.value
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
|
||||
isNearBottom.value = distanceFromBottom <= 100
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
checkNearBottom()
|
||||
}
|
||||
|
||||
function forceScrollToBottom() {
|
||||
nextTick(() => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
isNearBottom.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(() => props.messages, () => {
|
||||
nextTick(scrollToBottom)
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => props.streaming, (val) => {
|
||||
if (val) forceScrollToBottom()
|
||||
})
|
||||
|
||||
// Expose methods for parent
|
||||
defineExpose({
|
||||
forceScrollToBottom
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="chat-messages"
|
||||
ref="messagesContainer"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<!-- Parallel mode streaming view -->
|
||||
<ParallelMessages
|
||||
v-if="isParallelMode && streaming"
|
||||
:room-id="roomId"
|
||||
mode="parallel"
|
||||
/>
|
||||
|
||||
<!-- Parallel mode history messages (card layout) -->
|
||||
<template v-else-if="isParallelMode">
|
||||
<div v-if="messagesLoading" class="loading-messages">
|
||||
<div class="spinner-small"></div>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else-if="messages.length === 0" class="chat-empty">
|
||||
<p>点击「开始」启动多 Agent 对话</p>
|
||||
</div>
|
||||
<div v-else class="parallel-history">
|
||||
<div v-for="(roundMsgs, rIdx) in groupedMessages" :key="rIdx" class="parallel-round">
|
||||
<div class="round-divider">
|
||||
<span class="round-divider-line"></span>
|
||||
<span class="round-divider-label">第 {{ roundMsgs[0].round_number || rIdx + 1 }} 轮</span>
|
||||
<span class="round-divider-line"></span>
|
||||
</div>
|
||||
<div class="parallel-grid">
|
||||
<div
|
||||
v-for="msg in roundMsgs"
|
||||
:key="msg.id"
|
||||
class="parallel-card completed"
|
||||
:style="{ borderColor: msg.sender_color }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="agent-avatar" :style="{ background: msg.sender_color || '#2563eb' }">
|
||||
{{ (msg.sender_name || '?').charAt(0) }}
|
||||
</span>
|
||||
<span class="agent-name" :style="{ color: msg.sender_color }">{{ msg.sender_name }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<MessageBubble :message="msg" :deletable="false" :compact="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Sequential mode -->
|
||||
<template v-else>
|
||||
<div v-if="messagesLoading" class="loading-messages">
|
||||
<div class="spinner-small"></div>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="messages.length === 0 && Object.keys(streamingMessages).length === 0"
|
||||
class="chat-empty"
|
||||
>
|
||||
<p>点击「开始」启动多 Agent 对话</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="(roundMsgs, rIdx) in groupedMessages" :key="rIdx">
|
||||
<div class="round-divider" v-if="rIdx > 0">
|
||||
<span class="round-divider-line"></span>
|
||||
<span class="round-divider-label">第 {{ roundMsgs[0].round_number }} 轮</span>
|
||||
<span class="round-divider-line"></span>
|
||||
</div>
|
||||
<div class="round-group">
|
||||
<div class="round-header" v-if="roundMsgs[0].round_number">
|
||||
<span class="round-header-icon">💬</span>
|
||||
<span class="round-header-text">第 {{ roundMsgs[0].round_number }} 轮对话</span>
|
||||
</div>
|
||||
<MessageBubble
|
||||
v-for="msg in roundMsgs"
|
||||
:key="msg.id"
|
||||
:message="msg"
|
||||
:deletable="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Streaming messages -->
|
||||
<template v-if="Object.keys(streamingMessages).length > 0">
|
||||
<div class="round-divider" v-if="messages.length > 0">
|
||||
<span class="round-divider-line"></span>
|
||||
<span class="round-divider-label">进行中...</span>
|
||||
<span class="round-divider-line"></span>
|
||||
</div>
|
||||
<div class="round-group">
|
||||
<MessageBubble
|
||||
v-for="(msg, idx) in streamingMessageList"
|
||||
:key="msg.id + '-' + idx"
|
||||
:message="msg"
|
||||
:deletable="false"
|
||||
class="streaming-message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.spinner-small {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid var(--border-light);
|
||||
border-top-color: var(--accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Round group styling */
|
||||
.round-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.round-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.round-header-icon {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.round-header-text {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.round-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 1.25rem 0;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.round-divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--border-light), transparent);
|
||||
}
|
||||
|
||||
.round-divider-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-tertiary);
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-light);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Parallel history messages */
|
||||
.parallel-history {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.parallel-round {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.parallel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.parallel-card {
|
||||
background: var(--bg-primary);
|
||||
border: 2px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.parallel-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.parallel-card .card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.parallel-card .agent-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.parallel-card .agent-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.parallel-card .card-body {
|
||||
padding: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Streaming message animation */
|
||||
.streaming-message {
|
||||
opacity: 0.85;
|
||||
animation: streamingPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes streamingPulse {
|
||||
0%, 100% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.streaming-message :deep(.avatar) {
|
||||
animation: avatarPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes avatarPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,694 @@
|
|||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { agentsAPI } from '../../utils/api.js'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
agentPool: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
rooms: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
room: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
canEditRoom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'selectRoom',
|
||||
'createRoom',
|
||||
'deleteRoom',
|
||||
'createAgent',
|
||||
'editAgent',
|
||||
'deleteAgent',
|
||||
'showAddToRoom'
|
||||
])
|
||||
|
||||
// Sidebar tab state
|
||||
const sidebarTab = ref('rooms') // 'agents' | 'rooms' | 'roomAgents'
|
||||
|
||||
// Status map
|
||||
const statusMap = {
|
||||
idle: { label: '就绪', class: 'status-idle' },
|
||||
running: { label: '进行中', class: 'status-running' },
|
||||
paused: { label: '已暂停', class: 'status-paused' },
|
||||
completed: { label: '已完成', class: 'status-completed' },
|
||||
error: { label: '错误', class: 'status-error' }
|
||||
}
|
||||
|
||||
// Computed
|
||||
const roomAgents = computed(() => props.room?.agents || [])
|
||||
const canEdit = computed(() => props.canEditRoom)
|
||||
|
||||
// Handlers
|
||||
function handleSelectRoom(id) {
|
||||
emit('selectRoom', id)
|
||||
}
|
||||
|
||||
function handleDeleteRoom(e, id) {
|
||||
e.stopPropagation()
|
||||
emit('deleteRoom', id)
|
||||
}
|
||||
|
||||
function handleCreateRoom() {
|
||||
emit('createRoom')
|
||||
}
|
||||
|
||||
function handleCreateAgent() {
|
||||
emit('createAgent')
|
||||
}
|
||||
|
||||
function handleEditAgent(agent) {
|
||||
emit('editAgent', agent)
|
||||
}
|
||||
|
||||
function handleDeleteAgent(agent) {
|
||||
emit('deleteAgent', agent)
|
||||
}
|
||||
|
||||
function handleShowAddToRoom() {
|
||||
emit('showAddToRoom')
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
emit('selectRoom', null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="rooms-sidebar">
|
||||
<!-- Tab bar -->
|
||||
<div class="sidebar-tabs">
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: sidebarTab === 'rooms' }"
|
||||
@click="sidebarTab = 'rooms'"
|
||||
>
|
||||
💬 聊天室
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: sidebarTab === 'agents' }"
|
||||
@click="sidebarTab = 'agents'"
|
||||
>
|
||||
🤖 Agent 池
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Agent Pool tab -->
|
||||
<div v-if="sidebarTab === 'agents'" class="sidebar-tab-content">
|
||||
<div class="sidebar-header">
|
||||
<button class="btn-new-conv" @click="handleCreateAgent">+ 新建 Agent</button>
|
||||
</div>
|
||||
<div v-if="loading" class="sidebar-loading">
|
||||
<div class="spinner-small"></div>
|
||||
</div>
|
||||
<div v-else-if="agentPool.length === 0" class="sidebar-empty">暂无 Agent</div>
|
||||
<div v-else class="pool-list">
|
||||
<div
|
||||
v-for="agent in agentPool"
|
||||
:key="agent.id"
|
||||
class="pool-item"
|
||||
@click="handleEditAgent(agent)"
|
||||
>
|
||||
<span class="agent-dot" :style="{ background: agent.color }">
|
||||
{{ agent.name.charAt(0) }}
|
||||
</span>
|
||||
<div class="agent-info">
|
||||
<span class="agent-name" :style="{ color: agent.color }">{{ agent.name }}</span>
|
||||
<span v-if="agent.role" class="agent-role">{{ agent.role }}</span>
|
||||
<span class="agent-model">{{ agent.model || 'default' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Room list tab -->
|
||||
<div v-if="sidebarTab === 'rooms'" class="sidebar-tab-content">
|
||||
<div class="sidebar-header">
|
||||
<button class="btn-new-conv" @click="handleCreateRoom">+ 新建聊天室</button>
|
||||
</div>
|
||||
<div v-if="loading" class="sidebar-loading">
|
||||
<div class="spinner-small"></div>
|
||||
</div>
|
||||
<div v-else-if="rooms.length === 0" class="sidebar-empty">暂无聊天室</div>
|
||||
<div v-else class="room-list">
|
||||
<div
|
||||
v-for="r in rooms"
|
||||
:key="r.id"
|
||||
class="room-item"
|
||||
:class="{ active: selectedId === r.id }"
|
||||
@click="handleSelectRoom(r.id)"
|
||||
>
|
||||
<div class="room-item-header">
|
||||
<span class="room-item-title">{{ r.title }}</span>
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="statusMap[r.status]?.class"
|
||||
:title="statusMap[r.status]?.label"
|
||||
></span>
|
||||
</div>
|
||||
<div class="room-item-meta">
|
||||
<span class="meta-text">
|
||||
<span
|
||||
v-for="a in (r.agents || []).slice(0, 4)"
|
||||
:key="a.id"
|
||||
class="mini-dot"
|
||||
:style="{ background: a.color }"
|
||||
:title="a.name"
|
||||
></span>
|
||||
{{ r.agents?.length || 0 }} Agents
|
||||
</span>
|
||||
<span class="meta-text">{{ r.current_round }}/{{ r.max_rounds }}</span>
|
||||
<div class="room-item-actions" @click.stop>
|
||||
<button @click="(e) => handleDeleteRoom(e, r.id)" class="btn-icon-sm" title="删除">
|
||||
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Room agents tab (shown when a room is selected) -->
|
||||
<div v-if="sidebarTab === 'roomAgents' && room" class="sidebar-tab-content">
|
||||
<div class="sidebar-header sidebar-header-row">
|
||||
<button class="btn-back" @click="handleBack">
|
||||
<svg width="18" height="18">
|
||||
<use href="#arrow-left-icon"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="sidebar-title">{{ room.title }}</span>
|
||||
<button
|
||||
v-if="canEdit"
|
||||
class="btn-add-agent-sm"
|
||||
@click="handleShowAddToRoom"
|
||||
title="添加 Agent"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div class="room-agent-list">
|
||||
<div
|
||||
v-for="agent in roomAgents"
|
||||
:key="agent.id"
|
||||
class="agent-row"
|
||||
:class="{ editing: false }"
|
||||
@click="$emit('editRoomAgent', agent)"
|
||||
>
|
||||
<span class="agent-dot" :style="{ background: agent.color }">
|
||||
{{ agent.name.charAt(0) }}
|
||||
</span>
|
||||
<div class="agent-info">
|
||||
<span class="agent-name" :style="{ color: agent.color }">{{ agent.name }}</span>
|
||||
<span v-if="agent.role" class="agent-role">{{ agent.role }}</span>
|
||||
<span class="agent-model">{{ agent.model || 'default' }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="canEdit"
|
||||
class="btn-del-agent"
|
||||
@click.stop="$emit('deleteRoomAgent', agent)"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="roomAgents.length === 0" class="no-agents">
|
||||
<p>暂无 Agent</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Room agent edit form (inline) -->
|
||||
<slot name="agentEditForm"></slot>
|
||||
|
||||
<!-- Room info -->
|
||||
<div class="sidebar-room-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">任务</span>
|
||||
<p class="info-value">{{ room.task }}</p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">轮次</span>
|
||||
<p class="info-value">{{ room.max_rounds }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Sidebar container */
|
||||
.rooms-sidebar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Tab bar */
|
||||
.sidebar-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.6rem 0.4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--accent-primary);
|
||||
border-bottom-color: var(--accent-primary);
|
||||
background: var(--accent-primary-light);
|
||||
}
|
||||
|
||||
/* Tab content */
|
||||
.sidebar-tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Scrollable lists */
|
||||
.pool-list,
|
||||
.room-list,
|
||||
.room-agent-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pool-list {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
|
||||
.room-agent-list {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.sidebar-header {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.sidebar-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
background: var(--accent-primary);
|
||||
border: 1px solid var(--accent-primary);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 0.35rem;
|
||||
margin-right: 0.5rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: var(--accent-primary-hover);
|
||||
border-color: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
.btn-back svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-add-agent-sm {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-add-agent-sm:hover {
|
||||
background: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
.btn-new-conv {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-new-conv:hover {
|
||||
background: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
.sidebar-loading,
|
||||
.sidebar-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Pool item */
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.pool-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-light);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Room item */
|
||||
.room-item {
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.room-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.room-item.active {
|
||||
background: var(--accent-primary-light);
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.room-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.room-item-title {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #22c55e;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-paused {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.room-item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.mini-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.room-item-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-icon-sm {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-icon-sm:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* Agent row */
|
||||
.agent-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.agent-row:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-light);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.agent-row:hover .agent-dot {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.agent-dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.agent-role {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.agent-model {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-del-agent {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
opacity: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.agent-row:hover .btn-del-agent {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-del-agent:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.no-agents {
|
||||
text-align: center;
|
||||
padding: 0.8rem 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.no-agents p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Sidebar room info */
|
||||
.sidebar-room-info {
|
||||
border-top: 1px solid var(--border-light);
|
||||
padding: 0.5rem 0.6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
margin: 0.1rem 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner-small {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid var(--border-light);
|
||||
border-top-color: var(--accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
room: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
streaming: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canEditRoom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isParallelMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
parallelStats: {
|
||||
type: Object,
|
||||
default: () => ({ total: 0, completed: 0, streaming: 0, error: 0 })
|
||||
},
|
||||
parallelAgentList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'start',
|
||||
'stop',
|
||||
'reset',
|
||||
'updateExecutionMode'
|
||||
])
|
||||
|
||||
// Status map
|
||||
const statusMap = {
|
||||
idle: { label: '就绪', class: 'status-idle' },
|
||||
running: { label: '进行中', class: 'status-running' },
|
||||
paused: { label: '已暂停', class: 'status-paused' },
|
||||
completed: { label: '已完成', class: 'status-completed' },
|
||||
error: { label: '错误', class: 'status-error' }
|
||||
}
|
||||
|
||||
// Computed
|
||||
const executionMode = computed(() => props.room?.execution_mode || 'sequential')
|
||||
|
||||
const modeLabel = computed(() => {
|
||||
const labels = {
|
||||
sequential: '📋 Sequential',
|
||||
parallel: '⚡ Parallel',
|
||||
review_loop: '🔄 Review Loop'
|
||||
}
|
||||
return labels[executionMode.value] || executionMode.value
|
||||
})
|
||||
|
||||
const modeClass = computed(() => {
|
||||
const classes = {
|
||||
sequential: 'sequential',
|
||||
parallel: 'parallel',
|
||||
review_loop: 'review-loop'
|
||||
}
|
||||
return classes[executionMode.value] || 'sequential'
|
||||
})
|
||||
|
||||
const showStart = computed(() => !props.streaming && props.room?.status !== 'running')
|
||||
const showStop = computed(() => props.streaming || props.room?.status === 'running')
|
||||
const showReset = computed(() => props.room?.status !== 'running' && (props.room?.message_count || 0) > 0)
|
||||
|
||||
// Handlers
|
||||
function handleStart() {
|
||||
emit('start')
|
||||
}
|
||||
|
||||
function handleStop() {
|
||||
emit('stop')
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
function handleExecutionModeChange(e) {
|
||||
emit('updateExecutionMode', e.target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="room-toolbar">
|
||||
<!-- Room info -->
|
||||
<div class="toolbar-info">
|
||||
<h3>{{ room?.title || '聊天室' }}</h3>
|
||||
<div class="toolbar-badges">
|
||||
<span
|
||||
v-if="room"
|
||||
class="status-badge"
|
||||
:class="statusMap[room.status]?.class"
|
||||
>
|
||||
{{ statusMap[room.status]?.label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="room?.current_round > 0"
|
||||
class="round-badge"
|
||||
>
|
||||
R{{ room.current_round }}/{{ room.max_rounds }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isParallelMode"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Execution mode selector -->
|
||||
<div class="mode-selector">
|
||||
<select
|
||||
v-if="room"
|
||||
:value="executionMode"
|
||||
@change="handleExecutionModeChange"
|
||||
class="mode-select"
|
||||
:disabled="!canEditRoom"
|
||||
>
|
||||
<option value="sequential">📋 Sequential</option>
|
||||
<option value="parallel">⚡ Parallel</option>
|
||||
<option value="review_loop">🔄 Review Loop</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Parallel status indicator -->
|
||||
<div
|
||||
v-if="isParallelMode && streaming && parallelAgentList.length > 0"
|
||||
class="parallel-status"
|
||||
>
|
||||
<div class="agent-dots">
|
||||
<span
|
||||
v-for="agent in parallelAgentList"
|
||||
:key="agent.id"
|
||||
class="status-dot-small"
|
||||
:class="agent.status"
|
||||
:style="{ background: agent.message?.sender_color || agent.color || '#2563eb' }"
|
||||
:title="agent.name + ': ' + agent.status"
|
||||
></span>
|
||||
</div>
|
||||
<span class="progress-text">
|
||||
{{ parallelStats.completed }}/{{ parallelStats.total }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Control buttons -->
|
||||
<div class="toolbar-actions">
|
||||
<!-- Start button -->
|
||||
<button
|
||||
v-if="showStart"
|
||||
class="btn-ctrl btn-start"
|
||||
@click="handleStart"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</svg>
|
||||
{{ room?.status === 'paused' ? '继续' : '开始' }}
|
||||
</button>
|
||||
|
||||
<!-- Stop button -->
|
||||
<button
|
||||
v-if="showStop"
|
||||
class="btn-ctrl btn-stop"
|
||||
@click="handleStop"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="6" y="4" width="4" height="16"></rect>
|
||||
<rect x="14" y="4" width="4" height="16"></rect>
|
||||
</svg>
|
||||
停止
|
||||
</button>
|
||||
|
||||
<!-- Reset button -->
|
||||
<button
|
||||
v-if="showReset"
|
||||
class="btn-ctrl btn-reset"
|
||||
@click="handleReset"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.room-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toolbar-info h3 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.toolbar-badges {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.round-badge {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.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.sequential {
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.mode-badge.review-loop {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Status colors */
|
||||
.status-idle {
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-paused {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Mode selector */
|
||||
.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:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Parallel status */
|
||||
.parallel-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.agent-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-dot-small {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-dot-small.streaming {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.status-dot-small.completed {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status-dot-small.error {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.status-dot-small.pending {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Toolbar actions */
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-ctrl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-start {
|
||||
background: #22c55e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-start:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.btn-stop {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-stop:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.btn-reset:hover {
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue