- 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>
200 lines
4.4 KiB
JavaScript
200 lines
4.4 KiB
JavaScript
/**
|
|
* In-memory session store for diagram chat sessions.
|
|
* Stores conversation history and entity snapshots.
|
|
*/
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
// Session structure:
|
|
// {
|
|
// id: string,
|
|
// diagramId: string,
|
|
// conversationHistory: Array<{role, content, toolResults?, timestamp}>,
|
|
// entities: Array<{id, template, text, color, position}>,
|
|
// cameraPosition: { position: {x,y,z}, forward: {x,y,z}, groundForward: {x,y,z}, groundRight: {x,y,z} },
|
|
// preferences: { useDefaultConnectionLabels?: boolean },
|
|
// createdAt: Date,
|
|
// lastAccess: Date
|
|
// }
|
|
|
|
const sessions = new Map();
|
|
|
|
// Session timeout (1 hour of inactivity)
|
|
const SESSION_TIMEOUT_MS = 60 * 60 * 1000;
|
|
|
|
/**
|
|
* Create a new session for a diagram
|
|
*/
|
|
export function createSession(diagramId) {
|
|
const id = uuidv4();
|
|
const session = {
|
|
id,
|
|
diagramId,
|
|
conversationHistory: [],
|
|
entities: [],
|
|
cameraPosition: null,
|
|
preferences: {},
|
|
createdAt: new Date(),
|
|
lastAccess: new Date()
|
|
};
|
|
sessions.set(id, session);
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Get a session by ID
|
|
*/
|
|
export function getSession(sessionId) {
|
|
const session = sessions.get(sessionId);
|
|
if (session) {
|
|
session.lastAccess = new Date();
|
|
}
|
|
return session || null;
|
|
}
|
|
|
|
/**
|
|
* Find existing session for a diagram
|
|
*/
|
|
export function findSessionByDiagram(diagramId) {
|
|
for (const [, session] of sessions) {
|
|
if (session.diagramId === diagramId) {
|
|
session.lastAccess = new Date();
|
|
return session;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Update entities snapshot for a session
|
|
*/
|
|
export function syncEntities(sessionId, entities) {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) return null;
|
|
|
|
session.entities = entities;
|
|
session.lastAccess = new Date();
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Update camera position for a session
|
|
*/
|
|
export function syncCameraPosition(sessionId, cameraPosition) {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) return null;
|
|
|
|
session.cameraPosition = cameraPosition;
|
|
session.lastAccess = new Date();
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Add a message to conversation history
|
|
*/
|
|
export function addMessage(sessionId, message) {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) return null;
|
|
|
|
session.conversationHistory.push({
|
|
...message,
|
|
timestamp: new Date()
|
|
});
|
|
session.lastAccess = new Date();
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Get conversation history for API calls (formatted for Claude)
|
|
*/
|
|
export function getConversationForAPI(sessionId) {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) return [];
|
|
|
|
// Convert to Claude message format
|
|
return session.conversationHistory.map(msg => ({
|
|
role: msg.role,
|
|
content: msg.content
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Clear conversation history but keep session
|
|
*/
|
|
export function clearHistory(sessionId) {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) return null;
|
|
|
|
session.conversationHistory = [];
|
|
session.lastAccess = new Date();
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Delete a session
|
|
*/
|
|
export function deleteSession(sessionId) {
|
|
return sessions.delete(sessionId);
|
|
}
|
|
|
|
/**
|
|
* Clean up expired sessions
|
|
*/
|
|
export function cleanupExpiredSessions() {
|
|
const now = Date.now();
|
|
for (const [id, session] of sessions) {
|
|
if (now - session.lastAccess.getTime() > SESSION_TIMEOUT_MS) {
|
|
sessions.delete(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run cleanup every 15 minutes
|
|
setInterval(cleanupExpiredSessions, 15 * 60 * 1000);
|
|
|
|
/**
|
|
* Get session stats (for debugging)
|
|
*/
|
|
export function getStats(includeDetails = false) {
|
|
return {
|
|
activeSessions: sessions.size,
|
|
sessions: Array.from(sessions.values()).map(s => ({
|
|
id: s.id,
|
|
diagramId: s.diagramId,
|
|
messageCount: s.conversationHistory.length,
|
|
entityCount: s.entities.length,
|
|
lastAccess: s.lastAccess,
|
|
// Include full details if requested
|
|
...(includeDetails && {
|
|
entities: s.entities,
|
|
conversationHistory: s.conversationHistory
|
|
})
|
|
}))
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get session preferences
|
|
*/
|
|
export function getPreferences(sessionId) {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) return null;
|
|
session.lastAccess = new Date();
|
|
return session.preferences || {};
|
|
}
|
|
|
|
/**
|
|
* Set a session preference
|
|
*/
|
|
export function setPreference(sessionId, key, value) {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) return null;
|
|
|
|
if (!session.preferences) {
|
|
session.preferences = {};
|
|
}
|
|
session.preferences[key] = value;
|
|
session.lastAccess = new Date();
|
|
return session.preferences;
|
|
}
|