Compare commits

..

No commits in common. "31cd84186f316ebf538ae39c519b37420a302c81" and "610d4bb686e6153c68ef75e1eda1b65176f74abb" have entirely different histories.

11 changed files with 437 additions and 966 deletions

View File

@ -21,15 +21,15 @@ Aucune génération de code direct n'est effectuée : seules des **explications,
Reproduire une **table ronde d'IA spécialistes**, chaque agent IA représentant un rôle spécifique : Reproduire une **table ronde d'IA spécialistes**, chaque agent IA représentant un rôle spécifique :
- Lead Architect (Architecte logiciel) - Architecte logiciel
- Backend Engineer (Développeur backend) - Développeur backend
- Frontend Engineer (Développeur frontend) - Développeur frontend
- UI Designer (Designer UI/UX) - Designer UI/UX
- DevOps Engineer (Ingénieur DevOps) - Data engineer
- Product Manager (Chef de projet) - Chef de projet
- Security Specialist (Spécialiste sécurité) - Éventuellement d'autres rôles selon la complexité du prompt.
L'aspect le plus crucial de ces IA réside dans leur capacité à **collaborer itérativement** pour améliorer collectivement la spécification architecturale jusqu'à convergence naturelle (quand plus aucune amélioration n'est proposée). Cependant, l'aspect le plus crucial de ces IA réside dans leur capacité à **échanger, négocier, voire s'opposer** pour déterminer collectivement la meilleure approche à adopter.
--- ---
@ -96,17 +96,16 @@ graph TD
### Interface et visualisation ### Interface et visualisation
- **Saisie de prompt** décrivant le projet souhaité - **Saisie de prompt** décrivant le projet souhaité
- **Upload optionnel** de fichiers contexte (MD/TXT) - **Visualisation en temps réel** des échanges entre agents via WebSocket
- **Visualisation en temps réel** des modifications via WebSocket - **Affichage structuré** des propositions avec justifications et niveaux de confiance
- **Timeline d'évolution** du document avec historique complet - **Rendu automatique** de diagrammes Mermaid intégrés dans les réponses
- **Affichage interactif** du document Markdown final - **Indicateurs de progression** et statuts du débat
- **Indicateurs de progression** et statuts du session
### Architecture et stockage ### Architecture et stockage
- **API REST** pour gestion des sessions collaboratives - **API REST** pour gestion des débats et récupération des résultats
- **WebSocket** pour streaming temps réel des modifications d'agents - **WebSocket** pour streaming temps réel des réponses des agents
- **Base SQLite** pour persistance des sessions, versions et historique complet - **Base SQLite** pour persistance des débats et historique
- **Versioning intelligent** : Chaque modification crée une version avec métadonnées (agent, raison, round) - **Gestion d'erreurs** avec fallbacks et réponses de secours
--- ---

View File

@ -23,7 +23,7 @@ db.exec(`
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
initial_prompt TEXT NOT NULL, initial_prompt TEXT NOT NULL,
document_format TEXT DEFAULT 'md' CHECK(document_format IN ('md', 'txt')), document_format TEXT DEFAULT 'md' CHECK(document_format IN ('md', 'txt')),
status TEXT CHECK(status IN ('created', 'ongoing', 'completed', 'failed')) DEFAULT 'created', status TEXT CHECK(status IN ('ongoing', 'completed', 'failed')) DEFAULT 'ongoing',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP, completed_at TIMESTAMP,
final_document TEXT final_document TEXT

View File

@ -55,8 +55,8 @@ router.post('/:id/start', async (req, res) => {
return res.status(404).json({ error: 'Session not found' }); return res.status(404).json({ error: 'Session not found' });
} }
if (session.status !== 'created') { if (session.status !== 'ongoing') {
return res.status(400).json({ error: 'Session has already been started or is no longer available' }); return res.status(400).json({ error: 'Session is not in ongoing status' });
} }
// Start the session asynchronously // Start the session asynchronously

View File

@ -58,7 +58,7 @@ class CollaborativeOrchestrator {
const stmt = db.prepare( const stmt = db.prepare(
'INSERT INTO collaborative_sessions (initial_prompt, document_format, status) VALUES (?, ?, ?)' 'INSERT INTO collaborative_sessions (initial_prompt, document_format, status) VALUES (?, ?, ?)'
); );
const result = stmt.run(initialPrompt, documentFormat, 'created'); const result = stmt.run(initialPrompt, documentFormat, 'ongoing');
const sessionId = result.lastInsertRowid; const sessionId = result.lastInsertRowid;
// Select the agents to use // Select the agents to use
@ -145,10 +145,6 @@ class CollaborativeOrchestrator {
throw new Error('Active session not found'); throw new Error('Active session not found');
} }
// Update session status from 'created' to 'ongoing'
const updateStmt = db.prepare('UPDATE collaborative_sessions SET status = ? WHERE id = ?');
updateStmt.run('ongoing', sessionId);
activeSession.started = true; activeSession.started = true;
// Broadcast session start // Broadcast session start

View File

@ -3,7 +3,6 @@ import { ref } from 'vue'
import { useCollaborationStore } from './stores/collaboration' import { useCollaborationStore } from './stores/collaboration'
import CollaborativeInput from './components/CollaborativeInput.vue' import CollaborativeInput from './components/CollaborativeInput.vue'
import CollaborativeSession from './components/CollaborativeSession.vue' import CollaborativeSession from './components/CollaborativeSession.vue'
import TimelinePanel from './components/TimelinePanel.vue'
import NetworkStatus from './components/NetworkStatus.vue' import NetworkStatus from './components/NetworkStatus.vue'
const collaborationStore = useCollaborationStore() const collaborationStore = useCollaborationStore()
@ -26,28 +25,17 @@ function startNewCollaboration() {
<div class="app"> <div class="app">
<NetworkStatus /> <NetworkStatus />
<!-- Input Mode - Centered Full Screen --> <!-- Input Mode -->
<div v-if="!showSession" class="input-container"> <div v-if="!showSession">
<CollaborativeInput @session-created="handleCollaborationCreated" /> <CollaborativeInput @session-created="handleCollaborationCreated" />
</div> </div>
<!-- Session Mode - Two Column Layout --> <!-- Session Mode -->
<div v-else class="session-layout"> <div v-else>
<!-- Left Sidebar: Timeline --> <button @click="startNewCollaboration" class="new-session-btn">
<div class="timeline-sidebar"> New Session
<div class="sidebar-header">
<h2>Session Progress</h2>
<button @click="startNewCollaboration" class="new-btn-small" title="Start new session">
R
</button> </button>
</div>
<!-- Timeline Panel -->
<TimelinePanel />
</div>
<!-- Right Content: Session -->
<div class="main-content">
<CollaborativeSession <CollaborativeSession
v-if="currentSessionId" v-if="currentSessionId"
:session-id="currentSessionId" :session-id="currentSessionId"
@ -55,205 +43,54 @@ function startNewCollaboration() {
/> />
</div> </div>
</div> </div>
</div>
</template> </template>
<style> <style>
@keyframes gradient-shift { * {
0% { background-position: 0% 50%; } margin: 0;
50% { background-position: 100% 50%; } padding: 0;
100% { background-position: 0% 50%; } box-sizing: border-box;
} }
@keyframes float1 { body {
0%, 100% { transform: translate(0, 0) rotate(0deg); opacity: 0.03; } font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
25% { transform: translate(100px, -100px) rotate(90deg); opacity: 0.05; } background-color: #f5f7fa;
50% { transform: translate(-50px, 150px) rotate(180deg); opacity: 0.03; } color: #2c3e50;
75% { transform: translate(-100px, -50px) rotate(270deg); opacity: 0.04; }
} }
@keyframes float2 { #app {
0%, 100% { transform: translate(0, 0) rotate(0deg); opacity: 0.04; } min-height: 100vh;
25% { transform: translate(-120px, 80px) rotate(-90deg); opacity: 0.06; }
50% { transform: translate(80px, -120px) rotate(-180deg); opacity: 0.04; }
75% { transform: translate(120px, 100px) rotate(-270deg); opacity: 0.05; }
}
@keyframes float3 {
0%, 100% { transform: translate(0, 0) rotate(0deg); opacity: 0.03; }
25% { transform: translate(-80px, -80px) rotate(45deg); opacity: 0.05; }
50% { transform: translate(100px, 100px) rotate(135deg); opacity: 0.03; }
75% { transform: translate(-100px, 80px) rotate(225deg); opacity: 0.04; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
} }
</style> </style>
<style scoped> <style scoped>
.app { .app {
min-height: 100vh;
background: linear-gradient(-45deg, #0f0c29 0%, #302b63 25%, #24243e 50%, #302b63 75%, #0f0c29 100%);
background-size: 400% 400%;
animation: gradient-shift 15s ease infinite;
position: relative;
overflow: hidden;
}
.app::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 200%;
height: 200%;
background: radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.08) 0%, transparent 50%);
animation: float1 20s ease-in-out infinite;
pointer-events: none;
z-index: 0;
}
.app::after {
content: '';
position: fixed;
bottom: 0;
right: 0;
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(102, 126, 234, 0.12) 0%, transparent 70%);
animation: float2 25s ease-in-out infinite;
pointer-events: none;
z-index: 0;
border-radius: 50%;
}
.app > * {
position: relative;
z-index: 1;
}
.input-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh; min-height: 100vh;
padding: 2rem; padding: 2rem;
background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
} }
.session-layout { .new-session-btn {
display: grid; position: fixed;
grid-template-columns: 320px 1fr; top: 2rem;
min-height: 100vh; right: 2rem;
gap: 0; padding: 0.75rem 1.5rem;
background: rgba(102, 126, 234, 0.8);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2);
backdrop-filter: blur(10px);
z-index: 50;
} }
.timeline-sidebar { .new-session-btn:hover {
background: rgba(48, 43, 99, 0.4); background: rgba(102, 126, 234, 0.95);
border-right: 1px solid rgba(255, 255, 255, 0.1); transform: translateY(-2px);
backdrop-filter: blur(20px); box-shadow: 0 12px 40px rgba(102, 126, 234, 0.4);
padding: 1.5rem; border-color: rgba(255, 255, 255, 0.3);
overflow-y: auto;
max-height: 100vh;
box-shadow: inset -1px 0 0 rgba(102, 126, 234, 0.2);
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h2 {
font-size: 1.2rem;
margin: 0;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.new-btn-small {
width: 32px;
height: 32px;
padding: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
background: rgba(102, 126, 234, 0.6);
}
.new-btn-small:hover {
background: rgba(102, 126, 234, 0.9);
transform: rotate(20deg);
}
.timeline-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.main-content {
padding: 2rem;
overflow-y: auto;
max-height: 100vh;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.4);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.6);
}
/* Mobile responsive */
@media (max-width: 1024px) {
.session-layout {
grid-template-columns: 250px 1fr;
}
.timeline-sidebar {
padding: 1rem;
}
.main-content {
padding: 1rem;
}
}
@media (max-width: 768px) {
.session-layout {
grid-template-columns: 1fr;
}
.timeline-sidebar {
border-right: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
max-height: none;
}
.main-content {
max-height: none;
}
} }
</style> </style>

View File

@ -192,16 +192,7 @@ const removeFile = () => {
.collaborative-input { .collaborative-input {
min-height: 100vh; min-height: 100vh;
padding: 2rem; padding: 2rem;
background: linear-gradient( background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
-45deg,
#0f0c29 0%,
#302b63 25%,
#24243e 50%,
#302b63 75%,
#0f0c29 100%
);
background-size: 400% 400%;
animation: gradient-shift 15s ease infinite;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@ -209,53 +200,20 @@ const removeFile = () => {
.collaborative-input::before { .collaborative-input::before {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: -50%;
left: 0; right: -50%;
width: 200%; width: 200%;
height: 200%; height: 200%;
background: radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.1) 0%, transparent 50%), background: radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%);
radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.08) 0%, transparent 50%);
animation: float1 20s ease-in-out infinite;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
} }
@keyframes gradient-shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes float1 {
0%, 100% {
transform: translate(0, 0) rotate(0deg);
opacity: 0.03;
}
25% {
transform: translate(100px, -100px) rotate(90deg);
opacity: 0.05;
}
50% {
transform: translate(-50px, 150px) rotate(180deg);
opacity: 0.03;
}
75% {
transform: translate(-100px, -50px) rotate(270deg);
opacity: 0.04;
}
}
.container { .container {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
z-index: 2; z-index: 1;
} }
.header { .header {

View File

@ -3,7 +3,6 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useCollaborationStore } from '../stores/collaboration' import { useCollaborationStore } from '../stores/collaboration'
import { useWebSocket } from '../composables/useWebSocket' import { useWebSocket } from '../composables/useWebSocket'
import DocumentViewer from './DocumentViewer.vue' import DocumentViewer from './DocumentViewer.vue'
import TimelinePanel from './TimelinePanel.vue'
const props = defineProps({ const props = defineProps({
sessionId: { sessionId: {
@ -15,35 +14,21 @@ const props = defineProps({
const emit = defineEmits(['session-completed']) const emit = defineEmits(['session-completed'])
const collaborationStore = useCollaborationStore() const collaborationStore = useCollaborationStore()
const ws = useWebSocket(props.sessionId) const ws = useWebSocket(null, props.sessionId)
const isRunningRound = ref(false) const isRunningRound = ref(false)
const sessionStarted = ref(false) const sessionStarted = ref(false)
const isAutoRunning = ref(false) const showTimeline = ref(false)
const autoRunTimeout = ref(null) const selectedVersion = ref(null)
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)
// True convergence: ALL 7 agents must say no changes
const hasConverged = computed(() => { const hasConverged = computed(() => {
if (conversationHistory.value.length === 0) return false if (conversationHistory.value.length === 0) return false
const lastRound = conversationHistory.value[conversationHistory.value.length - 1] const lastRound = conversationHistory.value[conversationHistory.value.length - 1]
// Check if last round has no changes from ANY agent return !lastRound.agentsMadeChanges || lastRound.agentsMadeChanges.length === 0
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
}) })
onMounted(async () => { onMounted(async () => {
@ -62,10 +47,7 @@ onMounted(async () => {
} }
}, 100) }, 100)
onUnmounted(() => { onUnmounted(() => clearInterval(messageInterval))
clearInterval(messageInterval)
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
})
// If session hasn't been started, start it // If session hasn't been started, start it
if (currentSession.value?.status === 'created') { if (currentSession.value?.status === 'created') {
@ -80,50 +62,19 @@ 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.updateDocumentFromMessage(message)
// Start first auto-round after initial document
scheduleNextRound(2000)
} else if (message.type === 'document_modified') { } else if (message.type === 'document_modified') {
collaborationStore.updateDocumentFromMessage(message) collaborationStore.updateDocumentFromMessage(message)
} else if (message.type === 'round_complete') { } else if (message.type === 'round_complete') {
isRunningRound.value = false isRunningRound.value = false
collaborationStore.updateDocumentFromMessage(message) collaborationStore.updateDocumentFromMessage(message)
if (message.hasConverged) {
// Check convergence // Auto-complete on convergence
if (hasConverged.value && allAgentsConverged.value) {
// All agents converged - auto-complete after delay
isAutoRunning.value = false
setTimeout(() => { setTimeout(() => {
completeSession() completeSession()
}, 2000) }, 2000)
} else {
// Schedule next round with delay
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
isAutoRunning.value = false
}
}
const scheduleNextRound = (delay) => {
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
isAutoRunning.value = true
autoRunTimeout.value = setTimeout(() => {
runNextRoundAuto()
}, delay)
}
const runNextRoundAuto = async () => {
if (isRunningRound.value || hasConverged.value) return
isRunningRound.value = true
try {
await collaborationStore.runRound(props.sessionId)
} catch (error) {
console.error('Error running auto round:', error)
isRunningRound.value = false
isAutoRunning.value = false
} }
} }
@ -135,10 +86,20 @@ const startSession = async () => {
} }
} }
const runNextRound = async () => {
if (isRunningRound.value || hasConverged.value) return
isRunningRound.value = true
try {
await collaborationStore.runRound(props.sessionId)
} catch (error) {
console.error('Error running round:', error)
isRunningRound.value = false
}
}
const completeSession = async () => { const completeSession = async () => {
try { try {
isAutoRunning.value = false
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
await collaborationStore.completeSession(props.sessionId) await collaborationStore.completeSession(props.sessionId)
emit('session-completed') emit('session-completed')
} catch (error) { } catch (error) {
@ -151,287 +112,113 @@ const downloadDocument = () => {
const extension = format === 'md' ? 'md' : 'txt' const extension = format === 'md' ? 'md' : 'txt'
collaborationStore.downloadDocument(`collaborative-document.${extension}`) collaborationStore.downloadDocument(`collaborative-document.${extension}`)
} }
const viewVersion = (versionNumber) => {
selectedVersion.value = selectedVersion.value === versionNumber ? null : versionNumber
}
</script> </script>
<template> <template>
<div class="collaborative-session"> <div class="collaborative-session">
<!-- Header with minimal controls -->
<div class="session-header"> <div class="session-header">
<div class="header-content"> <div class="header-content">
<h1>Collaborative Design</h1> <h1>Design Session</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 }">
{{ allAgentsConverged ? 'Converged' : sessionStarted ? 'Active' : 'Waiting' }} {{ sessionStarted ? 'Active' : 'Waiting' }}
</span> </span>
</p> </p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button
@click="runNextRound"
:disabled="!sessionStarted || isRunningRound || hasConverged"
class="btn btn-primary"
>
{{ isRunningRound ? 'Round in Progress...' : 'Next Review Round' }}
</button>
<button
@click="completeSession"
:disabled="!sessionStarted"
class="btn btn-secondary"
>
Complete Session
</button>
<button <button
@click="downloadDocument" @click="downloadDocument"
:disabled="!currentDocument" :disabled="!currentDocument"
class="download-btn" class="btn btn-outline"
title="Download the document"
> >
Download Download
</button> </button>
<div v-if="isAutoRunning || isRunningRound" class="auto-run-indicator"> <button
<span class="pulse"></span> @click="showTimeline = !showTimeline"
Auto-running... class="btn btn-outline"
</div> >
Timeline
</button>
</div> </div>
</div> </div>
<!-- Status message --> <!-- Status Message -->
<div v-if="allAgentsConverged" class="convergence-message"> <div v-if="hasConverged" class="convergence-message">
<span class="checkmark">[OK]</span> Convergence reached. All agents satisfied.
All {{ agentCount }} agents have reviewed and approved. Auto-completing...
</div> </div>
<!-- Team agents display --> <!-- Agent List -->
<div class="team-display"> <div class="agents-section">
<h3>Team</h3> <h3>Team ({{ agents.length }} agents)</h3>
<div class="team-grid"> <div class="agents-grid">
<div v-for="agent in agents" :key="agent" class="team-member"> <div v-for="agent in agents" :key="agent" class="agent-badge">
{{ formatAgentName(agent) }} {{ formatAgentName(agent) }}
</div> </div>
</div> </div>
</div> </div>
<!-- Main Document Viewer --> <!-- Timeline View -->
<div class="document-section"> <div v-if="showTimeline" class="timeline-section">
<div class="document-header"> <h3>Modification Timeline</h3>
<h2>Architecture Document</h2> <div class="timeline">
<span v-if="currentDocument" class="doc-status">Live</span> <div v-for="(round, index) in conversationHistory" :key="index" class="timeline-item">
<div class="timeline-marker">
{{ index + 1 }}
</div> </div>
<div class="timeline-content">
<div class="round-title">Round {{ round.roundNumber }}</div>
<div v-if="Array.isArray(round.agentsMadeChanges) && round.agentsMadeChanges.length > 0" class="agents-modified">
Modified by: {{ round.agentsMadeChanges.join(', ') }}
</div>
<div v-else class="no-changes">
No changes
</div>
</div>
</div>
</div>
</div>
<!-- Document Viewer -->
<div class="document-section">
<DocumentViewer <DocumentViewer
:document="currentDocument || 'Waiting for initial document...'" :document="currentDocument"
:format="currentSession?.documentFormat || 'md'" :format="currentSession?.documentFormat || 'md'"
/> />
</div> </div>
<!-- Round Information -->
<div class="round-info">
<p>Current Round: <strong>{{ collaborationStore.currentRound }}</strong></p>
<p v-if="conversationHistory.length > 0">
Last Round: {{ conversationHistory.length }} agents reviewed
</p>
</div>
</div> </div>
</template> </template>
<style scoped>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.collaborative-session {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.session-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.1);
}
.header-content h1 {
margin: 0;
font-size: 1.8rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.session-meta {
margin-top: 0.5rem;
display: flex;
gap: 1rem;
align-items: center;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.7);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.9rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.badge.active {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.4);
color: rgba(102, 126, 234, 0.95);
}
.badge.converged {
background: rgba(76, 175, 80, 0.2);
border-color: rgba(76, 175, 80, 0.4);
color: rgba(76, 175, 80, 0.95);
}
.header-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.download-btn {
padding: 0.75rem 1.5rem;
background: rgba(102, 126, 234, 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);
}
.download-btn:hover:not(:disabled) {
background: rgba(102, 126, 234, 0.95);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
}
.download-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.auto-run-indicator {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.5rem 1rem;
background: rgba(76, 175, 80, 0.15);
border: 1px solid rgba(76, 175, 80, 0.3);
border-radius: 10px;
font-size: 0.85rem;
color: rgba(76, 175, 80, 0.9);
font-weight: 500;
}
.pulse {
display: inline-block;
width: 6px;
height: 6px;
background: rgba(76, 175, 80, 0.8);
border-radius: 50%;
animation: pulse 1.5s infinite;
}
.convergence-message {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.2rem;
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
border-radius: 12px;
color: rgba(76, 175, 80, 0.95);
font-weight: 500;
backdrop-filter: blur(10px);
}
.checkmark {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: rgba(76, 175, 80, 0.3);
border-radius: 50%;
font-weight: 700;
font-size: 0.9rem;
}
.team-display {
padding: 1rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
}
.team-display h3 {
margin: 0 0 0.75rem 0;
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.8);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.6rem;
}
.team-member {
padding: 0.6rem 0.9rem;
background: rgba(102, 126, 234, 0.12);
border: 1px solid rgba(102, 126, 234, 0.25);
border-radius: 8px;
font-size: 0.85rem;
color: rgba(102, 126, 234, 0.95);
font-weight: 500;
text-align: center;
transition: all 0.3s ease;
}
.team-member:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.4);
}
.document-section {
flex: 1;
display: flex;
flex-direction: column;
}
.document-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.document-header h2 {
margin: 0;
font-size: 1.2rem;
color: white;
}
.doc-status {
display: inline-block;
padding: 0.3rem 0.7rem;
background: rgba(76, 175, 80, 0.2);
border: 1px solid rgba(76, 175, 80, 0.3);
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
color: rgba(76, 175, 80, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
}
</style>
<script> <script>
function formatAgentName(agent) { function formatAgentName(agent) {
return agent return agent
@ -440,3 +227,227 @@ function formatAgentName(agent) {
.join(' ') .join(' ')
} }
</script> </script>
<style scoped>
.collaborative-session {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.session-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 2rem;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 20px;
color: white;
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2);
}
.header-content h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
}
.session-meta {
margin-top: 0.5rem;
opacity: 0.9;
display: flex;
gap: 1rem;
}
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(255, 255, 255, 0.2);
border-radius: 20px;
font-size: 0.875rem;
}
.badge.active {
background: rgba(76, 175, 80, 0.4);
}
.header-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: flex-end;
}
.btn {
padding: 0.75rem 1.25rem;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
font-size: 0.95rem;
}
.btn-primary {
background: rgba(255, 255, 255, 0.95);
color: #667eea;
border: none;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 255, 255, 0.3);
}
.btn-secondary,
.btn-outline {
background: rgba(255, 255, 255, 0.15);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(5px);
}
.btn-secondary:hover:not(:disabled),
.btn-outline:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.5);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.convergence-message {
padding: 1rem;
background: rgba(76, 175, 80, 0.15);
border: 1px solid rgba(76, 175, 80, 0.4);
border-radius: 12px;
color: rgba(76, 175, 80, 0.9);
margin-bottom: 1.5rem;
text-align: center;
font-weight: 600;
backdrop-filter: blur(5px);
}
.agents-section {
margin-bottom: 2rem;
}
.agents-section h3 {
margin-bottom: 1rem;
color: white;
}
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.agent-badge {
padding: 0.75rem;
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 10px;
text-align: center;
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(5px);
}
.timeline-section {
margin-bottom: 2rem;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
backdrop-filter: blur(10px);
}
.timeline-section h3 {
margin-top: 0;
color: white;
}
.timeline {
display: flex;
flex-direction: column;
gap: 1rem;
}
.timeline-item {
display: flex;
gap: 1rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border-left: 3px solid rgba(102, 126, 234, 0.6);
backdrop-filter: blur(5px);
}
.timeline-marker {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
background: rgba(102, 126, 234, 0.6);
color: white;
border-radius: 50%;
flex-shrink: 0;
font-weight: 700;
}
.timeline-content {
flex: 1;
}
.round-title {
font-weight: 600;
color: white;
margin-bottom: 0.25rem;
}
.agents-modified {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.7);
}
.no-changes {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.5);
font-style: italic;
}
.document-section {
margin: 2rem 0;
}
.round-info {
padding: 1rem;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
text-align: center;
color: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
}
@media (max-width: 768px) {
.session-header {
flex-direction: column;
gap: 1rem;
}
.header-actions {
justify-content: flex-start;
}
.agents-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
</style>

View File

@ -64,12 +64,12 @@ function getStatusColor() {
function getStatusEmoji() { function getStatusEmoji() {
switch (quality.value) { switch (quality.value) {
case 'excellent': return 'O' case 'excellent': return ''
case 'good': return 'O' case 'good': return ''
case 'fair': return 'O' case 'fair': return ''
case 'poor': return 'O' case 'poor': return ''
case 'offline': return 'O' case 'offline': return ''
default: return 'O' default: return ''
} }
} }
</script> </script>

View File

@ -1,275 +0,0 @@
<script setup>
import { computed } from 'vue'
import { useCollaborationStore } from '../stores/collaboration'
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 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))
}))
}
const roundProgress = computed(() => {
if (conversationHistory.value.length === 0) return 0
return Math.round((conversationHistory.value.length / 10) * 100)
})
function formatAgentName(agent) {
return agent
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
</script>
<template>
<div class="timeline-panel">
<!-- Progress Bar -->
<div class="progress-section">
<div class="progress-header">
<span class="progress-label">Session Progress</span>
<span class="progress-percent">{{ roundProgress }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: roundProgress + '%' }"></div>
</div>
<div class="round-counter">Round {{ currentRound }}</div>
</div>
<!-- Timeline -->
<div class="timeline-list">
<div
v-for="(round, index) in conversationHistory"
:key="index"
class="timeline-entry"
>
<div class="entry-header">
<div class="round-number">{{ index + 1 }}</div>
<div class="round-label">Round {{ round.roundNumber }}</div>
</div>
<!-- Agent status for this round -->
<div class="agent-list">
<div
v-for="agent in getAgentStatus(round)"
:key="agent.name"
class="agent-item"
:class="{ active: agent.made_changes }"
: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>
</div>
</div>
<!-- Changes summary -->
<div v-if="round.agentsMadeChanges && round.agentsMadeChanges.length > 0" class="changes-count">
{{ round.agentsMadeChanges.length }} agent{{ round.agentsMadeChanges.length !== 1 ? 's' : '' }} modified
</div>
<div v-else class="no-changes-text">No changes</div>
</div>
<!-- Empty state -->
<div v-if="conversationHistory.length === 0" class="empty-state">
<p>Waiting for first round...</p>
</div>
</div>
</div>
</template>
<style scoped>
.timeline-panel {
display: flex;
flex-direction: column;
gap: 1.5rem;
height: 100%;
}
.progress-section {
padding: 1rem;
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 12px;
backdrop-filter: blur(10px);
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
font-size: 0.875rem;
}
.progress-label {
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
}
.progress-percent {
color: var(--primary);
font-weight: 600;
}
.progress-bar {
height: 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 3px;
transition: width 0.3s ease;
}
.round-counter {
text-align: center;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
}
.timeline-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
flex: 1;
overflow-y: auto;
}
.timeline-entry {
padding: 0.875rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 10px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.timeline-entry:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(102, 126, 234, 0.4);
}
.entry-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.round-number {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: rgba(102, 126, 234, 0.5);
border-radius: 50%;
font-size: 0.75rem;
font-weight: 700;
color: white;
}
.round-label {
font-size: 0.875rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.agent-list {
display: flex;
gap: 0.4rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.agent-item {
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.5);
cursor: help;
transition: all 0.2s ease;
}
.agent-item.active {
background: rgba(76, 175, 80, 0.2);
color: rgba(76, 175, 80, 0.9);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.agent-dot {
width: 4px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.agent-dot.changed {
background: rgba(76, 175, 80, 0.8);
width: 5px;
height: 5px;
}
.agent-short {
font-weight: 600;
font-size: 0.65rem;
}
.changes-count {
font-size: 0.75rem;
color: rgba(76, 175, 80, 0.8);
font-weight: 500;
}
.no-changes-text {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.4);
font-style: italic;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: rgba(255, 255, 255, 0.4);
text-align: center;
font-size: 0.875rem;
}
/* Scrollbar */
.timeline-list::-webkit-scrollbar {
width: 6px;
}
.timeline-list::-webkit-scrollbar-track {
background: transparent;
}
.timeline-list::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.3);
border-radius: 3px;
}
.timeline-list::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.5);
}
</style>

View File

@ -1,6 +1,6 @@
import { ref, onUnmounted } from 'vue' import { ref, onUnmounted } from 'vue'
export function useWebSocket(sessionId = null) { export function useWebSocket(debateId = null, sessionId = null) {
const ws = ref(null) const ws = ref(null)
const connected = ref(false) const connected = ref(false)
const messages = ref([]) const messages = ref([])
@ -9,7 +9,9 @@ export function useWebSocket(sessionId = null) {
function connect() { function connect() {
let url = WS_URL let url = WS_URL
if (sessionId) { if (debateId) {
url += `?debateId=${debateId}`
} else if (sessionId) {
url += `?sessionId=${sessionId}` url += `?sessionId=${sessionId}`
} }

View File

@ -1,136 +1,79 @@
:root { :root {
--primary: #667eea; font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
--primary-dark: #5568d3; line-height: 1.5;
--secondary: #764ba2;
--accent: #f093fb;
--dark-bg: #0f0c29;
--mid-bg: #302b63;
--light-bg: #24243e;
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.7);
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
font-weight: 400; font-weight: 400;
color-scheme: dark;
color: var(--text-primary); color-scheme: light dark;
background-color: var(--dark-bg); color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
* { a {
margin: 0; font-weight: 500;
padding: 0; color: #646cff;
box-sizing: border-box; text-decoration: inherit;
}
a:hover {
color: #535bf2;
} }
body { body {
margin: 0; margin: 0;
display: flex;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
} }
a { h1 {
color: var(--primary); font-size: 3.2em;
text-decoration: none; line-height: 1.1;
transition: all 0.3s ease;
} }
a:hover {
color: var(--accent);
text-decoration: underline;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.2;
}
h1 { font-size: 2.5em; }
h2 { font-size: 2em; }
h3 { font-size: 1.5em; }
button { button {
border-radius: 12px; border-radius: 8px;
border: 1px solid var(--glass-border); border: 1px solid transparent;
padding: 0.75em 1.5em; padding: 0.6em 1.2em;
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 500;
font-family: inherit; font-family: inherit;
background: rgba(102, 126, 234, 0.8); background-color: #1a1a1a;
color: var(--text-primary);
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: border-color 0.25s;
backdrop-filter: blur(10px); }
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.1); button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
} }
button:hover:not(:disabled) { .card {
background: rgba(102, 126, 234, 0.95); padding: 2em;
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(102, 126, 234, 0.3);
border-color: var(--glass-border);
}
button:active:not(:disabled) {
transform: translateY(0);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button.secondary {
background: rgba(118, 75, 162, 0.6);
}
button.secondary:hover:not(:disabled) {
background: rgba(118, 75, 162, 0.8);
}
button.outline {
background: transparent;
border: 1px solid var(--primary);
color: var(--primary);
}
button.outline:hover:not(:disabled) {
background: rgba(102, 126, 234, 0.2);
border-color: var(--accent);
color: var(--accent);
}
input, textarea, select {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
color: var(--text-primary);
padding: 0.75em 1em;
border-radius: 8px;
font-family: inherit;
transition: all 0.3s ease;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);
background: rgba(255, 255, 255, 0.08);
} }
#app { #app {
min-height: 100vh; max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-color-scheme: light) {
*, *::before, *::after { :root {
animation-duration: 0.01ms !important; color: #213547;
animation-iteration-count: 1 !important; background-color: #ffffff;
transition-duration: 0.01ms !important; }
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
} }
} }