- Migrate to LangChain for model abstraction (@langchain/anthropic, @langchain/ollama) - Add custom ChatCloudflare class for Cloudflare Workers AI - Simplify API routes using unified LangChain interface - Add session preferences API for storing user settings - Add connection label preference (ask user once, remember for session) - Add shape modification support (change entity shapes via AI) - Add template setter to DiagramObject for shape changes - Improve entity inference with fuzzy matching - Map colors to 16 toolbox palette colors - Limit conversation history to last 6 messages - Fix model switching to accept display names Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
235 lines
5.7 KiB
JavaScript
235 lines
5.7 KiB
JavaScript
import { Router } from "express";
|
|
import {
|
|
createSession,
|
|
getSession,
|
|
findSessionByDiagram,
|
|
syncEntities,
|
|
syncCameraPosition,
|
|
addMessage,
|
|
clearHistory,
|
|
deleteSession,
|
|
getStats,
|
|
getPreferences,
|
|
setPreference
|
|
} from "../services/sessionStore.js";
|
|
import { getSessionUsage, getGlobalUsage, formatCost } from "../services/usageTracker.js";
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* GET /api/session/debug/stats
|
|
* Get session statistics (for debugging)
|
|
* Query params:
|
|
* - details=true: Include full entity and conversation data
|
|
* NOTE: Must be before /:id routes to avoid matching "debug" as an id
|
|
*/
|
|
router.get("/debug/stats", (req, res) => {
|
|
const includeDetails = req.query.details === 'true';
|
|
const stats = getStats(includeDetails);
|
|
console.log('[Session Debug] Stats requested:', JSON.stringify(stats, null, 2));
|
|
res.json(stats);
|
|
});
|
|
|
|
/**
|
|
* GET /api/session/usage/global
|
|
* Get global token usage and cost statistics
|
|
* NOTE: Must be before /:id routes
|
|
*/
|
|
router.get("/usage/global", (req, res) => {
|
|
const usage = getGlobalUsage();
|
|
res.json({
|
|
...usage,
|
|
totalCostFormatted: formatCost(usage.totalCost),
|
|
uptimeFormatted: `${Math.round(usage.uptime / 1000 / 60)} minutes`
|
|
});
|
|
});
|
|
|
|
/**
|
|
* GET /api/session/:id/usage
|
|
* Get token usage and cost for a specific session
|
|
*/
|
|
router.get("/:id/usage", (req, res) => {
|
|
const usage = getSessionUsage(req.params.id);
|
|
|
|
if (!usage) {
|
|
return res.status(404).json({ error: "No usage data for session" });
|
|
}
|
|
|
|
res.json({
|
|
...usage,
|
|
totalCostFormatted: formatCost(usage.totalCost)
|
|
});
|
|
});
|
|
|
|
/**
|
|
* POST /api/session/create
|
|
* Create a new session or return existing one for a diagram
|
|
*/
|
|
router.post("/create", (req, res) => {
|
|
const { diagramId } = req.body;
|
|
|
|
if (!diagramId) {
|
|
return res.status(400).json({ error: "diagramId is required" });
|
|
}
|
|
|
|
// Check for existing session
|
|
let session = findSessionByDiagram(diagramId);
|
|
if (session) {
|
|
console.log(`[Session] Resuming existing session ${session.id} for diagram ${diagramId} (${session.conversationHistory.length} messages, ${session.entities.length} entities)`);
|
|
return res.json({
|
|
session,
|
|
isNew: false
|
|
});
|
|
}
|
|
|
|
// Create new session
|
|
session = createSession(diagramId);
|
|
console.log(`[Session] Created new session ${session.id} for diagram ${diagramId}`);
|
|
res.json({
|
|
session,
|
|
isNew: true
|
|
});
|
|
});
|
|
|
|
/**
|
|
* GET /api/session/:id
|
|
* Get session details including history
|
|
*/
|
|
router.get("/:id", (req, res) => {
|
|
const session = getSession(req.params.id);
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: "Session not found" });
|
|
}
|
|
|
|
res.json({ session });
|
|
});
|
|
|
|
/**
|
|
* PUT /api/session/:id/sync
|
|
* Sync entities from client to server
|
|
*/
|
|
router.put("/:id/sync", (req, res) => {
|
|
const { entities } = req.body;
|
|
|
|
if (!entities || !Array.isArray(entities)) {
|
|
return res.status(400).json({ error: "entities array is required" });
|
|
}
|
|
|
|
const session = syncEntities(req.params.id, entities);
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: "Session not found" });
|
|
}
|
|
|
|
console.log(`[Session ${req.params.id}] Synced ${entities.length} entities:`,
|
|
entities.map(e => `${e.text || '(no label)'} (${e.template})`).join(', ') || 'none');
|
|
|
|
res.json({ success: true, entityCount: entities.length });
|
|
});
|
|
|
|
/**
|
|
* PUT /api/session/:id/camera
|
|
* Sync camera position from client to server
|
|
*/
|
|
router.put("/:id/camera", (req, res) => {
|
|
const { cameraPosition } = req.body;
|
|
|
|
if (!cameraPosition) {
|
|
return res.status(400).json({ error: "cameraPosition is required" });
|
|
}
|
|
|
|
const session = syncCameraPosition(req.params.id, cameraPosition);
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: "Session not found" });
|
|
}
|
|
|
|
res.json({ success: true });
|
|
});
|
|
|
|
/**
|
|
* POST /api/session/:id/message
|
|
* Add a message to history (used after successful Claude response)
|
|
*/
|
|
router.post("/:id/message", (req, res) => {
|
|
const { role, content, toolResults } = req.body;
|
|
|
|
if (!role || !content) {
|
|
return res.status(400).json({ error: "role and content are required" });
|
|
}
|
|
|
|
const session = addMessage(req.params.id, { role, content, toolResults });
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: "Session not found" });
|
|
}
|
|
|
|
res.json({ success: true, messageCount: session.conversationHistory.length });
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/session/:id/history
|
|
* Clear conversation history
|
|
*/
|
|
router.delete("/:id/history", (req, res) => {
|
|
const session = clearHistory(req.params.id);
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: "Session not found" });
|
|
}
|
|
|
|
res.json({ success: true });
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/session/:id
|
|
* Delete a session entirely
|
|
*/
|
|
router.delete("/:id", (req, res) => {
|
|
const deleted = deleteSession(req.params.id);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: "Session not found" });
|
|
}
|
|
|
|
res.json({ success: true });
|
|
});
|
|
|
|
/**
|
|
* GET /api/session/:id/preferences
|
|
* Get session preferences
|
|
*/
|
|
router.get("/:id/preferences", (req, res) => {
|
|
const preferences = getPreferences(req.params.id);
|
|
|
|
if (preferences === null) {
|
|
return res.status(404).json({ error: "Session not found" });
|
|
}
|
|
|
|
res.json({ preferences });
|
|
});
|
|
|
|
/**
|
|
* PUT /api/session/:id/preferences
|
|
* Set a session preference
|
|
*/
|
|
router.put("/:id/preferences", (req, res) => {
|
|
const { key, value } = req.body;
|
|
|
|
if (!key) {
|
|
return res.status(400).json({ error: "key is required" });
|
|
}
|
|
|
|
const preferences = setPreference(req.params.id, key, value);
|
|
|
|
if (preferences === null) {
|
|
return res.status(404).json({ error: "Session not found" });
|
|
}
|
|
|
|
console.log(`[Session ${req.params.id}] Set preference: ${key} = ${value}`);
|
|
res.json({ success: true, preferences });
|
|
});
|
|
|
|
export default router;
|