fix: 修复参数传递bug

This commit is contained in:
ViperEkura 2026-04-21 17:01:53 +08:00
parent feabfc8537
commit a31d751137
4 changed files with 836 additions and 63 deletions

View File

@ -40,10 +40,6 @@ const navItems = [
path: '/tools', path: '/tools',
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>` icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`
}, },
{
path: '/agents',
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"></path><path d="M8.5 8.5v.01"></path><path d="M16 15.5v.01"></path><path d="M12 12v.01"></path><path d="M11 17v.01"></path><path d="M7 14v.01"></path></svg>`
},
{ {
path: '/settings', path: '/settings',
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>` icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`

View File

@ -1,28 +1,108 @@
<template> <template>
<div class="room-view"> <div class="room-view">
<!-- 左侧房间列表 --> <!-- 左侧侧栏 -->
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-header"> <div class="sidebar-tabs">
<h2>聊天室</h2> <button :class="{ active: sidebarTab === 'rooms' }" @click="sidebarTab = 'rooms'">
<button class="btn-icon" @click="showCreateRoom = true" title="创建房间">+</button> 聊天室
</button>
<button :class="{ active: sidebarTab === 'agents' }" @click="sidebarTab = 'agents'">
Agent
</button>
</div> </div>
<div class="room-list"> <!-- 聊天室列表 -->
<div v-for="room in rooms" :key="room.id" <template v-if="sidebarTab === 'rooms'">
class="room-item" :class="{ active: currentRoom?.id === room.id }" <div class="sidebar-header">
@click="joinRoom(room)"> <button class="btn-icon" @click="showCreateRoom = true" title="创建房间">+</button>
<div class="room-icon">💬</div>
<div class="room-info">
<span class="room-name">{{ room.name }}</span>
<span class="room-desc">{{ room.description || '无描述' }}</span>
</div>
</div> </div>
</div> <div class="room-list">
<div v-for="room in rooms" :key="room.id"
class="room-item" :class="{ active: currentRoom?.id === room.id }"
@click="joinRoom(room)">
<div class="room-icon">💬</div>
<div class="room-info">
<span class="room-name">{{ room.name }}</span>
<span class="room-desc">{{ room.description || '无描述' }}</span>
</div>
</div>
<div v-if="!rooms.length" class="empty-sidebar">暂无房间</div>
</div>
</template>
<div class="sidebar-footer"> <!-- Agent 列表 -->
<router-link to="/agents" class="nav-link"> <template v-if="sidebarTab === 'agents'">
<span></span> 管理 Agent <div class="sidebar-header">
</router-link> <span class="sidebar-title">Agent 管理</span>
<button class="btn-icon" @click="openCreateAgent" title="创建 Agent">+</button>
</div>
<div class="agent-sidebar-list">
<div v-for="agent in allAgents" :key="agent.id" class="agent-sidebar-item"
:class="{ 'in-room': isAgentInRoom(agent.id) }">
<div class="agent-sidebar-avatar">{{ agent.name?.charAt(0).toUpperCase() }}</div>
<div class="agent-sidebar-info">
<span class="agent-sidebar-name">{{ agent.name }}</span>
<span class="agent-sidebar-meta">
{{ agent.auto_response ? '自动' : '@触发' }} · {{ agent.role }}
</span>
</div>
<div class="agent-sidebar-actions">
<button class="btn-icon-sm" @click="editAgent(agent)" title="编辑"></button>
<button class="btn-icon-sm btn-danger" @click="deleteAgent(agent.id)" title="删除">×</button>
</div>
</div>
<div v-if="!allAgents.length" class="empty-sidebar">暂无 Agent</div>
</div>
</template>
<!-- Agent 创建/编辑表单 -->
<div v-if="showAgentForm" class="agent-sidebar-form">
<div class="form-header">
<h3>{{ editingAgent ? '编辑 Agent' : '创建 Agent' }}</h3>
<button class="btn-close" @click="closeAgentForm">×</button>
</div>
<form @submit.prevent="saveAgent">
<div class="form-group">
<label>名称 *</label>
<input v-model="agentForm.name" type="text" required />
</div>
<div class="form-group">
<label>角色 *</label>
<input v-model="agentForm.role" type="text" required />
</div>
<div class="form-group">
<label>系统提示词 *</label>
<textarea v-model="agentForm.system_prompt" rows="3" required></textarea>
</div>
<div class="form-group">
<label>Provider</label>
<select v-model="agentForm.provider_id">
<option :value="null">默认</option>
<option v-for="p in providers" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<div class="form-group">
<label>模型</label>
<input v-model="agentForm.model" type="text" />
</div>
<div class="form-row">
<div class="form-group">
<label>优先级</label>
<input v-model.number="agentForm.priority" type="number" min="1" max="10" />
</div>
<div class="form-group">
<label>响应方式</label>
<select v-model="agentForm.auto_response">
<option :value="true">自动</option>
<option :value="false">@触发</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn-secondary" @click="closeAgentForm">取消</button>
<button type="submit" class="btn-primary">保存</button>
</div>
</form>
</div> </div>
</aside> </aside>
@ -36,7 +116,8 @@
<span class="agents-count">{{ roomAgents.length }} Agent</span> <span class="agents-count">{{ roomAgents.length }} Agent</span>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button class="btn-small" @click="showManageAgents = true">管理成员</button> <button v-if="currentRoom" class="btn-small" @click="openAddMembers">添加成员</button>
<button class="btn-small" @click="openRoomManage">管理房间</button>
</div> </div>
</header> </header>
@ -128,35 +209,93 @@
</div> </div>
</div> </div>
<!-- 管理 Agent Modal --> <!-- 管理房间 Modal -->
<div v-if="showManageAgents" class="modal-overlay" @click.self="showManageAgents = false"> <div v-if="showRoomManage" class="modal-overlay" @click.self="showRoomManage = false">
<div class="modal"> <div class="modal modal-wide">
<h2>管理聊天室成员</h2> <div class="modal-tabs">
<div class="agents-management"> <button :class="{ active: roomTab === 'list' }" @click="roomTab = 'list'">
<div class="section"> 房间列表
<h3>当前成员</h3> </button>
<div class="agent-tags"> <button :class="{ active: roomTab === 'edit' }" @click="openRoomForm">
<span v-for="agent in roomAgents" :key="agent.id" class="agent-tag"> {{ editingRoom ? '编辑房间' : '创建房间' }}
{{ agent.name }} </button>
<button @click="removeAgentFromRoom(agent.id)">×</button> </div>
</span>
<div v-if="!roomAgents.length" class="empty-hint">暂无成员</div> <!-- 房间列表 -->
</div> <div v-if="roomTab === 'list'" class="rooms-management">
</div> <div class="rooms-grid">
<div class="section"> <div v-for="room in rooms" :key="room.id"
<h3>添加成员</h3> class="room-card" :class="{ active: currentRoom?.id === room.id }">
<div class="agent-list-select"> <div class="room-card-header">
<div v-for="agent in availableAgents" :key="agent.id" <div class="room-icon">💬</div>
class="agent-option" @click="addAgentToRoom(agent.id)"> <div class="room-card-info">
<span class="agent-avatar-sm">{{ agent.name?.charAt(0) }}</span> <span class="room-card-name">{{ room.name }}</span>
<span>{{ agent.name }}</span> <span class="room-card-desc">{{ room.description || '无描述' }}</span>
<span class="role">{{ agent.role }}</span> </div>
</div>
<div class="room-card-actions">
<button class="btn-tiny" @click="joinRoom(room); showRoomManage = false">进入</button>
<button class="btn-tiny" @click="editRoom(room)">编辑</button>
<button class="btn-tiny btn-danger" @click="deleteRoom(room.id)">删除</button>
</div> </div>
</div> </div>
<div v-if="!rooms.length" class="empty-hint">暂无房间</div>
</div>
</div>
<!-- 房间表单 -->
<div v-if="roomTab === 'edit'" class="room-form-section">
<h3>{{ editingRoom ? '编辑房间' : '创建房间' }}</h3>
<form @submit.prevent="saveRoom">
<div class="form-group">
<label>房间名称 *</label>
<input v-model="roomForm.name" type="text" required />
</div>
<div class="form-group">
<label>描述</label>
<input v-model="roomForm.description" type="text" />
</div>
<div class="form-actions">
<button type="button" class="btn-secondary" @click="roomTab = 'list'">取消</button>
<button type="submit" class="btn-primary">保存</button>
</div>
</form>
</div>
<div class="form-actions">
<button class="btn-secondary" @click="showRoomManage = false">关闭</button>
</div>
</div>
</div>
<!-- 添加成员 Modal -->
<div v-if="showAddMembers" class="modal-overlay" @click.self="showAddMembers = false">
<div class="modal">
<h2>房间成员管理</h2>
<div class="section">
<h3>当前成员</h3>
<div class="agent-tags">
<span v-for="agent in roomAgents" :key="agent.id" class="agent-tag">
{{ agent.name }}
<button @click="removeAgentFromRoom(agent.id)">×</button>
</span>
<div v-if="!roomAgents.length" class="empty-hint">暂无成员</div>
</div>
</div>
<div class="section">
<h3>添加成员</h3>
<div class="agent-list-select">
<div v-for="agent in availableAgents" :key="agent.id"
class="agent-option" @click="addAgentToRoom(agent.id)">
<span class="agent-avatar-sm">{{ agent.name?.charAt(0) }}</span>
<span>{{ agent.name }}</span>
<span class="role">{{ agent.role }}</span>
</div>
<div v-if="!availableAgents.length" class="empty-hint">没有可添加的 Agent</div>
</div> </div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button class="btn-secondary" @click="showManageAgents = false">关闭</button> <button class="btn-secondary" @click="showAddMembers = false">关闭</button>
</div> </div>
</div> </div>
</div> </div>
@ -165,7 +304,7 @@
<script setup> <script setup>
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { roomsAPI, createRoomWS, agentsAPI } from '@/api' import { roomsAPI, createRoomWS, agentsAPI, providersAPI } from '@/api'
import { marked } from 'marked' import { marked } from 'marked'
const rooms = ref([]) const rooms = ref([])
@ -173,16 +312,35 @@ const currentRoom = ref(null)
const messages = ref([]) const messages = ref([])
const roomAgents = ref([]) const roomAgents = ref([])
const allAgents = ref([]) const allAgents = ref([])
const providers = ref([])
const typingAgents = ref(new Set()) const typingAgents = ref(new Set())
const streamingMessages = ref({}) const streamingMessages = ref({})
const sidebarTab = ref('rooms') // 'rooms' | 'agents'
const showCreateRoom = ref(false) const showCreateRoom = ref(false)
const showManageAgents = ref(false) const showAddMembers = ref(false)
const showRoomManage = ref(false)
const roomTab = ref('list') // 'list' | 'edit'
const editingRoom = ref(null)
const agentTab = ref('members') // 'members' | 'agents'
const showAgentForm = ref(false)
const editingAgent = ref(null)
const inputMessage = ref('') const inputMessage = ref('')
const messagesContainer = ref(null) const messagesContainer = ref(null)
let ws = null let ws = null
const newRoom = reactive({ name: '', description: '' }) const newRoom = reactive({ name: '', description: '' })
const roomForm = reactive({ name: '', description: '' })
const agentForm = reactive({
name: '',
role: '',
system_prompt: '',
provider_id: null,
model: '',
priority: 5,
auto_response: true
})
const availableAgents = computed(() => { const availableAgents = computed(() => {
const roomAgentIds = new Set(roomAgents.value.map(a => a.id)) const roomAgentIds = new Set(roomAgents.value.map(a => a.id))
@ -224,6 +382,14 @@ async function loadAllAgents() {
} }
} }
function isAgentInRoom(agentId) {
return roomAgents.value.some(a => a.id === agentId)
}
function openAddMembers() {
showAddMembers.value = true
}
async function createRoom() { async function createRoom() {
try { try {
const res = await roomsAPI.create(newRoom) const res = await roomsAPI.create(newRoom)
@ -237,6 +403,60 @@ async function createRoom() {
} }
} }
function openRoomManage() {
roomTab.value = 'list'
showRoomManage.value = true
}
function openRoomForm() {
editingRoom.value = null
Object.assign(roomForm, { name: '', description: '' })
roomTab.value = 'edit'
}
function editRoom(room) {
editingRoom.value = room
Object.assign(roomForm, { name: room.name, description: room.description || '' })
roomTab.value = 'edit'
}
async function saveRoom() {
try {
if (editingRoom.value) {
await roomsAPI.update(editingRoom.value.id, roomForm)
const idx = rooms.value.findIndex(r => r.id === editingRoom.value.id)
if (idx !== -1) {
rooms.value[idx] = { ...rooms.value[idx], ...roomForm }
}
if (currentRoom.value?.id === editingRoom.value.id) {
currentRoom.value = { ...currentRoom.value, ...roomForm }
}
} else {
const res = await roomsAPI.create(roomForm)
rooms.value.push(res.room)
}
roomTab.value = 'list'
} catch (e) {
console.error('Failed to save room:', e)
alert('保存失败')
}
}
async function deleteRoom(roomId) {
if (!confirm('确定删除此聊天室?')) return
try {
await roomsAPI.delete(roomId)
rooms.value = rooms.value.filter(r => r.id !== roomId)
if (currentRoom.value?.id === roomId) {
currentRoom.value = null
messages.value = []
}
} catch (e) {
console.error('Failed to delete room:', e)
alert('删除失败')
}
}
async function joinRoom(room) { async function joinRoom(room) {
if (ws) { if (ws) {
ws.close() ws.close()
@ -280,14 +500,27 @@ async function joinRoom(room) {
}, },
onStream: (event, data, agentId, agentName) => { onStream: (event, data, agentId, agentName) => {
if (event === 'process_step') { if (event === 'process_step') {
// data.step contains {id, index, type, content}
const step = data.step || data
if (!streamingMessages.value[agentId]) { if (!streamingMessages.value[agentId]) {
streamingMessages.value[agentId] = { agentName, content: '' } streamingMessages.value[agentId] = { agentName, content: '' }
} }
if (data.type === 'text') { if (step.type === 'text') {
streamingMessages.value[agentId].content = data.content streamingMessages.value[agentId].content = step.content
} }
} else if (event === 'done') { } else if (event === 'done') {
delete streamingMessages.value[agentId] // Save streaming message to messages list before removing
if (streamingMessages.value[agentId]) {
messages.value.push({
id: `msg-${Date.now()}`,
sender_type: 'agent',
sender_id: agentId,
sender_name: agentName,
content: streamingMessages.value[agentId].content,
created_at: new Date().toISOString()
})
delete streamingMessages.value[agentId]
}
typingAgents.value.delete(agentId) typingAgents.value.delete(agentId)
} }
}, },
@ -323,6 +556,69 @@ async function removeAgentFromRoom(agentId) {
} }
} }
async function loadProviders() {
try {
const res = await providersAPI.list()
if (res.success) {
providers.value = res.data?.providers || []
}
} catch (e) {
console.error('Failed to load providers:', e)
}
}
function openCreateAgent() {
editingAgent.value = null
// Provider
const defaultProvider = providers.value.find(p => p.is_default)
Object.assign(agentForm, { name: '', role: '', system_prompt: '', provider_id: defaultProvider?.id || null, model: '', priority: 5, auto_response: true })
showAgentForm.value = true
}
function editAgent(agent) {
editingAgent.value = agent
Object.assign(agentForm, {
name: agent.name,
role: agent.role,
system_prompt: agent.system_prompt,
provider_id: agent.provider_id,
model: agent.model || '',
priority: agent.priority || 5,
auto_response: agent.auto_response
})
showAgentForm.value = true
}
async function saveAgent() {
try {
if (editingAgent.value) {
await agentsAPI.update(editingAgent.value.id, agentForm)
} else {
await agentsAPI.create(agentForm)
}
await loadAllAgents()
closeAgentForm()
} catch (e) {
console.error('Failed to save agent:', e)
alert('保存失败')
}
}
async function deleteAgent(agentId) {
if (!confirm('确定删除此 Agent')) return
try {
await agentsAPI.delete(agentId)
await loadAllAgents()
} catch (e) {
console.error('Failed to delete agent:', e)
}
}
function closeAgentForm() {
showAgentForm.value = false
editingAgent.value = null
}
function scrollToBottom() { function scrollToBottom() {
nextTick(() => { nextTick(() => {
if (messagesContainer.value) { if (messagesContainer.value) {
@ -334,6 +630,7 @@ function scrollToBottom() {
onMounted(() => { onMounted(() => {
loadRooms() loadRooms()
loadAllAgents() loadAllAgents()
loadProviders()
}) })
onUnmounted(() => { onUnmounted(() => {
@ -350,6 +647,7 @@ onUnmounted(() => {
/* Sidebar */ /* Sidebar */
.sidebar { .sidebar {
position: relative;
width: 280px; width: 280px;
background: var(--card-bg, #fff); background: var(--card-bg, #fff);
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
@ -358,27 +656,52 @@ onUnmounted(() => {
} }
.sidebar-header { .sidebar-header {
padding: 20px; padding: 12px 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.sidebar-header h2 { .sidebar-title {
margin: 0; font-size: 14px;
font-size: 18px; font-weight: 500;
color: var(--text-secondary);
}
.sidebar-tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
}
.sidebar-tabs button {
flex: 1;
padding: 12px;
border: none;
background: transparent;
cursor: pointer;
font-size: 13px;
color: #666;
border-bottom: 2px solid transparent;
}
.sidebar-tabs button.active {
color: #667eea;
border-bottom-color: #667eea;
} }
.btn-icon { .btn-icon {
width: 32px; width: 28px;
height: 32px; height: 28px;
border-radius: 50%; border-radius: 50%;
border: none; border: none;
background: #667eea; background: #667eea;
color: white; color: white;
cursor: pointer; cursor: pointer;
font-size: 18px; font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
} }
.room-list { .room-list {
@ -424,6 +747,218 @@ onUnmounted(() => {
color: #888; color: #888;
} }
/* Agent sidebar */
.agent-sidebar-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.agent-sidebar-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
.agent-sidebar-item:hover {
background: var(--hover-bg, #f0f0f0);
}
.agent-sidebar-item.in-room {
background: #667eea10;
}
.agent-sidebar-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.agent-sidebar-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.agent-sidebar-name {
font-weight: 500;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-sidebar-meta {
font-size: 11px;
color: #888;
}
.agent-sidebar-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.agent-sidebar-item:hover .agent-sidebar-actions {
opacity: 1;
}
.btn-icon-sm {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid var(--border-color);
background: transparent;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon-sm:hover {
background: var(--hover-bg);
}
.btn-icon-sm.btn-danger {
color: #dc2626;
border-color: #dc2626;
}
.btn-icon-sm.btn-danger:hover {
background: #fef2f2;
}
/* Agent sidebar form */
.agent-sidebar-form {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--card-bg, #fff);
padding: 16px;
overflow-y: auto;
z-index: 10;
}
.agent-sidebar-form .form-group {
margin-bottom: 12px;
}
.agent-sidebar-form .form-group label {
display: block;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
color: #666;
}
.agent-sidebar-form .form-group input,
.agent-sidebar-form .form-group textarea,
.agent-sidebar-form .form-group select {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 13px;
box-sizing: border-box;
}
.agent-sidebar-form .form-group textarea {
resize: vertical;
min-height: 60px;
}
.agent-sidebar-form .form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.agent-sidebar-form .form-actions {
display: flex;
gap: 8px;
margin-top: 16px;
}
.agent-sidebar-form .form-actions .btn-secondary,
.agent-sidebar-form .form-actions .btn-primary {
flex: 1 !important;
padding: 10px 16px !important;
border-radius: 6px;
font-size: 13px !important;
border: none !important;
cursor: pointer;
margin-top: 0 !important;
min-width: 80px;
box-sizing: border-box;
}
.agent-sidebar-form .btn-secondary {
background: #e5e7eb;
color: #333;
}
.agent-sidebar-form .btn-primary {
background: #667eea;
color: white;
}
.agent-sidebar-form .btn-secondary:hover {
background: #d1d5db;
}
.agent-sidebar-form .btn-primary:hover {
background: #5a67d8;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.form-header h3 {
margin: 0;
font-size: 14px;
}
.btn-close {
width: 24px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
font-size: 18px;
color: #888;
}
.btn-close:hover {
color: #333;
}
.empty-sidebar {
padding: 20px;
text-align: center;
color: #888;
font-size: 13px;
}
.sidebar-footer { .sidebar-footer {
padding: 16px; padding: 16px;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
@ -782,4 +1317,224 @@ onUnmounted(() => {
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
} }
/* Tabs */
.modal-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 12px;
}
.modal-tabs button {
padding: 8px 16px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 6px;
color: #666;
font-size: 14px;
}
.modal-tabs button.active {
background: #667eea;
color: white;
}
/* Wide modal */
.modal-wide {
max-width: 800px;
}
/* Section header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.section-header h3 {
margin: 0;
}
/* Agent cards in grid */
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
max-height: 250px;
overflow-y: auto;
}
.agent-card-small {
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg);
}
.agent-card-small .agent-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.agent-card-small .agent-info {
display: flex;
flex-direction: column;
}
.agent-card-small .name {
font-weight: 500;
font-size: 14px;
}
.agent-card-small .role-badge {
font-size: 11px;
color: #888;
}
.agent-meta-row {
display: flex;
gap: 12px;
font-size: 12px;
color: #666;
margin-bottom: 8px;
}
.agent-actions-row {
display: flex;
gap: 8px;
}
.btn-tiny {
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
border: 1px solid var(--border-color);
background: transparent;
cursor: pointer;
}
.btn-tiny.btn-danger {
color: #dc2626;
border-color: #dc2626;
}
.btn-small.btn-primary {
background: #667eea;
color: white;
border: none;
}
/* Agent form section */
.agent-form-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
.agent-form-section h3 {
margin: 0 0 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
textarea {
width: 100%;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 8px;
box-sizing: border-box;
font-family: inherit;
resize: vertical;
}
select {
width: 100%;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 8px;
box-sizing: border-box;
background: white;
}
/* Header actions */
.header-actions {
display: flex;
gap: 8px;
}
/* Rooms management */
.rooms-management {
max-height: 400px;
overflow-y: auto;
}
.rooms-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.room-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg);
}
.room-card.active {
border-color: #667eea;
background: #667eea10;
}
.room-card-header {
display: flex;
align-items: center;
gap: 12px;
}
.room-icon {
font-size: 24px;
}
.room-card-info {
display: flex;
flex-direction: column;
}
.room-card-name {
font-weight: 500;
}
.room-card-desc {
font-size: 12px;
color: #888;
}
.room-card-actions {
display: flex;
gap: 6px;
}
.room-form-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border-color);
}
.room-form-section h3 {
margin: 0 0 16px;
}
</style> </style>

View File

@ -123,27 +123,48 @@ async def websocket_handler(websocket: WebSocket, room_id: str):
}) })
context = {"user_id": user_id, "username": user_name} context = {"user_id": user_id, "username": user_name}
logger.info(f"[ROOM_WS] Starting process_message, agents count: {len(agents)}")
event_count = 0
async for event in chat_room_service.process_message( async for event in chat_room_service.process_message(
room_id=room_id, user_message=content, user_id=user_id, room_id=room_id, user_message=content, user_id=user_id,
user_name=user_name, context=context user_name=user_name, context=context
): ):
event_count += 1
logger.info(f"[ROOM_WS] Received event {event_count}: {event.get('event')}")
if event.get("event") in ["process_step", "done", "error"]: if event.get("event") in ["process_step", "done", "error"]:
# Find agent_name from agents list
agent_name = None
for agent in agents:
if agent.agent_id == event.get("agent_id"):
agent_name = agent.name
break
await connection_manager.broadcast_to_room(room_id, { await connection_manager.broadcast_to_room(room_id, {
"event": event.get("event"), "event": event.get("event"),
"data": event.get("data", {}), "data": event.get("data", {}),
"agent_id": event.get("agent_id"), "agent_id": event.get("agent_id"),
"agent_name": event.get("agent_name") "agent_name": agent_name
}) })
if event.get("event") == "done": if event.get("event") == "done":
# Find agent_name from agents list
agent_name = None
for agent in agents:
if agent.agent_id == event.get("agent_id"):
agent_name = agent.name
break
chat_room_service.save_message( chat_room_service.save_message(
room_id=room_id, sender_type="agent", room_id=room_id, sender_type="agent",
sender_id=event.get("agent_id"), sender_id=event.get("agent_id"),
sender_name=event.get("agent_name"), sender_name=agent_name,
content=event.get("content", ""), content=event.get("data", {}).get("content", "") if isinstance(event.get("data"), dict) else "",
token_count=event.get("token_count", 0) token_count=event.get("data", {}).get("token_count", 0) if isinstance(event.get("data"), dict) else 0
) )
else:
logger.info(f"[ROOM_WS] Skipping event: {event.get('event')}")
logger.info(f"[ROOM_WS] process_message completed, total events: {event_count}")
for agent in agents: for agent in agents:
await connection_manager.broadcast_to_room(room_id, { await connection_manager.broadcast_to_room(room_id, {

View File

@ -369,6 +369,7 @@ class StreamService:
yield _sse_event("done", { yield _sse_event("done", {
"message_id": msg_id, "message_id": msg_id,
"content": ctx.full_content,
"token_count": actual_token_count, "token_count": actual_token_count,
"usage": total_usage "usage": total_usage
}) })