Refactor frontend to support N dynamic AI agents architecture

Update frontend components to work with the new N-agents system (3-50 agents)
instead of fixed 7 roles. Each agent now has a random name and can modify
individual sections of the document.

Key changes:
- CollaborativeInput: Support dynamic agent count 3-50 via dropdown
- CollaborativeSession: Complete rewrite with N-agents, real-time thinking,
  raised hand animation, automatic rounds, convergence logic, and stop button
- TimelinePanel: Updated to display dynamic agent names and updated progress calculation
- collaboration.js store: Fixed WebSocket message handlers to match new backend events

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Augustin ROUX 2025-10-18 23:06:38 +02:00
parent b566671ea4
commit 1a7e0d218e
4 changed files with 206 additions and 77 deletions

View File

@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useCollaborationStore } from '../stores/collaboration'
const emit = defineEmits(['session-created'])
@ -11,11 +11,12 @@ const contextFile = ref(null)
const agentCount = ref(7)
const isCreating = ref(false)
const agentOptions = [
{ value: 3, label: '3 agents (Quick)' },
{ value: 5, label: '5 agents (Balanced)' },
{ value: 7, label: '7 agents (Comprehensive)' }
]
const agentOptions = computed(() => {
return Array.from({ length: 48 }, (_, i) => ({
value: i + 3,
label: `${i + 3} agents`
}))
})
const handleFileSelect = (event) => {
const file = event.target.files?.[0]

View File

@ -21,43 +21,39 @@ const isRunningRound = ref(false)
const sessionStarted = ref(false)
const isAutoRunning = ref(false)
const autoRunTimeout = ref(null)
const currentWorkingAgent = ref(null)
const currentAgentThinking = ref('')
const isStopping = ref(false)
const currentSession = computed(() => collaborationStore.currentSession)
const currentDocument = computed(() => collaborationStore.currentDocument)
const agents = computed(() => currentSession.value?.agents || [])
const conversationHistory = computed(() => collaborationStore.conversationHistory)
const agentCount = computed(() => currentSession.value?.agentCount || 0)
// True convergence: ALL 7 agents must say no changes
// Convergence logic: consecutive rounds with no changes >= agent count
const hasConverged = computed(() => {
if (conversationHistory.value.length === 0) return false
const lastRound = conversationHistory.value[conversationHistory.value.length - 1]
// Check if last round has no changes from ANY agent
return !lastRound.agentsMadeChanges || (Array.isArray(lastRound.agentsMadeChanges) && lastRound.agentsMadeChanges.length === 0)
})
// Count of agents in current session
const agentCount = computed(() => agents.value?.length || 0)
// Check if all available agents have participated without changes
const allAgentsConverged = computed(() => {
if (conversationHistory.value.length === 0) return false
const lastRound = conversationHistory.value[conversationHistory.value.length - 1]
// All agents in this session must have zero changes
return hasConverged.value && agentCount.value > 0
if (conversationHistory.value.length < 1) return false
let consecutiveNoChanges = 0
for (let i = conversationHistory.value.length - 1; i >= 0; i--) {
const round = conversationHistory.value[i]
if (!round.agentsMadeChanges || round.agentsMadeChanges.length === 0) {
consecutiveNoChanges++
} else {
break
}
}
return consecutiveNoChanges >= agentCount.value
})
onMounted(async () => {
try {
// Get session details
await collaborationStore.getSession(props.sessionId)
// Connect WebSocket
ws.connect()
// Listen to WebSocket messages
const messageInterval = setInterval(() => {
if (ws.messages.value.length > 0) {
const message = ws.messages.value.pop()
const message = ws.messages.value.shift()
handleWebSocketMessage(message)
}
}, 100)
@ -67,7 +63,6 @@ onMounted(async () => {
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
})
// If session hasn't been started, start it
if (currentSession.value?.status === 'created') {
await startSession()
}
@ -79,35 +74,49 @@ onMounted(async () => {
const handleWebSocketMessage = (message) => {
if (message.type === 'initial_document_created') {
sessionStarted.value = true
collaborationStore.updateDocumentFromMessage(message)
// Start first auto-round after initial document
collaborationStore.currentDocument = message.content
currentWorkingAgent.value = null
currentAgentThinking.value = ''
scheduleNextRound(2000)
} else if (message.type === 'document_modified') {
collaborationStore.updateDocumentFromMessage(message)
collaborationStore.currentDocument = message.content
} else if (message.type === 'agent_working') {
currentWorkingAgent.value = message.agentName
currentAgentThinking.value = ''
} else if (message.type === 'agent_thinking') {
currentAgentThinking.value = message.thinking || ''
} else if (message.type === 'round_complete') {
isRunningRound.value = false
collaborationStore.updateDocumentFromMessage(message)
collaborationStore.conversationHistory.push({
roundNumber: message.roundNumber,
agentsMadeChanges: message.agentsMadeChanges,
timestamp: Date.now()
})
currentWorkingAgent.value = null
currentAgentThinking.value = ''
// Check convergence
if (hasConverged.value && allAgentsConverged.value) {
// All agents converged - auto-complete after delay
if (hasConverged.value) {
isAutoRunning.value = false
setTimeout(() => {
completeSession()
}, 2000)
} else {
// Schedule next round with delay
scheduleNextRound(1500)
}
} else if (message.type === 'session_error') {
console.error('Session error:', message.error)
isRunningRound.value = false
isAutoRunning.value = false
currentWorkingAgent.value = null
} else if (message.type === 'session_completed') {
currentWorkingAgent.value = null
isAutoRunning.value = false
}
}
const scheduleNextRound = (delay) => {
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
if (isStopping.value) return
isAutoRunning.value = true
autoRunTimeout.value = setTimeout(() => {
runNextRoundAuto()
@ -115,13 +124,13 @@ const scheduleNextRound = (delay) => {
}
const runNextRoundAuto = async () => {
if (isRunningRound.value || hasConverged.value) return
if (isRunningRound.value || hasConverged.value || isStopping.value) return
isRunningRound.value = true
try {
await collaborationStore.runRound(props.sessionId)
} catch (error) {
console.error('Error running auto round:', error)
console.error('Error running round:', error)
isRunningRound.value = false
isAutoRunning.value = false
}
@ -146,23 +155,35 @@ const completeSession = async () => {
}
}
const stopSession = async () => {
isStopping.value = true
isAutoRunning.value = false
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
await completeSession()
}
const downloadDocument = () => {
const format = currentSession.value?.documentFormat || 'md'
const extension = format === 'md' ? 'md' : 'txt'
collaborationStore.downloadDocument(`collaborative-document.${extension}`)
}
function formatAgentName(agent) {
if (!agent) return 'Unknown'
return agent.charAt(0).toUpperCase() + agent.slice(1)
}
</script>
<template>
<div class="collaborative-session">
<!-- Header with minimal controls -->
<!-- Header -->
<div class="session-header">
<div class="header-content">
<h1>Collaborative Design</h1>
<p class="session-meta">
<span>Session #{{ sessionId }}</span>
<span class="badge" :class="{ active: sessionStarted, converged: allAgentsConverged }">
{{ allAgentsConverged ? 'Converged' : sessionStarted ? 'Active' : 'Waiting' }}
<span class="badge" :class="{ active: sessionStarted, converged: hasConverged }">
{{ hasConverged ? 'Converged' : sessionStarted ? 'Active' : 'Waiting' }}
</span>
</p>
</div>
@ -177,6 +198,15 @@ const downloadDocument = () => {
Download
</button>
<button
v-if="isAutoRunning || isRunningRound"
@click="stopSession"
class="stop-btn"
title="Stop the session"
>
Stop
</button>
<div v-if="isAutoRunning || isRunningRound" class="auto-run-indicator">
<span class="pulse"></span>
Auto-running...
@ -184,13 +214,31 @@ const downloadDocument = () => {
</div>
</div>
<!-- Status message -->
<div v-if="allAgentsConverged" class="convergence-message">
<span class="checkmark">[OK]</span>
All {{ agentCount }} agents have reviewed and approved. Auto-completing...
<!-- Working Agent Display -->
<div v-if="currentWorkingAgent" class="working-agent-card">
<div class="working-agent-header">
<div class="raised-hand-animation">
<span class="hand"></span>
</div>
<div class="agent-name-display">
<h3>{{ formatAgentName(currentWorkingAgent) }}</h3>
<p class="agent-label">is reviewing...</p>
</div>
</div>
<div v-if="currentAgentThinking" class="agent-thinking">
<p class="thinking-label">Thinking:</p>
<p class="thinking-content">{{ currentAgentThinking }}</p>
</div>
</div>
<!-- Team agents display -->
<!-- Convergence Message -->
<div v-if="hasConverged" class="convergence-message">
<span class="checkmark">[OK]</span>
All {{ agentCount }} agents have reviewed and approved. Session complete!
</div>
<!-- Team Display -->
<div class="team-display">
<h3>Team</h3>
<div class="team-grid">
@ -200,7 +248,7 @@ const downloadDocument = () => {
</div>
</div>
<!-- Main Document Viewer -->
<!-- Main Document -->
<div class="document-section">
<div class="document-header">
<h2>Architecture Document</h2>
@ -220,6 +268,14 @@ const downloadDocument = () => {
50% { opacity: 0.5; }
}
@keyframes raise-hand {
0% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-10px) rotate(-5deg); }
50% { transform: translateY(-20px) rotate(5deg); }
75% { transform: translateY(-10px) rotate(-5deg); }
100% { transform: translateY(0) rotate(0deg); }
}
.collaborative-session {
width: 100%;
display: flex;
@ -311,6 +367,24 @@ const downloadDocument = () => {
cursor: not-allowed;
}
.stop-btn {
padding: 0.75rem 1.5rem;
background: rgba(244, 67, 54, 0.8);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.stop-btn:hover {
background: rgba(244, 67, 54, 0.95);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(244, 67, 54, 0.3);
}
.auto-run-indicator {
display: flex;
align-items: center;
@ -333,6 +407,70 @@ const downloadDocument = () => {
animation: pulse 1.5s infinite;
}
.working-agent-card {
padding: 1.5rem;
background: rgba(102, 126, 234, 0.1);
border: 2px solid rgba(102, 126, 234, 0.4);
border-radius: 16px;
backdrop-filter: blur(10px);
}
.working-agent-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.raised-hand-animation {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background: rgba(102, 126, 234, 0.2);
border-radius: 50%;
}
.hand {
font-size: 2rem;
animation: raise-hand 1s infinite;
}
.agent-name-display h3 {
margin: 0;
font-size: 1.3rem;
color: rgba(102, 126, 234, 0.95);
}
.agent-label {
margin: 0.25rem 0 0 0;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.6);
}
.agent-thinking {
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
}
.thinking-label {
margin: 0 0 0.5rem 0;
font-size: 0.85rem;
font-weight: 600;
color: rgba(102, 126, 234, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.thinking-content {
margin: 0;
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.8);
line-height: 1.5;
}
.convergence-message {
display: flex;
align-items: center;
@ -431,12 +569,3 @@ const downloadDocument = () => {
letter-spacing: 0.5px;
}
</style>
<script>
function formatAgentName(agent) {
return agent
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
</script>

View File

@ -6,28 +6,23 @@ const collaborationStore = useCollaborationStore()
const conversationHistory = computed(() => collaborationStore.conversationHistory)
const currentRound = computed(() => collaborationStore.currentRound)
const allAgents = ['lead_architect', 'backend_engineer', 'frontend_engineer', 'ui_designer', 'devops_engineer', 'product_manager', 'security_specialist']
const agents = computed(() => collaborationStore.currentSession?.agents || [])
const agentCount = computed(() => collaborationStore.currentSession?.agentCount || 0)
const getAgentStatus = (round) => {
if (!round.agentsMadeChanges) return []
return allAgents.map(agent => ({
name: formatAgentName(agent),
made_changes: round.agentsMadeChanges.includes(agent) || round.agentsMadeChanges.some(a => a.includes(agent))
if (!round.agentsMadeChanges) return agents.value.map(agent => ({ name: agent, made_changes: false }))
return agents.value.map(agent => ({
name: agent,
made_changes: round.agentsMadeChanges.includes(agent)
}))
}
const roundProgress = computed(() => {
if (conversationHistory.value.length === 0) return 0
return Math.round((conversationHistory.value.length / 10) * 100)
if (agentCount.value === 0 || conversationHistory.value.length === 0) return 0
// Estimate progress: assume ~10 rounds max before convergence
const maxRounds = Math.ceil(agentCount.value * 1.5)
return Math.min(Math.round((conversationHistory.value.length / maxRounds) * 100), 100)
})
function formatAgentName(agent) {
return agent
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
</script>
<template>
@ -66,7 +61,7 @@ function formatAgentName(agent) {
:title="agent.made_changes ? 'Made changes' : 'No changes'"
>
<span class="agent-dot" :class="{ changed: agent.made_changes }"></span>
<span class="agent-short">{{ agent.name.split(' ')[0].slice(0, 1) }}</span>
<span class="agent-name">{{ agent.name }}</span>
</div>
</div>
@ -228,9 +223,13 @@ function formatAgentName(agent) {
height: 5px;
}
.agent-short {
.agent-name {
font-weight: 600;
font-size: 0.65rem;
font-size: 0.7rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60px;
}
.changes-count {

View File

@ -143,14 +143,14 @@ export const useCollaborationStore = defineStore('collaboration', () => {
*/
function updateDocumentFromMessage(message) {
if (message.type === 'initial_document_created') {
currentDocument.value = message.document
currentDocument.value = message.content
currentRound.value = 1
} else if (message.type === 'document_modified') {
currentDocument.value = message.document
currentDocument.value = message.content
} else if (message.type === 'round_complete') {
conversationHistory.value.push({
roundNumber: message.roundNumber,
agentsMadeChanges: message.agentsWhoModified
agentsMadeChanges: message.agentsMadeChanges
})
}
}