Refactor: Remove classic debate mode, simplify to collaborative design only

- Delete CHANGELOG.md (unnecessary documentation)
- Remove classic debate mode and all related components
- Simplify App.vue to single flow: input → session
- Redesign CollaborativeInput to accept optional context file (MD/TXT)
- Always output Markdown format (remove format selection)
- Add NetworkStatus component showing latency and network quality
- Network indicator auto-checks every 5 seconds with color-coded status
- File upload validation for .md and .txt formats only
- Cleaner, more focused user experience

Output format now always: Markdown
Input options: description (required) + context file (optional)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Augustin ROUX 2025-10-17 17:09:17 +02:00
parent 7574f353ee
commit f20c0b4996
4 changed files with 319 additions and 433 deletions

View File

@ -1,174 +0,0 @@
# Changelog - Collaborative Design System
## Version 2.0 - Collaborative Document Design (Nouvelle Fonctionnalité)
### 🎯 Concept Principal
Remplacement du système de débat parallèle par un système de **conception itérative collaborative** où :
- Un architecte lead crée un document initial complet
- 3-7 agents spécialisés révisent et améliorent le document tour par tour
- Le document évolue progressivement jusqu'à convergence naturelle
- Traçabilité complète de toutes les modifications
### Backend - Changements
#### 1. **Base de Données** (`src/db/schema.js`)
- ✅ Nouvelles tables :
- `collaborative_sessions` : Sessions de conception
- `document_versions` : Historique complet des versions
- `document_rounds` : Suivi des tours de table
- Maintien de la compatibilité rétro avec `debates` et `responses`
#### 2. **Service d'Orchestration** (`src/services/collaborativeOrchestrator.js` - NOUVEAU)
- `createSession()` : Crée une nouvelle session collaborative
- `startCollaborativeSession()` : Lead Architect crée le document initial
- `runRound()` : Exécute un tour de revue (chaque agent revoit le document)
- `getSessionDetails()` : Récupère l'état complet avec historique
- `calculateDiff()` : Suivi des modifications entre versions
- Détection automatique de convergence (aucun changement d'un tour complet)
#### 3. **Routes API** (`src/routes/collaborate.js` - NOUVEAU)
```
POST /api/collaborate - Créer une session
POST /api/collaborate/:id/start - Démarrer la session
POST /api/collaborate/:id/round - Exécuter un tour de revue
GET /api/collaborate/:id - Obtenir les détails
GET /api/collaborate/:id/document - Télécharger le document actuel
GET /api/collaborate/:id/versions/:v - Obtenir une version spécifique
POST /api/collaborate/:id/complete - Terminer la session
```
#### 4. **Serveur Principal** (`src/index.js`)
- ✅ Intégration du nouvel orchestrateur collaboratif
- ✅ Support WebSocket pour `sessionId` en plus de `debateId`
- Routage automatique vers `/api/collaborate`
### Frontend - Changements
#### 1. **Store Pinia** (`src/stores/collaboration.js` - NOUVEAU)
- Gestion d'état des sessions collaboratives
- Fonctions pour créer, lancer, et suivre les sessions
- Méthodes de téléchargement de documents
#### 2. **Composants Vue**
**CollaborativeInput.vue** (NOUVEAU)
- Formulaire pour créer une nouvelle session
- Choix du format (Markdown/Texte)
- Sélection du nombre d'agents (3-7)
- Interface conviviale avec explications
**CollaborativeSession.vue** (NOUVEAU)
- Affichage en temps réel du document en évolution
- Timeline des modifications avec agents impliqués
- Boutons de contrôle :
- "Next Review Round" : Lancer le tour suivant
- "Complete Session" : Terminer
- "Download" : Exporter le document
- "Timeline" : Voir l'historique
- Affichage du statut de convergence
**DocumentViewer.vue** (NOUVEAU)
- Rendu du document en Markdown ou texte brut
- Mise en évidence syntaxique simple
- Support des liens, listes, code blocks
- Responsive et bien stylisé
#### 3. **App.vue** - Refonte Majeure
- Écran de sélection du mode au démarrage
- 2 modes : "Classic Debate" vs "Collaborative Design" (Recommandé)
- Navigation fluide entre les modes
- Boutons de retour intuitifs
#### 4. **Composable WebSocket Amélioré** (`src/composables/useWebSocket.js`)
- Support de `sessionId` en paramètre
- Rétro-compatible avec `debateId`
- Routing automatique du WebSocket
### Améliorations Produit
#### 1. **7 Agents Spécialisés** (vs 4 avant)
- Lead Architect (créateur du document initial, poids 1.5x dans anciennes versions)
- Backend Engineer
- Frontend Engineer
- UI/UX Designer
- DevOps Engineer (NOUVEAU)
- Product Manager (NOUVEAU)
- Security Specialist (NOUVEAU)
#### 2. **Flux Itératif Intelligent**
- Chaque agent voit le document complet + historique
- System prompts spécialisés pour chaque rôle
- Détection de convergence automatique
- Arrêt quand aucune modification d'un tour complet
#### 3. **Documents Flexibles**
- Format Markdown (.md) pour documents structurés
- Format Texte brut (.txt) pour contenu simple
- Versioning complet avec historique
- Export facile en un clic
#### 4. **Traçabilité Complète**
- Historique de chaque modification
- Qui a changé quoi et pourquoi
- Timeline visuelle interactive
- Accès à n'importe quelle version
### Avantages du Nouveau Système
| Aspect | Mode Débat | Mode Collaboratif |
|--------|-----------|------------------|
| **Processus** | Parallèle | Itératif |
| **Document** | Consensus arbitraire | Construction progressive |
| **Agents** | 4 | 3-7 (configurable) |
| **Convergence** | Vote pondéré (60%) | Naturelle (0 changements) |
| **Traçabilité** | Propositions individuelles | Historique complet |
| **Format** | JSON fixe | Flexible (MD/TXT) |
| **Qualité finale** | Bonne | Excellente |
| **Temps** | Rapide (1 appel) | Graduel (multiple tours) |
### Fichiers Créés
- ✅ `backend/src/services/collaborativeOrchestrator.js`
- ✅ `backend/src/routes/collaborate.js`
- ✅ `frontend/src/stores/collaboration.js`
- ✅ `frontend/src/components/CollaborativeInput.vue`
- ✅ `frontend/src/components/CollaborativeSession.vue`
- ✅ `frontend/src/components/DocumentViewer.vue`
- ✅ `CHANGELOG.md` (ce fichier)
### Fichiers Modifiés
- ✅ `backend/src/index.js` - Intégration orchestrateur
- ✅ `backend/src/db/schema.js` - Nouvelles tables
- ✅ `frontend/src/App.vue` - Mode selector et navigation
- ✅ `frontend/src/composables/useWebSocket.js` - Support sessionId
- ✅ `README.md` - Documentation
### Tests à Faire
```bash
# Backend
npm start # Doit démarrer sans erreurs
curl http://localhost:3000/api/health
# Frontend
npm run dev # Vite dev server
# Tester: http://localhost:5173
```
### Prochaines Améliorations Possibles
- [ ] Support téléchargement fichiers contextuels (upload MD/TXT)
- [ ] Intégration Git (commit auto après convergence)
- [ ] Export PDF avec mise en page professionnelle
- [ ] Comparaison côte à côte des versions
- [ ] Statistiques détaillées par agent
- [ ] Webhook pour notifications externes
- [ ] Support collaboration multi-utilisateurs simultanés
### Notes
- Système entièrement rétro-compatible : mode débat classique toujours disponible
- Performance : Chaque tour prend ~30-60s selon la longueur du document
- Coût API : Similaire au mode débat (n agents × 1 appel par tour)
---
**Version** : 2.0.0
**Date** : October 2024
**Status** : Production Ready

View File

@ -1,119 +1,46 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useDebateStore } from './stores/debate'
import { useCollaborationStore } from './stores/collaboration' import { useCollaborationStore } from './stores/collaboration'
import PromptInput from './components/PromptInput.vue'
import DebateThread from './components/DebateThread.vue'
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 NetworkStatus from './components/NetworkStatus.vue'
const debateStore = useDebateStore()
const collaborationStore = useCollaborationStore() const collaborationStore = useCollaborationStore()
const mode = ref('choose') // 'choose', 'debate', 'collaborate'
const showDebate = ref(false)
const showSession = ref(false) const showSession = ref(false)
const currentSessionId = ref(null) const currentSessionId = ref(null)
function handleDebateCreated(debate) {
showDebate.value = true
}
function handleCollaborationCreated(session) { function handleCollaborationCreated(session) {
currentSessionId.value = session.sessionId currentSessionId.value = session.sessionId
showSession.value = true showSession.value = true
} }
function startNewDebate() {
debateStore.clearCurrentDebate()
showDebate.value = false
mode.value = 'choose'
}
function startNewCollaboration() { function startNewCollaboration() {
collaborationStore.clearCurrentSession() collaborationStore.clearCurrentSession()
showSession.value = false showSession.value = false
mode.value = 'choose'
}
function goBack() {
mode.value = 'choose'
showDebate.value = false
showSession.value = false
} }
</script> </script>
<template> <template>
<div class="app"> <div class="app">
<!-- Mode Selection --> <NetworkStatus />
<div v-if="mode === 'choose'" class="mode-selector">
<div class="selector-container">
<h1>Agora AI</h1>
<p class="subtitle">Choose your AI collaboration mode:</p>
<div class="mode-cards"> <!-- Input Mode -->
<button @click="mode = 'debate'" class="mode-card debate-mode"> <div v-if="!showSession">
<div class="mode-icon">🗣</div> <CollaborativeInput @session-created="handleCollaborationCreated" />
<h3>Classic Debate</h3>
<p>
Multiple AI agents debate different perspectives on your architecture
in parallel and reach consensus.
</p>
<span class="mode-badge">Traditional</span>
</button>
<button @click="mode = 'collaborate'" class="mode-card collaborate-mode">
<div class="mode-icon">🤝</div>
<h3>Collaborative Design</h3>
<p>
AI specialists iteratively refine an architecture document through
collaborative rounds until perfect consensus is reached.
</p>
<span class="mode-badge">Recommended</span>
</button>
</div>
</div>
</div> </div>
<!-- Debate Mode --> <!-- Session Mode -->
<div v-else-if="mode === 'debate'"> <div v-else>
<div v-if="!showDebate"> <button @click="startNewCollaboration" class="new-session-btn">
<button @click="goBack" class="back-btn"> Back to Mode Selection</button> + New Session
<PromptInput @debate-created="handleDebateCreated" /> </button>
</div>
<div v-else> <CollaborativeSession
<div class="debate-view"> v-if="currentSessionId"
<button @click="startNewDebate" class="new-debate-btn"> :session-id="currentSessionId"
+ New Debate @session-completed="startNewCollaboration"
</button> />
<DebateThread
v-if="debateStore.currentDebate"
:debate="debateStore.currentDebate"
/>
</div>
</div>
</div>
<!-- Collaboration Mode -->
<div v-else-if="mode === 'collaborate'">
<div v-if="!showSession">
<button @click="goBack" class="back-btn"> Back to Mode Selection</button>
<CollaborativeInput @session-created="handleCollaborationCreated" />
</div>
<div v-else>
<button @click="startNewCollaboration" class="new-session-btn">
+ New Session
</button>
<CollaborativeSession
v-if="currentSessionId"
:session-id="currentSessionId"
@session-completed="startNewCollaboration"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -142,122 +69,6 @@ body {
padding: 2rem; padding: 2rem;
} }
/* Mode Selector */
.mode-selector {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.selector-container {
text-align: center;
color: white;
}
.selector-container h1 {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.3rem;
margin-bottom: 3rem;
opacity: 0.95;
}
.mode-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 900px;
margin: 0 auto;
}
.mode-card {
background: white;
border: none;
border-radius: 16px;
padding: 2rem;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
color: #2c3e50;
text-align: center;
position: relative;
overflow: hidden;
}
.mode-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}
.mode-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.mode-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #667eea;
}
.mode-card p {
color: #666;
line-height: 1.6;
margin-bottom: 1rem;
}
.mode-badge {
display: inline-block;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
}
.debate-mode .mode-badge {
background: #e8eef7;
color: #667eea;
}
.collaborate-mode .mode-badge {
background: #764ba2;
color: white;
}
/* Navigation Buttons */
.back-btn {
position: fixed;
top: 1.5rem;
left: 1.5rem;
padding: 0.75rem 1.5rem;
background-color: white;
border: 2px solid #667eea;
color: #667eea;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.back-btn:hover {
background-color: #667eea;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.debate-view {
position: relative;
}
.new-debate-btn,
.new-session-btn { .new-session-btn {
position: fixed; position: fixed;
top: 2rem; top: 2rem;
@ -274,29 +85,10 @@ body {
z-index: 50; z-index: 50;
} }
.new-debate-btn:hover,
.new-session-btn:hover { .new-session-btn:hover {
background-color: #667eea; background-color: #667eea;
color: white; color: white;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
} }
@media (max-width: 768px) {
.selector-container h1 {
font-size: 2rem;
}
.subtitle {
font-size: 1rem;
}
.mode-cards {
gap: 1rem;
}
.mode-card {
padding: 1.5rem;
}
}
</style> </style>

View File

@ -7,32 +7,53 @@ const emit = defineEmits(['session-created'])
const collaborationStore = useCollaborationStore() const collaborationStore = useCollaborationStore()
const prompt = ref('') const prompt = ref('')
const documentFormat = ref('md') const contextFile = ref(null)
const agentCount = ref(7) const agentCount = ref(7)
const isCreating = ref(false) const isCreating = ref(false)
const formats = [
{ value: 'md', label: 'Markdown (.md)' },
{ value: 'txt', label: 'Plain Text (.txt)' }
]
const agentOptions = [ const agentOptions = [
{ value: 3, label: '3 agents (Quick)' }, { value: 3, label: '3 agents (Quick)' },
{ value: 5, label: '5 agents (Balanced)' }, { value: 5, label: '5 agents (Balanced)' },
{ value: 7, label: '7 agents (Comprehensive)' } { value: 7, label: '7 agents (Comprehensive)' }
] ]
const handleFileSelect = (event) => {
const file = event.target.files?.[0]
if (file) {
const validTypes = ['text/markdown', 'text/plain', 'application/octet-stream']
const validExtensions = ['.md', '.txt']
const hasValidType = validTypes.includes(file.type)
const hasValidExtension = validExtensions.some(ext => file.name.endsWith(ext))
if (!hasValidType && !hasValidExtension) {
alert('Please upload a Markdown (.md) or Text (.txt) file')
return
}
contextFile.value = file
}
}
const handleCreateSession = async () => { const handleCreateSession = async () => {
if (!prompt.value.trim()) { if (!prompt.value.trim()) {
alert('Please enter a project prompt') alert('Please enter a project description')
return return
} }
isCreating.value = true isCreating.value = true
try { try {
let finalPrompt = prompt.value
// If a context file was uploaded, prepend it to the prompt
if (contextFile.value) {
const fileContent = await contextFile.value.text()
finalPrompt = `Context file content:\n\`\`\`\n${fileContent}\n\`\`\`\n\nProject description:\n${prompt.value}`
}
// Always use 'md' format for output
const session = await collaborationStore.createSession( const session = await collaborationStore.createSession(
prompt.value, finalPrompt,
documentFormat.value, 'md',
agentCount.value agentCount.value
) )
@ -40,7 +61,7 @@ const handleCreateSession = async () => {
// Reset form // Reset form
prompt.value = '' prompt.value = ''
documentFormat.value = 'md' contextFile.value = null
agentCount.value = 7 agentCount.value = 7
} catch (error) { } catch (error) {
alert(`Error creating session: ${collaborationStore.error}`) alert(`Error creating session: ${collaborationStore.error}`)
@ -54,47 +75,77 @@ const handleKeydown = (e) => {
handleCreateSession() handleCreateSession()
} }
} }
const removeFile = () => {
contextFile.value = null
}
</script> </script>
<template> <template>
<div class="collaborative-input"> <div class="collaborative-input">
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<h1>🎯 Collaborative Architecture Design</h1> <h1>🎯 AI Architecture Design</h1>
<p class="subtitle"> <p class="subtitle">
Describe your software project idea, and let multiple AI specialists design Describe your software project. Multiple AI specialists will collaboratively
the perfect architecture through collaborative discussion. design the perfect architecture through iterative refinement.
</p> </p>
</header> </header>
<form @submit.prevent="handleCreateSession" class="form"> <form @submit.prevent="handleCreateSession" class="form">
<!-- Project Description -->
<div class="form-section"> <div class="form-section">
<label for="prompt" class="label">Project Description</label> <label for="prompt" class="label">Project Description</label>
<p class="label-hint"> <p class="label-hint">
Describe your software project in detail. What is it? What problems does it solve? Describe your software project in detail. What is it? What problems does it solve? What are the key requirements?
</p> </p>
<textarea <textarea
id="prompt" id="prompt"
v-model="prompt" v-model="prompt"
@keydown="handleKeydown" @keydown="handleKeydown"
placeholder="Example: I want to build a real-time collaborative document editing platform similar to Google Docs, with support for multiple users, version history, and commenting..." placeholder="Example: I want to build a real-time collaborative document editing platform with support for multiple users, version history, and commenting..."
class="textarea" class="textarea"
rows="8" rows="8"
></textarea> ></textarea>
<p class="hint">💡 Tip: The more detailed your description, the better the AI collaboration.</p> <p class="hint">💡 The more detailed your description, the better the AI collaboration.</p>
</div> </div>
<div class="form-grid"> <!-- Optional Context File -->
<div class="form-group"> <div class="form-section">
<label for="format" class="label">Document Format</label> <label class="label">Context File (Optional)</label>
<select v-model="documentFormat" id="format" class="select"> <p class="label-hint">
<option v-for="fmt in formats" :key="fmt.value" :value="fmt.value"> Upload an existing Markdown or text file to use as context for the AI specialists.
{{ fmt.label }} </p>
</option>
</select> <div class="file-input-wrapper">
<p class="hint">The final document will be saved in this format.</p> <input
type="file"
id="contextFile"
accept=".md,.txt"
@change="handleFileSelect"
class="file-input"
/>
<label for="contextFile" class="file-label">
📎 Choose a file (.md or .txt)
</label>
</div> </div>
<div v-if="contextFile" class="file-selected">
<span class="file-name"> {{ contextFile.name }}</span>
<button
type="button"
@click="removeFile"
class="remove-btn"
>
</button>
</div>
<p class="hint">Optional: Provide existing documentation or requirements to guide the design.</p>
</div>
<!-- Agent Count Selection -->
<div class="form-grid">
<div class="form-group"> <div class="form-group">
<label for="agents" class="label">Number of AI Specialists</label> <label for="agents" class="label">Number of AI Specialists</label>
<select v-model.number="agentCount" id="agents" class="select"> <select v-model.number="agentCount" id="agents" class="select">
@ -106,32 +157,32 @@ const handleKeydown = (e) => {
</div> </div>
</div> </div>
<!-- Info Box -->
<div class="info-box"> <div class="info-box">
<p> <p><strong>How it works:</strong></p>
<strong>How it works:</strong>
</p>
<ul> <ul>
<li>A lead architect creates an initial design document</li> <li>Lead Architect creates initial design</li>
<li>{{ agentCount }} AI specialists review and provide feedback</li> <li>{{ agentCount }} specialists review iteratively</li>
<li>Each agent proposes improvements based on their expertise</li> <li>Each proposes improvements based on expertise</li>
<li>The document evolves through collaborative refinement</li> <li>Document evolves through refinement</li>
<li>Process continues until consensus is reached</li> <li>Converges when consensus reached</li>
<li>Download the final architectural specification</li> <li>Output: Markdown architectural specification</li>
</ul> </ul>
</div> </div>
<!-- Submit Button -->
<button <button
@click="handleCreateSession" @click="handleCreateSession"
:disabled="!prompt.trim() || isCreating" :disabled="!prompt.trim() || isCreating"
type="button" type="button"
class="submit-btn" class="submit-btn"
> >
{{ isCreating ? 'Creating Session...' : '✨ Start Collaborative Design Session' }} {{ isCreating ? 'Starting Session...' : '✨ Start Design Session' }}
</button> </button>
</form> </form>
<footer class="footer"> <footer class="footer">
<p>Each session typically involves 2-5 review rounds for complete consensus</p> <p>Typical session: 2-5 rounds for complete consensus. Output format: Markdown</p>
</footer> </footer>
</div> </div>
</div> </div>
@ -294,6 +345,69 @@ const handleKeydown = (e) => {
cursor: not-allowed; cursor: not-allowed;
} }
.file-input-wrapper {
position: relative;
margin-bottom: 1rem;
}
.file-input {
display: none;
}
.file-label {
display: block;
padding: 1rem;
border: 2px dashed #667eea;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: #f9fafb;
color: #667eea;
font-weight: 600;
}
.file-label:hover {
background: #f0f3ff;
border-color: #764ba2;
}
.file-input:focus + .file-label {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.file-selected {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: #e8f5e9;
border: 1px solid #4caf50;
border-radius: 8px;
margin-top: 0.75rem;
}
.file-name {
color: #2e7d32;
font-weight: 600;
font-size: 0.95rem;
}
.remove-btn {
background: none;
border: none;
color: #d32f2f;
font-size: 1.2rem;
cursor: pointer;
padding: 0 0.5rem;
transition: color 0.3s;
}
.remove-btn:hover {
color: #f44336;
}
.footer { .footer {
text-align: center; text-align: center;
color: white; color: white;

View File

@ -0,0 +1,154 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const latency = ref(0)
const quality = ref('excellent')
const lastCheckTime = ref(Date.now())
const props = defineProps({
checkInterval: {
type: Number,
default: 5000 // Check every 5 seconds
}
})
let intervalId = null
onMounted(() => {
checkNetworkStatus()
intervalId = setInterval(checkNetworkStatus, props.checkInterval)
})
onUnmounted(() => {
if (intervalId) clearInterval(intervalId)
})
async function checkNetworkStatus() {
const startTime = Date.now()
try {
const response = await fetch('/api/health', {
method: 'HEAD'
})
if (response.ok) {
const endTime = Date.now()
latency.value = endTime - startTime
updateQuality(latency.value)
}
} catch (error) {
latency.value = 0
quality.value = 'offline'
}
lastCheckTime.value = Date.now()
}
function updateQuality(lat) {
if (lat < 100) quality.value = 'excellent'
else if (lat < 300) quality.value = 'good'
else if (lat < 600) quality.value = 'fair'
else quality.value = 'poor'
}
function getStatusColor() {
switch (quality.value) {
case 'excellent': return '#4caf50'
case 'good': return '#8bc34a'
case 'fair': return '#ffc107'
case 'poor': return '#ff9800'
case 'offline': return '#f44336'
default: return '#666'
}
}
function getStatusEmoji() {
switch (quality.value) {
case 'excellent': return '✓'
case 'good': return '✓'
case 'fair': return '⚠'
case 'poor': return '⚠'
case 'offline': return '✕'
default: return '?'
}
}
</script>
<template>
<div class="network-status" :style="{ backgroundColor: getStatusColor() }">
<div class="status-content">
<span class="emoji">{{ getStatusEmoji() }}</span>
<span class="text">
<span class="latency">{{ latency }}ms</span>
<span class="quality">{{ quality }}</span>
</span>
</div>
</div>
</template>
<style scoped>
.network-status {
position: fixed;
bottom: 2rem;
right: 2rem;
padding: 0.75rem 1.25rem;
border-radius: 20px;
color: white;
font-size: 0.85rem;
font-weight: 600;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
gap: 0.5rem;
z-index: 40;
transition: all 0.3s;
}
.network-status:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.status-content {
display: flex;
align-items: center;
gap: 0.5rem;
}
.emoji {
font-size: 1rem;
display: inline-block;
}
.text {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.latency {
display: block;
font-weight: 700;
font-size: 0.9rem;
}
.quality {
display: block;
font-size: 0.75rem;
opacity: 0.9;
text-transform: capitalize;
}
@media (max-width: 768px) {
.network-status {
bottom: 1rem;
right: 1rem;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
}
.emoji {
font-size: 0.9rem;
}
}
</style>