Compare commits

..

3 Commits

Author SHA1 Message Date
0179bf75f2 Update et upgrade by codex 2025-10-24 13:09:18 +02:00
130e21c867 Add multi-provider AI support (Claude and Mistral)
- Update runRound() to dynamically select correct AI client based on session.aiProvider
- Modify POST /api/collaborate to accept and validate aiProvider parameter
- Add aiProvider field to session creation response
- Add AI provider selector dropdown to frontend (Mistral Large 2411 vs Claude 3.5 Sonnet)
- Update collaboration store to pass aiProvider parameter through API

Users can now choose between Mistral and Claude AI for their collaborative sessions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 16:35:21 +02:00
60fdc9a66f Add Claude AI support as alternative to Mistral
- Create claudeClient.js service for Claude API integration
- Add CLAUDE_API_KEY to .env configuration
- Claude uses identical prompt structure and section extraction logic
- Supports claude-3-5-sonnet-20241022 model

Next: Add provider selection to orchestrator and routes
2025-10-19 16:32:48 +02:00
7 changed files with 339 additions and 43 deletions

View File

@ -10,7 +10,7 @@ const router = express.Router()
*/ */
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
try { try {
const { prompt, documentFormat = 'md', agentCount = 7 } = req.body const { prompt, documentFormat = 'md', agentCount = 7, aiProvider = 'mistral' } = req.body
if (!prompt || prompt.trim().length === 0) { if (!prompt || prompt.trim().length === 0) {
return res.status(400).json({ error: 'Prompt is required' }) return res.status(400).json({ error: 'Prompt is required' })
@ -20,13 +20,18 @@ router.post('/', async (req, res) => {
return res.status(400).json({ error: 'Document format must be "md" or "txt"' }) return res.status(400).json({ error: 'Document format must be "md" or "txt"' })
} }
if (!['mistral', 'claude'].includes(aiProvider)) {
return res.status(400).json({ error: 'AI provider must be "mistral" or "claude"' })
}
// Validate agent count // Validate agent count
const validAgentCount = Math.min(Math.max(agentCount, 3), 50) const validAgentCount = Math.min(Math.max(agentCount, 3), 50)
const sessionId = collaborativeOrchestrator.createSession( const sessionId = collaborativeOrchestrator.createSession(
prompt, prompt,
documentFormat, documentFormat,
validAgentCount validAgentCount,
aiProvider
) )
const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId) const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId)
@ -36,6 +41,7 @@ router.post('/', async (req, res) => {
prompt, prompt,
documentFormat, documentFormat,
agentCount: validAgentCount, agentCount: validAgentCount,
aiProvider,
status: 'created', status: 'created',
agents: sessionInfo.agents, agents: sessionInfo.agents,
message: 'Collaborative session created. Start the session to begin collaboration.' message: 'Collaborative session created. Start the session to begin collaboration.'

View File

@ -0,0 +1,144 @@
import dotenv from 'dotenv'
dotenv.config()
const CLAUDE_API_KEY = process.env.CLAUDE_API_KEY
const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages'
/**
* Generic AI prompt for collaborative document editing
*/
function getAgentPrompt(agentName) {
return `You are an AI assistant named ${agentName} collaborating on a technical document design.
Your responsibilities:
1. Review the current document structure
2. Either:
a) Modify ONE existing section (identified by #, ##, ###, #### headers), OR
b) Create a NEW section if you think it's needed, OR
c) Delete a section if you think it's redundant or not useful
3. Provide your thinking process and reasoning
4. Return ONLY the section (modified or new) with its header, or command to delete, or confirm it's good as-is
CRITICAL RULES - FOLLOW THESE EXACTLY:
- Work on exactly ONE section only
- NEVER return the entire document
- NEVER return multiple sections
- Return ONLY the section you're working on, not the whole document
- You CAN create a new section if document is missing important content
- You CAN delete a section if it's redundant, duplicate, or not useful
- To delete a section, respond: "DELETE: ## Section Name" (with exact header)
- If section is good, respond: "Section is good, no changes needed"
- Think step-by-step about what could be improved or removed
- Share your reasoning process
RESPONSE FORMAT - FOLLOW THIS EXACTLY:
THINKING: [Your analysis and reasoning about the current document]
DECISION: [Exactly what you will do: modify section X, create new section Y, delete section Z, or keep as-is]
SECTION:
[ONLY ONE: Either a markdown section starting with # or ##, a DELETE command, or the text "Section is good, no changes needed"]
EXAMPLE OF CORRECT RESPONSE:
THINKING: The Overview section is too brief and doesn't explain the main purpose.
DECISION: I will modify the Overview section to be more comprehensive.
SECTION:
## Overview
This is a technical document for designing system architecture...
EXAMPLE OF INCORRECT RESPONSE (DO NOT DO THIS):
[The entire document repeated here] <- WRONG!`
}
/**
* Call Claude API
*/
async function callClaudeAPI(messages) {
const response = await fetch(CLAUDE_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': CLAUDE_API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 2048,
system: messages[0].content,
messages: messages.slice(1)
})
})
if (!response.ok) {
const error = await response.text()
throw new Error(`Claude API error: ${error}`)
}
return response
}
/**
* Generate agent response using Claude
*/
export async function generateAgentResponseSync(agentName, prompt, currentDocument = '') {
const systemPrompt = getAgentPrompt(agentName)
const messages = [
{ role: 'user', content: systemPrompt },
{ role: 'user', content: `Project description: ${prompt}\n\nCurrent document:\n${currentDocument}` }
]
try {
const response = await callClaudeAPI(messages)
const data = await response.json()
if (!data.content?.[0]?.text) {
throw new Error('Invalid response from Claude API')
}
return data.content[0].text
} catch (error) {
console.error(`Error generating response from ${agentName}:`, error)
return `Error: ${error.message}`
}
}
/**
* Extract section from AI response
* Validates that we get a proper section, not the entire document
*/
export function extractSection(aiResponse) {
const sectionMatch = aiResponse.match(/SECTION:\s*([\s\S]*?)(?:$)/)
if (sectionMatch) {
const extracted = sectionMatch[1].trim()
// Validate: extracted should be either:
// 1. A markdown section starting with # (DELETE: or "Section is good...")
// 2. OR a single section with < 5000 chars (not entire document)
const isMarkdownSection = /^(#|DELETE:|Section is good)/.test(extracted)
const isShortEnough = extracted.length < 5000 // Single section should be < 5KB
if (isMarkdownSection || isShortEnough) {
return extracted
}
}
// Fallback: if no SECTION: tag found, treat entire response as section
// but only if it starts with markdown header or is a command
if (/^(#|DELETE:|Section is good)/.test(aiResponse)) {
return aiResponse.trim()
}
// If none of the above, return error indicator
return "ERROR: Response does not contain a valid section format"
}
/**
* Extract thinking from AI response
*/
export function extractThinking(aiResponse) {
const thinkingMatch = aiResponse.match(/THINKING:\s*([\s\S]*?)(?:DECISION:|SECTION:)/)
if (thinkingMatch) {
return thinkingMatch[1].trim()
}
return ''
}

View File

@ -1,5 +1,6 @@
import db from '../db/schema.js' import db from '../db/schema.js'
import { generateAgentResponseSync, extractSection, extractThinking } from './mistralClient.js' import * as mistralClient from './mistralClient.js'
import * as claudeClient from './claudeClient.js'
import { getRandomNames } from './nameGenerator.js' import { getRandomNames } from './nameGenerator.js'
class CollaborativeOrchestrator { class CollaborativeOrchestrator {
@ -44,13 +45,13 @@ class CollaborativeOrchestrator {
/** /**
* Create a new collaborative session with N random-named agents * Create a new collaborative session with N random-named agents
*/ */
createSession(initialPrompt, documentFormat = 'md', agentCount = 7) { createSession(initialPrompt, documentFormat = 'md', agentCount = 7, aiProvider = 'mistral') {
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, 'created')
const sessionId = result.lastInsertRowid const sessionId = result.lastInsertRowid
console.log(`[Session ${sessionId}] Created with ${agentCount} agents, format: ${documentFormat}`) console.log(`[Session ${sessionId}] Created with ${agentCount} agents, format: ${documentFormat}, provider: ${aiProvider}`)
// Generate random names for agents // Generate random names for agents
const agentNames = getRandomNames(Math.min(agentCount, 50)) const agentNames = getRandomNames(Math.min(agentCount, 50))
@ -59,6 +60,7 @@ class CollaborativeOrchestrator {
id: sessionId, id: sessionId,
initialPrompt, initialPrompt,
documentFormat, documentFormat,
aiProvider, // Claude or Mistral
agents: agentNames, // Array of agent names agents: agentNames, // Array of agent names
agentCount, agentCount,
currentAgentIndex: 0, currentAgentIndex: 0,
@ -179,14 +181,15 @@ class CollaborativeOrchestrator {
try { try {
console.log(`[Session ${sessionId}] ${agentName} analyzing and generating response...`) console.log(`[Session ${sessionId}] ${agentName} analyzing and generating response...`)
const response = await generateAgentResponseSync( const client = session.aiProvider === 'claude' ? claudeClient : mistralClient
const response = await client.generateAgentResponseSync(
agentName, agentName,
session.initialPrompt, session.initialPrompt,
session.currentDocument // <-- This is always the latest version session.currentDocument // <-- This is always the latest version
) )
const thinking = extractThinking(response) const thinking = client.extractThinking(response)
const section = extractSection(response) const section = client.extractSection(response)
console.log(`[Session ${sessionId}] ${agentName} response received (${response.length} chars)`) console.log(`[Session ${sessionId}] ${agentName} response received (${response.length} chars)`)
console.log(`[Session ${sessionId}] --- THINKING (${agentName}) ---`) console.log(`[Session ${sessionId}] --- THINKING (${agentName}) ---`)

View File

@ -9,24 +9,23 @@ const collaborationStore = useCollaborationStore()
const prompt = ref('') const prompt = ref('')
const contextFile = ref(null) const contextFile = ref(null)
const agentCount = ref(7) const agentCount = ref(7)
const aiProvider = ref('mistral')
const documentFormat = ref('md')
const isCreating = ref(false) const isCreating = ref(false)
const previousSessions = ref([])
const loadingPreviousSessions = ref(false)
const showAllSessions = ref(false) const showAllSessions = ref(false)
const showArchives = ref(false)
const sessionStatusFilter = ref('all') // 'all', 'completed', 'ongoing', 'created' const sessionStatusFilter = ref('all') // 'all', 'completed', 'ongoing', 'created'
const searchQuery = ref('') const searchQuery = ref('')
const isFetchingSessions = ref(false)
onMounted(async () => { onMounted(async () => {
loadingPreviousSessions.value = true if (collaborationStore.sessionsLoaded.value) return
isFetchingSessions.value = true
try { try {
const response = await fetch('/api/collaborate') await collaborationStore.fetchSessions()
const data = await response.json()
previousSessions.value = data.sessions || []
} catch (error) { } catch (error) {
console.error('Error loading previous sessions:', error) console.error('Error loading previous sessions:', error)
} finally { } finally {
loadingPreviousSessions.value = false isFetchingSessions.value = false
} }
}) })
@ -34,6 +33,8 @@ const handleOpenSession = (sessionId) => {
emit('session-created', { sessionId }) emit('session-created', { sessionId })
} }
const sessions = computed(() => collaborationStore.sessions.value)
const agentOptions = computed(() => { const agentOptions = computed(() => {
return Array.from({ length: 48 }, (_, i) => ({ return Array.from({ length: 48 }, (_, i) => ({
value: i + 3, value: i + 3,
@ -42,7 +43,7 @@ const agentOptions = computed(() => {
}) })
const filteredSessions = computed(() => { const filteredSessions = computed(() => {
let filtered = previousSessions.value let filtered = sessions.value
if (sessionStatusFilter.value !== 'all') { if (sessionStatusFilter.value !== 'all') {
filtered = filtered.filter(s => s.status === sessionStatusFilter.value) filtered = filtered.filter(s => s.status === sessionStatusFilter.value)
@ -64,11 +65,11 @@ const displayedSessions = computed(() => {
}) })
const completedCount = computed(() => { const completedCount = computed(() => {
return previousSessions.value.filter(s => s.status === 'completed').length return sessions.value.filter(s => s.status === 'completed').length
}) })
const ongoingCount = computed(() => { const ongoingCount = computed(() => {
return previousSessions.value.filter(s => s.status === 'ongoing').length return sessions.value.filter(s => s.status === 'ongoing').length
}) })
const handleFileSelect = (event) => { const handleFileSelect = (event) => {
@ -107,8 +108,9 @@ const handleCreateSession = async () => {
// Always use 'md' format for output // Always use 'md' format for output
const session = await collaborationStore.createSession( const session = await collaborationStore.createSession(
finalPrompt, finalPrompt,
'md', documentFormat.value,
agentCount.value agentCount.value,
aiProvider.value
) )
emit('session-created', session) emit('session-created', session)
@ -117,6 +119,8 @@ const handleCreateSession = async () => {
prompt.value = '' prompt.value = ''
contextFile.value = null contextFile.value = null
agentCount.value = 7 agentCount.value = 7
aiProvider.value = 'mistral'
documentFormat.value = 'md'
} catch (error) { } catch (error) {
alert(`Error creating session: ${collaborationStore.error}`) alert(`Error creating session: ${collaborationStore.error}`)
} finally { } finally {
@ -147,7 +151,11 @@ const removeFile = () => {
</header> </header>
<!-- Quick Access Section --> <!-- Quick Access Section -->
<div v-if="previousSessions.length > 0" class="quick-access-section"> <div v-if="isFetchingSessions" class="loading-sessions">
Loading previous sessions...
</div>
<div v-else-if="sessions.length > 0" class="quick-access-section">
<div class="stats-bar"> <div class="stats-bar">
<div class="stat"> <div class="stat">
<span class="stat-label">Completed</span> <span class="stat-label">Completed</span>
@ -159,7 +167,7 @@ const removeFile = () => {
</div> </div>
<div class="stat"> <div class="stat">
<span class="stat-label">Total</span> <span class="stat-label">Total</span>
<span class="stat-value">{{ previousSessions.length }}</span> <span class="stat-value">{{ sessions.length }}</span>
</div> </div>
</div> </div>
@ -180,7 +188,7 @@ const removeFile = () => {
class="filter-btn" class="filter-btn"
:class="{ active: sessionStatusFilter === 'all' }" :class="{ active: sessionStatusFilter === 'all' }"
> >
All Sessions ({{ previousSessions.length }}) All Sessions ({{ sessions.length }})
</button> </button>
<button <button
@click="sessionStatusFilter = 'completed'" @click="sessionStatusFilter = 'completed'"
@ -234,7 +242,7 @@ const removeFile = () => {
</div> </div>
<!-- Divider --> <!-- Divider -->
<div v-if="previousSessions.length > 0" class="divider"> <div v-if="sessions.length > 0" class="divider">
<span>Or start a new session</span> <span>Or start a new session</span>
</div> </div>
@ -290,7 +298,7 @@ const removeFile = () => {
<p class="hint">Optional: Provide existing documentation or requirements to guide the design.</p> <p class="hint">Optional: Provide existing documentation or requirements to guide the design.</p>
</div> </div>
<!-- Agent Count Selection --> <!-- Agent Count and AI Provider Selection -->
<div class="form-grid"> <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>
@ -301,6 +309,22 @@ const removeFile = () => {
</select> </select>
<p class="hint">More agents = more diverse perspectives.</p> <p class="hint">More agents = more diverse perspectives.</p>
</div> </div>
<div class="form-group">
<label for="provider" class="label">AI Provider</label>
<select v-model="aiProvider" id="provider" class="select">
<option value="mistral">Mistral (Large 2411)</option>
<option value="claude">Claude (3.5 Sonnet)</option>
</select>
<p class="hint">Choose the AI model for specialists.</p>
</div>
<div class="form-group">
<label for="format" class="label">Document Format</label>
<select v-model="documentFormat" id="format" class="select">
<option value="md">Markdown (.md)</option>
<option value="txt">Plain Text (.txt)</option>
</select>
<p class="hint">Markdown recommended for structured documents.</p>
</div>
</div> </div>
<!-- Info Box --> <!-- Info Box -->
@ -613,6 +637,17 @@ const removeFile = () => {
cursor: not-allowed; cursor: not-allowed;
} }
.loading-sessions {
margin-bottom: 2rem;
padding: 1rem;
text-align: center;
color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
backdrop-filter: blur(8px);
}
.file-input-wrapper { .file-input-wrapper {
position: relative; position: relative;
margin-bottom: 1rem; margin-bottom: 1rem;

View File

@ -80,11 +80,19 @@ const handleWebSocketMessage = (message) => {
if (message.type === 'initial_document_created') { if (message.type === 'initial_document_created') {
sessionStarted.value = true sessionStarted.value = true
collaborationStore.currentDocument = message.content collaborationStore.currentDocument = message.content
collaborationStore.currentRound = 0
if (collaborationStore.currentSession) {
collaborationStore.currentSession.status = 'ongoing'
collaborationStore.currentSession.currentDocument = message.content
}
currentWorkingAgent.value = null currentWorkingAgent.value = null
currentAgentThinking.value = '' currentAgentThinking.value = ''
scheduleNextRound(2000) scheduleNextRound(2000)
} else if (message.type === 'document_modified') { } else if (message.type === 'document_modified') {
collaborationStore.currentDocument = message.content collaborationStore.currentDocument = message.content
if (collaborationStore.currentSession) {
collaborationStore.currentSession.currentDocument = message.content
}
} else if (message.type === 'agent_working') { } else if (message.type === 'agent_working') {
currentWorkingAgent.value = message.agentName currentWorkingAgent.value = message.agentName
currentAgentThinking.value = '' currentAgentThinking.value = ''
@ -92,11 +100,17 @@ const handleWebSocketMessage = (message) => {
currentAgentThinking.value = message.thinking || '' currentAgentThinking.value = message.thinking || ''
} else if (message.type === 'round_complete') { } else if (message.type === 'round_complete') {
isRunningRound.value = false isRunningRound.value = false
collaborationStore.conversationHistory.push({ const roundEntry = {
roundNumber: message.roundNumber, roundNumber: message.roundNumber,
agentsMadeChanges: message.agentsMadeChanges, agentsMadeChanges: message.agentsMadeChanges,
timestamp: Date.now() timestamp: Date.now()
}) }
collaborationStore.conversationHistory.push(roundEntry)
if (collaborationStore.currentSession) {
const history = collaborationStore.currentSession.conversationHistory || []
collaborationStore.currentSession.conversationHistory = [...history, roundEntry]
}
collaborationStore.currentRound = message.roundNumber
currentWorkingAgent.value = null currentWorkingAgent.value = null
currentAgentThinking.value = '' currentAgentThinking.value = ''
@ -116,6 +130,9 @@ const handleWebSocketMessage = (message) => {
} else if (message.type === 'session_completed') { } else if (message.type === 'session_completed') {
currentWorkingAgent.value = null currentWorkingAgent.value = null
isAutoRunning.value = false isAutoRunning.value = false
if (collaborationStore.currentSession) {
collaborationStore.currentSession.status = 'completed'
}
} }
} }
@ -237,6 +254,10 @@ function formatAgentName(agent) {
<span class="hand"></span> <span class="hand"></span>
</div> </div>
<div class="agent-name-working">{{ formatAgentName(currentWorkingAgent) }}</div> <div class="agent-name-working">{{ formatAgentName(currentWorkingAgent) }}</div>
<div class="agent-thinking">
<span v-if="currentAgentThinking">{{ currentAgentThinking }}</span>
<span v-else>Analyzing the document...</span>
</div>
</div> </div>
<!-- Convergence Message --> <!-- Convergence Message -->
@ -461,6 +482,16 @@ function formatAgentName(agent) {
font-weight: 600; font-weight: 600;
} }
.agent-thinking {
flex: 1;
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.75);
background: rgba(102, 126, 234, 0.08);
border-radius: 12px;
padding: 0.75rem 1rem;
line-height: 1.4;
}
.convergence-message { .convergence-message {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,7 +1,8 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed, onMounted, watch, ref, nextTick } from 'vue'
import { marked } from 'marked' import { marked } from 'marked'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import mermaid from 'mermaid'
const props = defineProps({ const props = defineProps({
document: { document: {
@ -23,22 +24,28 @@ marked.setOptions({
const renderer = new marked.Renderer() const renderer = new marked.Renderer()
// Override code block rendering to add syntax highlighting // Configure mermaid once
renderer.code = ({ text, lang }) => { mermaid.initialize({
const language = lang || 'plain' startOnLoad: false,
let highlighted = text theme: 'dark',
securityLevel: 'loose'
})
if (hljs.getLanguage(language)) { // Override code block rendering to handle Mermaid and syntax highlighting
highlighted = hljs.highlight(text, { language }).value renderer.code = ({ text, lang }) => {
} else { if ((lang || '').toLowerCase() === 'mermaid') {
highlighted = hljs.highlight(text, { language: 'plain' }).value return `<div class="mermaid">${text}</div>`
} }
const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext'
const highlighted = hljs.highlight(text, { language }).value
return `<pre><code class="hljs language-${language}">${highlighted}</code></pre>` return `<pre><code class="hljs language-${language}">${highlighted}</code></pre>`
} }
marked.setOptions({ renderer }) marked.setOptions({ renderer })
const containerRef = ref(null)
const renderedContent = computed(() => { const renderedContent = computed(() => {
if (!props.document) { if (!props.document) {
return '<p class="empty-state">No document content yet. Start the session to begin collaboration.</p>' return '<p class="empty-state">No document content yet. Start the session to begin collaboration.</p>'
@ -57,18 +64,40 @@ const renderedContent = computed(() => {
return `<pre><code>${escaped}</code></pre>` return `<pre><code>${escaped}</code></pre>`
} }
}) })
async function renderMermaid() {
await nextTick()
if (!containerRef.value) return
const nodes = containerRef.value.querySelectorAll('.mermaid')
if (!nodes.length) return
try {
await mermaid.run({ nodes })
} catch (error) {
console.error('Mermaid render error:', error)
}
}
onMounted(() => {
renderMermaid()
})
watch(() => [props.document, props.format], () => {
renderMermaid()
})
</script> </script>
<template> <template>
<div class="document-viewer"> <div class="document-viewer">
<div class="document-container" :class="`format-${format}`"> <div class="document-container" :class="`format-${format}`">
<div v-html="renderedContent" class="document-content"></div> <div ref="containerRef" v-html="renderedContent" class="document-content"></div>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
:import 'highlight.js/styles/atom-one-dark.css'; @import 'highlight.js/styles/atom-one-dark.css';
.document-viewer { .document-viewer {
width: 100%; width: 100%;

View File

@ -9,13 +9,40 @@ export const useCollaborationStore = defineStore('collaboration', () => {
const currentDocument = ref('') const currentDocument = ref('')
const currentRound = ref(0) const currentRound = ref(0)
const conversationHistory = ref([]) const conversationHistory = ref([])
const sessionsLoaded = ref(false)
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000' const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'
/**
* Fetch recent collaborative sessions
*/
async function fetchSessions() {
if (loading.value) return
loading.value = true
error.value = null
try {
const response = await fetch(`${API_URL}/api/collaborate`)
if (!response.ok) {
throw new Error('Failed to fetch sessions')
}
const data = await response.json()
sessions.value = data.sessions || []
sessionsLoaded.value = true
return sessions.value
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
/** /**
* Create a new collaborative session * Create a new collaborative session
*/ */
async function createSession(prompt, documentFormat = 'md', agentCount = 7) { async function createSession(prompt, documentFormat = 'md', agentCount = 7, aiProvider = 'mistral') {
loading.value = true loading.value = true
error.value = null error.value = null
@ -28,7 +55,8 @@ export const useCollaborationStore = defineStore('collaboration', () => {
body: JSON.stringify({ body: JSON.stringify({
prompt, prompt,
documentFormat, documentFormat,
agentCount agentCount,
aiProvider
}) })
}) })
@ -38,7 +66,8 @@ export const useCollaborationStore = defineStore('collaboration', () => {
const data = await response.json() const data = await response.json()
currentSession.value = data currentSession.value = data
sessions.value.unshift(data) sessions.value = [data, ...sessions.value.filter(s => s.sessionId !== data.sessionId)]
sessionsLoaded.value = true
return data return data
} catch (err) { } catch (err) {
@ -152,7 +181,24 @@ export const useCollaborationStore = defineStore('collaboration', () => {
throw new Error('Failed to complete session') throw new Error('Failed to complete session')
} }
return await response.json() const data = await response.json()
if (currentSession.value && currentSession.value.sessionId === sessionId) {
currentSession.value = {
...currentSession.value,
status: 'completed',
completedAt: new Date().toISOString()
}
}
sessions.value = sessions.value.map(session => {
if (session.sessionId === sessionId) {
return { ...session, status: 'completed', completedAt: new Date().toISOString() }
}
return session
})
return data
} catch (err) { } catch (err) {
error.value = err.message error.value = err.message
throw err throw err
@ -190,11 +236,13 @@ export const useCollaborationStore = defineStore('collaboration', () => {
return { return {
currentSession, currentSession,
sessions, sessions,
sessionsLoaded,
loading, loading,
error, error,
currentDocument, currentDocument,
currentRound, currentRound,
conversationHistory, conversationHistory,
fetchSessions,
createSession, createSession,
startSession, startSession,
runRound, runRound,