Compare commits

..

No commits in common. "0179bf75f2de8b9a9fc8a83a9db2437ecc2a3e3a" and "5a2ca101f8949b6058c288da5fd3eb06c980abf6" have entirely different histories.

7 changed files with 43 additions and 339 deletions

View File

@ -10,7 +10,7 @@ const router = express.Router()
*/
router.post('/', async (req, res) => {
try {
const { prompt, documentFormat = 'md', agentCount = 7, aiProvider = 'mistral' } = req.body
const { prompt, documentFormat = 'md', agentCount = 7 } = req.body
if (!prompt || prompt.trim().length === 0) {
return res.status(400).json({ error: 'Prompt is required' })
@ -20,18 +20,13 @@ router.post('/', async (req, res) => {
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
const validAgentCount = Math.min(Math.max(agentCount, 3), 50)
const sessionId = collaborativeOrchestrator.createSession(
prompt,
documentFormat,
validAgentCount,
aiProvider
validAgentCount
)
const sessionInfo = collaborativeOrchestrator.getSessionInfo(sessionId)
@ -41,7 +36,6 @@ router.post('/', async (req, res) => {
prompt,
documentFormat,
agentCount: validAgentCount,
aiProvider,
status: 'created',
agents: sessionInfo.agents,
message: 'Collaborative session created. Start the session to begin collaboration.'

View File

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

View File

@ -9,23 +9,24 @@ const collaborationStore = useCollaborationStore()
const prompt = ref('')
const contextFile = ref(null)
const agentCount = ref(7)
const aiProvider = ref('mistral')
const documentFormat = ref('md')
const isCreating = ref(false)
const previousSessions = ref([])
const loadingPreviousSessions = ref(false)
const showAllSessions = ref(false)
const showArchives = ref(false)
const sessionStatusFilter = ref('all') // 'all', 'completed', 'ongoing', 'created'
const searchQuery = ref('')
const isFetchingSessions = ref(false)
onMounted(async () => {
if (collaborationStore.sessionsLoaded.value) return
isFetchingSessions.value = true
loadingPreviousSessions.value = true
try {
await collaborationStore.fetchSessions()
const response = await fetch('/api/collaborate')
const data = await response.json()
previousSessions.value = data.sessions || []
} catch (error) {
console.error('Error loading previous sessions:', error)
} finally {
isFetchingSessions.value = false
loadingPreviousSessions.value = false
}
})
@ -33,8 +34,6 @@ const handleOpenSession = (sessionId) => {
emit('session-created', { sessionId })
}
const sessions = computed(() => collaborationStore.sessions.value)
const agentOptions = computed(() => {
return Array.from({ length: 48 }, (_, i) => ({
value: i + 3,
@ -43,7 +42,7 @@ const agentOptions = computed(() => {
})
const filteredSessions = computed(() => {
let filtered = sessions.value
let filtered = previousSessions.value
if (sessionStatusFilter.value !== 'all') {
filtered = filtered.filter(s => s.status === sessionStatusFilter.value)
@ -65,11 +64,11 @@ const displayedSessions = computed(() => {
})
const completedCount = computed(() => {
return sessions.value.filter(s => s.status === 'completed').length
return previousSessions.value.filter(s => s.status === 'completed').length
})
const ongoingCount = computed(() => {
return sessions.value.filter(s => s.status === 'ongoing').length
return previousSessions.value.filter(s => s.status === 'ongoing').length
})
const handleFileSelect = (event) => {
@ -108,9 +107,8 @@ const handleCreateSession = async () => {
// Always use 'md' format for output
const session = await collaborationStore.createSession(
finalPrompt,
documentFormat.value,
agentCount.value,
aiProvider.value
'md',
agentCount.value
)
emit('session-created', session)
@ -119,8 +117,6 @@ const handleCreateSession = async () => {
prompt.value = ''
contextFile.value = null
agentCount.value = 7
aiProvider.value = 'mistral'
documentFormat.value = 'md'
} catch (error) {
alert(`Error creating session: ${collaborationStore.error}`)
} finally {
@ -151,11 +147,7 @@ const removeFile = () => {
</header>
<!-- 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 v-if="previousSessions.length > 0" class="quick-access-section">
<div class="stats-bar">
<div class="stat">
<span class="stat-label">Completed</span>
@ -167,7 +159,7 @@ const removeFile = () => {
</div>
<div class="stat">
<span class="stat-label">Total</span>
<span class="stat-value">{{ sessions.length }}</span>
<span class="stat-value">{{ previousSessions.length }}</span>
</div>
</div>
@ -188,7 +180,7 @@ const removeFile = () => {
class="filter-btn"
:class="{ active: sessionStatusFilter === 'all' }"
>
All Sessions ({{ sessions.length }})
All Sessions ({{ previousSessions.length }})
</button>
<button
@click="sessionStatusFilter = 'completed'"
@ -242,7 +234,7 @@ const removeFile = () => {
</div>
<!-- Divider -->
<div v-if="sessions.length > 0" class="divider">
<div v-if="previousSessions.length > 0" class="divider">
<span>Or start a new session</span>
</div>
@ -298,7 +290,7 @@ const removeFile = () => {
<p class="hint">Optional: Provide existing documentation or requirements to guide the design.</p>
</div>
<!-- Agent Count and AI Provider Selection -->
<!-- Agent Count Selection -->
<div class="form-grid">
<div class="form-group">
<label for="agents" class="label">Number of AI Specialists</label>
@ -309,22 +301,6 @@ const removeFile = () => {
</select>
<p class="hint">More agents = more diverse perspectives.</p>
</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>
<!-- Info Box -->
@ -637,17 +613,6 @@ const removeFile = () => {
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 {
position: relative;
margin-bottom: 1rem;

View File

@ -80,19 +80,11 @@ const handleWebSocketMessage = (message) => {
if (message.type === 'initial_document_created') {
sessionStarted.value = true
collaborationStore.currentDocument = message.content
collaborationStore.currentRound = 0
if (collaborationStore.currentSession) {
collaborationStore.currentSession.status = 'ongoing'
collaborationStore.currentSession.currentDocument = message.content
}
currentWorkingAgent.value = null
currentAgentThinking.value = ''
scheduleNextRound(2000)
} else if (message.type === 'document_modified') {
collaborationStore.currentDocument = message.content
if (collaborationStore.currentSession) {
collaborationStore.currentSession.currentDocument = message.content
}
} else if (message.type === 'agent_working') {
currentWorkingAgent.value = message.agentName
currentAgentThinking.value = ''
@ -100,17 +92,11 @@ const handleWebSocketMessage = (message) => {
currentAgentThinking.value = message.thinking || ''
} else if (message.type === 'round_complete') {
isRunningRound.value = false
const roundEntry = {
collaborationStore.conversationHistory.push({
roundNumber: message.roundNumber,
agentsMadeChanges: message.agentsMadeChanges,
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
currentAgentThinking.value = ''
@ -130,9 +116,6 @@ const handleWebSocketMessage = (message) => {
} else if (message.type === 'session_completed') {
currentWorkingAgent.value = null
isAutoRunning.value = false
if (collaborationStore.currentSession) {
collaborationStore.currentSession.status = 'completed'
}
}
}
@ -254,10 +237,6 @@ function formatAgentName(agent) {
<span class="hand"></span>
</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>
<!-- Convergence Message -->
@ -482,16 +461,6 @@ function formatAgentName(agent) {
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 {
display: flex;
align-items: center;

View File

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

View File

@ -9,40 +9,13 @@ export const useCollaborationStore = defineStore('collaboration', () => {
const currentDocument = ref('')
const currentRound = ref(0)
const conversationHistory = ref([])
const sessionsLoaded = ref(false)
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
*/
async function createSession(prompt, documentFormat = 'md', agentCount = 7, aiProvider = 'mistral') {
async function createSession(prompt, documentFormat = 'md', agentCount = 7) {
loading.value = true
error.value = null
@ -55,8 +28,7 @@ export const useCollaborationStore = defineStore('collaboration', () => {
body: JSON.stringify({
prompt,
documentFormat,
agentCount,
aiProvider
agentCount
})
})
@ -66,8 +38,7 @@ export const useCollaborationStore = defineStore('collaboration', () => {
const data = await response.json()
currentSession.value = data
sessions.value = [data, ...sessions.value.filter(s => s.sessionId !== data.sessionId)]
sessionsLoaded.value = true
sessions.value.unshift(data)
return data
} catch (err) {
@ -181,24 +152,7 @@ export const useCollaborationStore = defineStore('collaboration', () => {
throw new Error('Failed to complete session')
}
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
return await response.json()
} catch (err) {
error.value = err.message
throw err
@ -236,13 +190,11 @@ export const useCollaborationStore = defineStore('collaboration', () => {
return {
currentSession,
sessions,
sessionsLoaded,
loading,
error,
currentDocument,
currentRound,
conversationHistory,
fetchSessions,
createSession,
startSession,
runRound,