Compare commits

..

2 Commits

Author SHA1 Message Date
c5f7806718 Fix: Make generateAgentResponse an async generator function
The generateAgentResponse function uses 'yield' to stream responses,
so it must be declared as an async generator function (async function*)
instead of a regular async function.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 23:07:32 +02:00
1a7e0d218e 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>
2025-10-18 23:06:38 +02:00
5 changed files with 207 additions and 78 deletions

View File

@ -98,7 +98,7 @@ async function* parseStreamResponse(reader) {
/** /**
* Generate agent response with streaming thoughts * Generate agent response with streaming thoughts
*/ */
export async function generateAgentResponse(agentName, prompt, currentDocument = '', onThought = null) { export async function* generateAgentResponse(agentName, prompt, currentDocument = '', onThought = null) {
const systemPrompt = getAgentPrompt(agentName) const systemPrompt = getAgentPrompt(agentName)
const messages = [ const messages = [

View File

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

View File

@ -21,43 +21,39 @@ const isRunningRound = ref(false)
const sessionStarted = ref(false) const sessionStarted = ref(false)
const isAutoRunning = ref(false) const isAutoRunning = ref(false)
const autoRunTimeout = ref(null) const autoRunTimeout = ref(null)
const currentWorkingAgent = ref(null)
const currentAgentThinking = ref('')
const isStopping = ref(false)
const currentSession = computed(() => collaborationStore.currentSession) const currentSession = computed(() => collaborationStore.currentSession)
const currentDocument = computed(() => collaborationStore.currentDocument) const currentDocument = computed(() => collaborationStore.currentDocument)
const agents = computed(() => currentSession.value?.agents || []) const agents = computed(() => currentSession.value?.agents || [])
const conversationHistory = computed(() => collaborationStore.conversationHistory) 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(() => { const hasConverged = computed(() => {
if (conversationHistory.value.length === 0) return false if (conversationHistory.value.length < 1) return false
const lastRound = conversationHistory.value[conversationHistory.value.length - 1] let consecutiveNoChanges = 0
// Check if last round has no changes from ANY agent for (let i = conversationHistory.value.length - 1; i >= 0; i--) {
return !lastRound.agentsMadeChanges || (Array.isArray(lastRound.agentsMadeChanges) && lastRound.agentsMadeChanges.length === 0) const round = conversationHistory.value[i]
}) if (!round.agentsMadeChanges || round.agentsMadeChanges.length === 0) {
consecutiveNoChanges++
// Count of agents in current session } else {
const agentCount = computed(() => agents.value?.length || 0) break
}
// Check if all available agents have participated without changes }
const allAgentsConverged = computed(() => { return consecutiveNoChanges >= agentCount.value
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
}) })
onMounted(async () => { onMounted(async () => {
try { try {
// Get session details
await collaborationStore.getSession(props.sessionId) await collaborationStore.getSession(props.sessionId)
// Connect WebSocket
ws.connect() ws.connect()
// Listen to WebSocket messages
const messageInterval = setInterval(() => { const messageInterval = setInterval(() => {
if (ws.messages.value.length > 0) { if (ws.messages.value.length > 0) {
const message = ws.messages.value.pop() const message = ws.messages.value.shift()
handleWebSocketMessage(message) handleWebSocketMessage(message)
} }
}, 100) }, 100)
@ -67,7 +63,6 @@ onMounted(async () => {
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value) if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
}) })
// If session hasn't been started, start it
if (currentSession.value?.status === 'created') { if (currentSession.value?.status === 'created') {
await startSession() await startSession()
} }
@ -79,35 +74,49 @@ onMounted(async () => {
const handleWebSocketMessage = (message) => { const handleWebSocketMessage = (message) => {
if (message.type === 'initial_document_created') { if (message.type === 'initial_document_created') {
sessionStarted.value = true sessionStarted.value = true
collaborationStore.updateDocumentFromMessage(message) collaborationStore.currentDocument = message.content
// Start first auto-round after initial document currentWorkingAgent.value = null
currentAgentThinking.value = ''
scheduleNextRound(2000) scheduleNextRound(2000)
} else if (message.type === 'document_modified') { } 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') { } else if (message.type === 'round_complete') {
isRunningRound.value = false 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) {
if (hasConverged.value && allAgentsConverged.value) {
// All agents converged - auto-complete after delay
isAutoRunning.value = false isAutoRunning.value = false
setTimeout(() => { setTimeout(() => {
completeSession() completeSession()
}, 2000) }, 2000)
} else { } else {
// Schedule next round with delay
scheduleNextRound(1500) scheduleNextRound(1500)
} }
} else if (message.type === 'session_error') { } else if (message.type === 'session_error') {
console.error('Session error:', message.error) console.error('Session error:', message.error)
isRunningRound.value = false isRunningRound.value = false
isAutoRunning.value = false isAutoRunning.value = false
currentWorkingAgent.value = null
} else if (message.type === 'session_completed') {
currentWorkingAgent.value = null
isAutoRunning.value = false
} }
} }
const scheduleNextRound = (delay) => { const scheduleNextRound = (delay) => {
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value) if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
if (isStopping.value) return
isAutoRunning.value = true isAutoRunning.value = true
autoRunTimeout.value = setTimeout(() => { autoRunTimeout.value = setTimeout(() => {
runNextRoundAuto() runNextRoundAuto()
@ -115,13 +124,13 @@ const scheduleNextRound = (delay) => {
} }
const runNextRoundAuto = async () => { const runNextRoundAuto = async () => {
if (isRunningRound.value || hasConverged.value) return if (isRunningRound.value || hasConverged.value || isStopping.value) return
isRunningRound.value = true isRunningRound.value = true
try { try {
await collaborationStore.runRound(props.sessionId) await collaborationStore.runRound(props.sessionId)
} catch (error) { } catch (error) {
console.error('Error running auto round:', error) console.error('Error running round:', error)
isRunningRound.value = false isRunningRound.value = false
isAutoRunning.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 downloadDocument = () => {
const format = currentSession.value?.documentFormat || 'md' const format = currentSession.value?.documentFormat || 'md'
const extension = format === 'md' ? 'md' : 'txt' const extension = format === 'md' ? 'md' : 'txt'
collaborationStore.downloadDocument(`collaborative-document.${extension}`) collaborationStore.downloadDocument(`collaborative-document.${extension}`)
} }
function formatAgentName(agent) {
if (!agent) return 'Unknown'
return agent.charAt(0).toUpperCase() + agent.slice(1)
}
</script> </script>
<template> <template>
<div class="collaborative-session"> <div class="collaborative-session">
<!-- Header with minimal controls --> <!-- Header -->
<div class="session-header"> <div class="session-header">
<div class="header-content"> <div class="header-content">
<h1>Collaborative Design</h1> <h1>Collaborative Design</h1>
<p class="session-meta"> <p class="session-meta">
<span>Session #{{ sessionId }}</span> <span>Session #{{ sessionId }}</span>
<span class="badge" :class="{ active: sessionStarted, converged: allAgentsConverged }"> <span class="badge" :class="{ active: sessionStarted, converged: hasConverged }">
{{ allAgentsConverged ? 'Converged' : sessionStarted ? 'Active' : 'Waiting' }} {{ hasConverged ? 'Converged' : sessionStarted ? 'Active' : 'Waiting' }}
</span> </span>
</p> </p>
</div> </div>
@ -177,6 +198,15 @@ const downloadDocument = () => {
Download Download
</button> </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"> <div v-if="isAutoRunning || isRunningRound" class="auto-run-indicator">
<span class="pulse"></span> <span class="pulse"></span>
Auto-running... Auto-running...
@ -184,13 +214,31 @@ const downloadDocument = () => {
</div> </div>
</div> </div>
<!-- Status message --> <!-- Working Agent Display -->
<div v-if="allAgentsConverged" class="convergence-message"> <div v-if="currentWorkingAgent" class="working-agent-card">
<span class="checkmark">[OK]</span> <div class="working-agent-header">
All {{ agentCount }} agents have reviewed and approved. Auto-completing... <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> </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"> <div class="team-display">
<h3>Team</h3> <h3>Team</h3>
<div class="team-grid"> <div class="team-grid">
@ -200,7 +248,7 @@ const downloadDocument = () => {
</div> </div>
</div> </div>
<!-- Main Document Viewer --> <!-- Main Document -->
<div class="document-section"> <div class="document-section">
<div class="document-header"> <div class="document-header">
<h2>Architecture Document</h2> <h2>Architecture Document</h2>
@ -220,6 +268,14 @@ const downloadDocument = () => {
50% { opacity: 0.5; } 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 { .collaborative-session {
width: 100%; width: 100%;
display: flex; display: flex;
@ -311,6 +367,24 @@ const downloadDocument = () => {
cursor: not-allowed; 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 { .auto-run-indicator {
display: flex; display: flex;
align-items: center; align-items: center;
@ -333,6 +407,70 @@ const downloadDocument = () => {
animation: pulse 1.5s infinite; 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 { .convergence-message {
display: flex; display: flex;
align-items: center; align-items: center;
@ -431,12 +569,3 @@ const downloadDocument = () => {
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
</style> </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 conversationHistory = computed(() => collaborationStore.conversationHistory)
const currentRound = computed(() => collaborationStore.currentRound) const currentRound = computed(() => collaborationStore.currentRound)
const agents = computed(() => collaborationStore.currentSession?.agents || [])
const allAgents = ['lead_architect', 'backend_engineer', 'frontend_engineer', 'ui_designer', 'devops_engineer', 'product_manager', 'security_specialist'] const agentCount = computed(() => collaborationStore.currentSession?.agentCount || 0)
const getAgentStatus = (round) => { const getAgentStatus = (round) => {
if (!round.agentsMadeChanges) return [] if (!round.agentsMadeChanges) return agents.value.map(agent => ({ name: agent, made_changes: false }))
return allAgents.map(agent => ({ return agents.value.map(agent => ({
name: formatAgentName(agent), name: agent,
made_changes: round.agentsMadeChanges.includes(agent) || round.agentsMadeChanges.some(a => a.includes(agent)) made_changes: round.agentsMadeChanges.includes(agent)
})) }))
} }
const roundProgress = computed(() => { const roundProgress = computed(() => {
if (conversationHistory.value.length === 0) return 0 if (agentCount.value === 0 || conversationHistory.value.length === 0) return 0
return Math.round((conversationHistory.value.length / 10) * 100) // 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> </script>
<template> <template>
@ -66,7 +61,7 @@ function formatAgentName(agent) {
:title="agent.made_changes ? 'Made changes' : 'No changes'" :title="agent.made_changes ? 'Made changes' : 'No changes'"
> >
<span class="agent-dot" :class="{ changed: agent.made_changes }"></span> <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>
</div> </div>
@ -228,9 +223,13 @@ function formatAgentName(agent) {
height: 5px; height: 5px;
} }
.agent-short { .agent-name {
font-weight: 600; font-weight: 600;
font-size: 0.65rem; font-size: 0.7rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60px;
} }
.changes-count { .changes-count {

View File

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