Implement session management to maintain conversation history and entity context across page refreshes. Sessions are stored in-memory and include: - Conversation history (stored server-side, restored on reconnect) - Entity snapshots synced before each message for LLM context - Auto-injection of diagram state into Claude's system prompt Key changes: - Add session store service with create/resume/sync/clear operations - Add session API endpoints (/api/session/*) - Update Claude API to inject entity context and manage history - Update ChatPanel to initialize sessions and sync entities - Add debug endpoint for inspecting session state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
116 lines
4.2 KiB
JavaScript
116 lines
4.2 KiB
JavaScript
import { Router } from "express";
|
|
import { getSession, addMessage, getConversationForAPI } from "../services/sessionStore.js";
|
|
|
|
const router = Router();
|
|
const ANTHROPIC_API_URL = "https://api.anthropic.com";
|
|
|
|
/**
|
|
* Build entity context string for the system prompt
|
|
*/
|
|
function buildEntityContext(entities) {
|
|
if (!entities || entities.length === 0) {
|
|
return "\n\nThe diagram is currently empty.";
|
|
}
|
|
|
|
const entityList = entities.map(e => {
|
|
const shape = e.template?.replace('#', '').replace('-template', '') || 'unknown';
|
|
const pos = e.position || { x: 0, y: 0, z: 0 };
|
|
return `- ${e.text || '(no label)'} (${shape}, ${e.color || 'unknown'}) at (${pos.x?.toFixed(1)}, ${pos.y?.toFixed(1)}, ${pos.z?.toFixed(1)})`;
|
|
}).join('\n');
|
|
|
|
return `\n\n## Current Diagram State\nThe diagram currently contains ${entities.length} entities:\n${entityList}`;
|
|
}
|
|
|
|
// Express 5 uses named parameters for wildcards
|
|
router.post("/*path", async (req, res) => {
|
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
|
|
if (!apiKey) {
|
|
return res.status(500).json({ error: "API key not configured" });
|
|
}
|
|
|
|
// Get the path after /api/claude (e.g., /v1/messages)
|
|
// Express 5 returns path segments as an array
|
|
const pathParam = req.params.path;
|
|
const path = "/" + (Array.isArray(pathParam) ? pathParam.join("/") : pathParam || "");
|
|
|
|
// Check for session-based request
|
|
const { sessionId, ...requestBody } = req.body;
|
|
let modifiedBody = requestBody;
|
|
|
|
if (sessionId) {
|
|
const session = getSession(sessionId);
|
|
if (session) {
|
|
console.log(`[Claude API] Session ${sessionId}: ${session.entities.length} entities, ${session.conversationHistory.length} messages in history`);
|
|
|
|
// Inject entity context into system prompt
|
|
if (modifiedBody.system) {
|
|
const entityContext = buildEntityContext(session.entities);
|
|
console.log(`[Claude API] Entity context:`, entityContext);
|
|
modifiedBody.system += entityContext;
|
|
}
|
|
|
|
// Get conversation history and merge with current messages
|
|
const historyMessages = getConversationForAPI(sessionId);
|
|
if (historyMessages.length > 0 && modifiedBody.messages) {
|
|
// Filter out any duplicate messages (in case client sent history too)
|
|
const currentContent = modifiedBody.messages[modifiedBody.messages.length - 1]?.content;
|
|
const filteredHistory = historyMessages.filter(msg => msg.content !== currentContent);
|
|
modifiedBody.messages = [...filteredHistory, ...modifiedBody.messages];
|
|
console.log(`[Claude API] Merged ${filteredHistory.length} history messages with ${modifiedBody.messages.length - filteredHistory.length} new messages`);
|
|
}
|
|
} else {
|
|
console.log(`[Claude API] Session ${sessionId} not found`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-api-key": apiKey,
|
|
"anthropic-version": "2023-06-01",
|
|
},
|
|
body: JSON.stringify(modifiedBody),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// If session exists and response is successful, store messages
|
|
if (sessionId && response.ok && data.content) {
|
|
const session = getSession(sessionId);
|
|
if (session) {
|
|
// Store the user message if it was new (only if it's a string, not tool results)
|
|
const userMessage = requestBody.messages?.[requestBody.messages.length - 1];
|
|
if (userMessage && userMessage.role === 'user' && typeof userMessage.content === 'string') {
|
|
addMessage(sessionId, {
|
|
role: 'user',
|
|
content: userMessage.content
|
|
});
|
|
}
|
|
|
|
// Store the assistant response (text only, not tool use blocks)
|
|
const assistantContent = data.content
|
|
.filter(c => c.type === 'text')
|
|
.map(c => c.text)
|
|
.join('\n');
|
|
|
|
if (assistantContent) {
|
|
addMessage(sessionId, {
|
|
role: 'assistant',
|
|
content: assistantContent
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
res.status(response.status).json(data);
|
|
} catch (error) {
|
|
console.error("Claude API error:", error.message);
|
|
res.status(500).json({ error: "Failed to proxy request to Claude API" });
|
|
}
|
|
});
|
|
|
|
export default router;
|