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