Add camera position tool and fix entity modification bugs

- Add get_camera_position tool for positioning entities relative to user view
- Fix color change causing entities to disappear (dispose mesh before rebuild)
- Fix connections being lost when modifying entities (defer disposal, let
  scene observer re-find meshes after they're recreated with same ID)
- Add position and color setters to DiagramObject for real-time updates
- Add debug logging to diagramAI and claude.js for troubleshooting

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-12-20 15:59:30 -06:00
parent 2fd87b2d14
commit c9dc61b918
6 changed files with 256 additions and 44 deletions

View File

@ -23,9 +23,13 @@ function buildEntityContext(entities) {
// Express 5 uses named parameters for wildcards // Express 5 uses named parameters for wildcards
router.post("/*path", async (req, res) => { router.post("/*path", async (req, res) => {
const requestStart = Date.now();
console.log(`[Claude API] ========== REQUEST START ==========`);
const apiKey = process.env.ANTHROPIC_API_KEY; const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) { if (!apiKey) {
console.error(`[Claude API] ERROR: API key not configured`);
return res.status(500).json({ error: "API key not configured" }); return res.status(500).json({ error: "API key not configured" });
} }
@ -33,20 +37,24 @@ router.post("/*path", async (req, res) => {
// Express 5 returns path segments as an array // Express 5 returns path segments as an array
const pathParam = req.params.path; const pathParam = req.params.path;
const path = "/" + (Array.isArray(pathParam) ? pathParam.join("/") : pathParam || ""); const path = "/" + (Array.isArray(pathParam) ? pathParam.join("/") : pathParam || "");
console.log(`[Claude API] Path: ${path}`);
// Check for session-based request // Check for session-based request
const { sessionId, ...requestBody } = req.body; const { sessionId, ...requestBody } = req.body;
let modifiedBody = requestBody; let modifiedBody = requestBody;
console.log(`[Claude API] Session ID: ${sessionId || 'none'}`);
console.log(`[Claude API] Model: ${requestBody.model}`);
console.log(`[Claude API] Messages count: ${requestBody.messages?.length || 0}`);
if (sessionId) { if (sessionId) {
const session = getSession(sessionId); const session = getSession(sessionId);
if (session) { if (session) {
console.log(`[Claude API] Session ${sessionId}: ${session.entities.length} entities, ${session.conversationHistory.length} messages in history`); console.log(`[Claude API] Session found: ${session.entities.length} entities, ${session.conversationHistory.length} messages in history`);
// Inject entity context into system prompt // Inject entity context into system prompt
if (modifiedBody.system) { if (modifiedBody.system) {
const entityContext = buildEntityContext(session.entities); const entityContext = buildEntityContext(session.entities);
console.log(`[Claude API] Entity context:`, entityContext); console.log(`[Claude API] Entity context added (${entityContext.length} chars)`);
modifiedBody.system += entityContext; modifiedBody.system += entityContext;
} }
@ -57,14 +65,17 @@ router.post("/*path", async (req, res) => {
const currentContent = modifiedBody.messages[modifiedBody.messages.length - 1]?.content; const currentContent = modifiedBody.messages[modifiedBody.messages.length - 1]?.content;
const filteredHistory = historyMessages.filter(msg => msg.content !== currentContent); const filteredHistory = historyMessages.filter(msg => msg.content !== currentContent);
modifiedBody.messages = [...filteredHistory, ...modifiedBody.messages]; modifiedBody.messages = [...filteredHistory, ...modifiedBody.messages];
console.log(`[Claude API] Merged ${filteredHistory.length} history messages with ${modifiedBody.messages.length - filteredHistory.length} new messages`); console.log(`[Claude API] Merged ${filteredHistory.length} history + ${modifiedBody.messages.length - filteredHistory.length} new = ${modifiedBody.messages.length} total messages`);
} }
} else { } else {
console.log(`[Claude API] Session ${sessionId} not found`); console.log(`[Claude API] WARNING: Session ${sessionId} not found`);
} }
} }
try { try {
console.log(`[Claude API] Sending request to Anthropic API...`);
const fetchStart = Date.now();
const response = await fetch(`${ANTHROPIC_API_URL}${path}`, { const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
method: "POST", method: "POST",
headers: { headers: {
@ -75,7 +86,16 @@ router.post("/*path", async (req, res) => {
body: JSON.stringify(modifiedBody), body: JSON.stringify(modifiedBody),
}); });
const fetchDuration = Date.now() - fetchStart;
console.log(`[Claude API] Response received in ${fetchDuration}ms, status: ${response.status}`);
console.log(`[Claude API] Parsing response JSON...`);
const data = await response.json(); const data = await response.json();
console.log(`[Claude API] Response parsed. Stop reason: ${data.stop_reason}, content blocks: ${data.content?.length || 0}`);
if (data.error) {
console.error(`[Claude API] API returned error:`, data.error);
}
// If session exists and response is successful, store messages // If session exists and response is successful, store messages
if (sessionId && response.ok && data.content) { if (sessionId && response.ok && data.content) {
@ -88,6 +108,7 @@ router.post("/*path", async (req, res) => {
role: 'user', role: 'user',
content: userMessage.content content: userMessage.content
}); });
console.log(`[Claude API] Stored user message to session`);
} }
// Store the assistant response (text only, not tool use blocks) // Store the assistant response (text only, not tool use blocks)
@ -101,14 +122,21 @@ router.post("/*path", async (req, res) => {
role: 'assistant', role: 'assistant',
content: assistantContent content: assistantContent
}); });
console.log(`[Claude API] Stored assistant response to session (${assistantContent.length} chars)`);
} }
} }
} }
const totalDuration = Date.now() - requestStart;
console.log(`[Claude API] ========== REQUEST COMPLETE (${totalDuration}ms) ==========`);
res.status(response.status).json(data); res.status(response.status).json(data);
} catch (error) { } catch (error) {
console.error("Claude API error:", error.message); const totalDuration = Date.now() - requestStart;
res.status(500).json({ error: "Failed to proxy request to Claude API" }); console.error(`[Claude API] ========== REQUEST FAILED (${totalDuration}ms) ==========`);
console.error(`[Claude API] Error:`, error);
console.error(`[Claude API] Error message:`, error.message);
console.error(`[Claude API] Error stack:`, error.stack);
res.status(500).json({ error: "Failed to proxy request to Claude API", details: error.message });
} }
}); });

View File

@ -150,16 +150,19 @@ export class DiagramManager {
if (entity) { if (entity) {
const diagramObject = this._diagramObjects.get(entity.id); const diagramObject = this._diagramObjects.get(entity.id);
if (diagramObject) { if (diagramObject) {
// Apply updates using setters (each setter handles its own DB notification)
if (updates.text !== undefined) { if (updates.text !== undefined) {
diagramObject.text = updates.text; diagramObject.text = updates.text;
} }
// Note: color and position updates would require additional DiagramObject methods if (updates.color !== undefined) {
const updatedEntity = {...entity, ...updates}; diagramObject.color = updates.color;
this.onDiagramEventObservable.notifyObservers({ }
type: DiagramEventType.MODIFY, if (updates.position !== undefined) {
entity: updatedEntity diagramObject.position = updates.position;
}, DiagramEventObserverMask.TO_DB); }
} }
} else {
this._logger.warn('chatModifyEntity: entity not found:', target);
} }
}); });
@ -216,6 +219,30 @@ export class DiagramManager {
this._logger.debug(`Cleared ${entitiesToRemove.length} entities`); this._logger.debug(`Cleared ${entitiesToRemove.length} entities`);
}); });
// Get current camera position and orientation
document.addEventListener('chatGetCamera', () => {
this._logger.debug('chatGetCamera');
const camera = this._scene.activeCamera;
if (!camera) {
this._logger.warn('No active camera found');
return;
}
const position = camera.globalPosition;
const forward = camera.getForwardRay(1).direction;
const up = camera.upVector || {x: 0, y: 1, z: 0};
const responseEvent = new CustomEvent('chatGetCameraResponse', {
detail: {
position: {x: position.x, y: position.y, z: position.z},
forward: {x: forward.x, y: forward.y, z: forward.z},
up: {x: up.x, y: up.y, z: up.z}
},
bubbles: true
});
document.dispatchEvent(responseEvent);
});
this._logger.debug("DiagramManager constructed"); this._logger.debug("DiagramManager constructed");
} }

View File

@ -142,6 +142,56 @@ export class DiagramObject {
return this._diagramEntity; return this._diagramEntity;
} }
public set position(value: { x: number; y: number; z: number }) {
if (this._baseTransform) {
this._baseTransform.position = new Vector3(value.x, value.y, value.z);
if (this._diagramEntity) {
this._diagramEntity.position = value;
}
this._eventObservable.notifyObservers({
type: DiagramEventType.MODIFY,
entity: this._diagramEntity
}, DiagramEventObserverMask.TO_DB);
}
}
public set color(value: string) {
if (!this._diagramEntity || this._diagramEntity.color === value) {
return;
}
this._logger.debug('Changing color from', this._diagramEntity.color, 'to', value);
// Update the entity color
this._diagramEntity.color = value;
// Rebuild mesh with new color (since instances share materials)
// Must dispose old mesh FIRST, otherwise buildMeshFromDiagramEntity
// finds it by ID and returns the same mesh (which we then dispose!)
if (this._mesh) {
const actionManager = this._mesh.actionManager;
this._mesh.dispose();
this._mesh = null;
this._mesh = buildMeshFromDiagramEntity(this._diagramEntity, this._scene);
if (this._mesh) {
this._mesh.setParent(this._baseTransform);
this._mesh.position = Vector3.Zero();
this._mesh.rotation = Vector3.Zero();
if (actionManager) {
this._mesh.actionManager = actionManager;
}
} else {
this._logger.error('Failed to rebuild mesh with new color');
}
}
this._eventObservable.notifyObservers({
type: DiagramEventType.MODIFY,
entity: this._diagramEntity
}, DiagramEventObserverMask.TO_DB);
}
public set text(value: string) { public set text(value: string) {
if (this._label) { if (this._label) {
this._label.dispose(); this._label.dispose();
@ -269,30 +319,26 @@ export class DiagramObject {
if (!this._meshRemovedObserver) { if (!this._meshRemovedObserver) {
this._meshRemovedObserver = this._scene.onMeshRemovedObservable.add((mesh) => { this._meshRemovedObserver = this._scene.onMeshRemovedObservable.add((mesh) => {
if (mesh && mesh.id) { if (mesh && mesh.id) {
// When an endpoint mesh is removed, don't immediately dispose the connection.
// Instead, clear the mesh references and reset the timer. The scene observer
// will try to re-find the meshes (handles entity modification where mesh is
// disposed and recreated with same ID). If meshes can't be found after
// timeout, the scene observer will dispose the connection.
switch (mesh.id) { switch (mesh.id) {
case this._from: case this._from:
this._fromMesh = null; this._fromMesh = null;
this._lastFromPosition = null; this._lastFromPosition = null;
this._meshesPresent = false; this._meshesPresent = false;
this._eventObservable.notifyObservers({ this._observingStart = Date.now(); // Reset timeout
type: DiagramEventType.REMOVE,
entity: this._diagramEntity
}, DiagramEventObserverMask.ALL);
this.dispose();
break; break;
case this._to: case this._to:
this._toMesh = null; this._toMesh = null;
this._lastToPosition = null; this._lastToPosition = null;
this._meshesPresent = false; this._meshesPresent = false;
this._eventObservable.notifyObservers({ this._observingStart = Date.now(); // Reset timeout
type: DiagramEventType.REMOVE, break;
entity: this._diagramEntity
}, DiagramEventObserverMask.ALL);
this.dispose();
} }
} }
}, -1, false, this); }, -1, false, this);
} }
if (!this._sceneObserver) { if (!this._sceneObserver) {

View File

@ -1,6 +1,10 @@
import {ChatMessage, CreateSessionResponse, DiagramSession, DiagramToolCall, SessionEntity, SyncEntitiesResponse, ToolResult} from "../types/chatTypes"; import {ChatMessage, CreateSessionResponse, DiagramSession, DiagramToolCall, SessionEntity, SyncEntitiesResponse, ToolResult} from "../types/chatTypes";
import {clearDiagram, connectEntities, createEntity, listEntities, modifyEntity, removeEntity} from "./entityBridge"; import {clearDiagram, connectEntities, createEntity, getCameraPosition, listEntities, modifyEntity, removeEntity} from "./entityBridge";
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import log from 'loglevel';
const logger = log.getLogger('diagramAI');
logger.setLevel('debug');
// Session management // Session management
let currentSessionId: string | null = null; let currentSessionId: string | null = null;
@ -218,6 +222,14 @@ const TOOLS = [
}, },
required: ["confirmed"] required: ["confirmed"]
} }
},
{
name: "get_camera_position",
description: "Get the current camera/viewer position and orientation in the 3D scene. Use this to understand where the user is looking and to position new entities relative to their view. Returns position (x, y, z) and forward direction vector.",
input_schema: {
type: "object",
properties: {}
}
} }
]; ];
@ -242,33 +254,47 @@ interface ClaudeResponse {
} }
async function executeToolCall(toolCall: DiagramToolCall): Promise<ToolResult> { async function executeToolCall(toolCall: DiagramToolCall): Promise<ToolResult> {
logger.debug('[executeToolCall] Executing:', toolCall.name, toolCall.input);
let result: ToolResult;
switch (toolCall.name) { switch (toolCall.name) {
case 'create_entity': case 'create_entity':
return createEntity(toolCall.input); result = createEntity(toolCall.input);
break;
case 'connect_entities': case 'connect_entities':
return await connectEntities(toolCall.input); result = await connectEntities(toolCall.input);
break;
case 'remove_entity': case 'remove_entity':
return removeEntity(toolCall.input); result = removeEntity(toolCall.input);
break;
case 'modify_entity': case 'modify_entity':
return modifyEntity(toolCall.input); result = modifyEntity(toolCall.input);
break;
case 'list_entities': case 'list_entities':
return await listEntities(); result = await listEntities();
break;
case 'clear_diagram': case 'clear_diagram':
const result = await clearDiagram(toolCall.input); result = await clearDiagram(toolCall.input);
// If successful, also clear the session (history and entities) // If successful, also clear the session (history and entities)
if (result.success && currentSessionId) { if (result.success && currentSessionId) {
await clearSessionHistory(); await clearSessionHistory();
// Sync empty entity list to clear server-side entity cache // Sync empty entity list to clear server-side entity cache
await syncEntitiesToSession([]); await syncEntitiesToSession([]);
} }
return result; break;
case 'get_camera_position':
result = await getCameraPosition();
break;
default: default:
return { result = {
toolName: 'unknown', toolName: 'unknown',
success: false, success: false,
message: 'Unknown tool' message: 'Unknown tool'
}; };
} }
logger.debug('[executeToolCall] Result:', result);
return result;
} }
export async function sendMessage( export async function sendMessage(
@ -276,6 +302,9 @@ export async function sendMessage(
conversationHistory: ChatMessage[], conversationHistory: ChatMessage[],
onToolResult?: (result: ToolResult) => void onToolResult?: (result: ToolResult) => void
): Promise<{ response: string; toolResults: ToolResult[] }> { ): Promise<{ response: string; toolResults: ToolResult[] }> {
logger.debug('[sendMessage] Starting with message:', userMessage);
logger.debug('[sendMessage] Session ID:', currentSessionId);
// When using sessions, we don't need to send full history - server manages it // When using sessions, we don't need to send full history - server manages it
// Just send the new message // Just send the new message
const messages: ClaudeMessage[] = currentSessionId const messages: ClaudeMessage[] = currentSessionId
@ -291,42 +320,60 @@ export async function sendMessage(
messages.push({role: 'user', content: userMessage}); messages.push({role: 'user', content: userMessage});
} }
logger.debug('[sendMessage] Messages to send:', messages.length);
const allToolResults: ToolResult[] = []; const allToolResults: ToolResult[] = [];
let finalResponse = ''; let finalResponse = '';
let continueLoop = true; let continueLoop = true;
let loopCount = 0;
while (continueLoop) { while (continueLoop) {
loopCount++;
logger.debug(`[sendMessage] Loop iteration ${loopCount}`);
const requestBody = {
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: SYSTEM_PROMPT,
tools: TOOLS,
messages,
...(currentSessionId && { sessionId: currentSessionId })
};
logger.debug('[sendMessage] Request body:', JSON.stringify(requestBody, null, 2).substring(0, 500) + '...');
const response = await fetch('/api/claude/v1/messages', { const response = await fetch('/api/claude/v1/messages', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify(requestBody)
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: SYSTEM_PROMPT,
tools: TOOLS,
messages,
// Include sessionId if we have an active session
...(currentSessionId && { sessionId: currentSessionId })
})
}); });
logger.debug('[sendMessage] Response status:', response.status);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
logger.error('[sendMessage] API error:', response.status, errorText);
throw new Error(`API error: ${response.status} - ${errorText}`); throw new Error(`API error: ${response.status} - ${errorText}`);
} }
const data: ClaudeResponse = await response.json(); const data: ClaudeResponse = await response.json();
logger.debug('[sendMessage] Response data:', JSON.stringify(data, null, 2).substring(0, 500) + '...');
logger.debug('[sendMessage] Stop reason:', data.stop_reason);
const textBlocks = data.content.filter(b => b.type === 'text'); const textBlocks = data.content.filter(b => b.type === 'text');
const toolBlocks = data.content.filter(b => b.type === 'tool_use'); const toolBlocks = data.content.filter(b => b.type === 'tool_use');
logger.debug('[sendMessage] Text blocks:', textBlocks.length, 'Tool blocks:', toolBlocks.length);
if (textBlocks.length > 0) { if (textBlocks.length > 0) {
finalResponse = textBlocks.map(b => b.text).join('\n'); finalResponse = textBlocks.map(b => b.text).join('\n');
logger.debug('[sendMessage] Final response:', finalResponse.substring(0, 200) + '...');
} }
if (data.stop_reason === 'tool_use' && toolBlocks.length > 0) { if (data.stop_reason === 'tool_use' && toolBlocks.length > 0) {
logger.debug('[sendMessage] Processing tool calls...');
messages.push({ messages.push({
role: 'assistant', role: 'assistant',
content: data.content content: data.content
@ -335,12 +382,14 @@ export async function sendMessage(
const toolResults: ClaudeContentBlock[] = []; const toolResults: ClaudeContentBlock[] = [];
for (const toolBlock of toolBlocks) { for (const toolBlock of toolBlocks) {
logger.debug('[sendMessage] Tool call:', toolBlock.name, JSON.stringify(toolBlock.input));
const toolCall: DiagramToolCall = { const toolCall: DiagramToolCall = {
name: toolBlock.name as DiagramToolCall['name'], name: toolBlock.name as DiagramToolCall['name'],
input: toolBlock.input as DiagramToolCall['input'] input: toolBlock.input as DiagramToolCall['input']
}; };
const result = await executeToolCall(toolCall); const result = await executeToolCall(toolCall);
logger.debug('[sendMessage] Tool result:', result);
allToolResults.push(result); allToolResults.push(result);
onToolResult?.(result); onToolResult?.(result);
@ -355,11 +404,14 @@ export async function sendMessage(
role: 'user', role: 'user',
content: toolResults content: toolResults
}); });
logger.debug('[sendMessage] Added tool results, continuing loop...');
} else { } else {
logger.debug('[sendMessage] No more tool calls, ending loop');
continueLoop = false; continueLoop = false;
} }
} }
logger.debug('[sendMessage] Complete. Final response length:', finalResponse.length, 'Tool results:', allToolResults.length);
return {response: finalResponse, toolResults: allToolResults}; return {response: finalResponse, toolResults: allToolResults};
} }

View File

@ -10,6 +10,10 @@ import {
ToolResult ToolResult
} from "../types/chatTypes"; } from "../types/chatTypes";
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import log from 'loglevel';
const logger = log.getLogger('entityBridge');
logger.setLevel('debug');
function resolveColor(color?: string): string { function resolveColor(color?: string): string {
if (!color) return '#0000ff'; if (!color) return '#0000ff';
@ -32,11 +36,13 @@ interface ResolvedEntity {
* Resolve an entity label or ID to actual entity ID and label * Resolve an entity label or ID to actual entity ID and label
*/ */
function resolveEntity(target: string): Promise<ResolvedEntity> { function resolveEntity(target: string): Promise<ResolvedEntity> {
logger.debug('[resolveEntity] Resolving:', target);
return new Promise((resolve) => { return new Promise((resolve) => {
const requestId = 'req-' + Date.now() + '-' + Math.random(); const requestId = 'req-' + Date.now() + '-' + Math.random();
const responseHandler = (e: CustomEvent) => { const responseHandler = (e: CustomEvent) => {
if (e.detail.requestId !== requestId) return; if (e.detail.requestId !== requestId) return;
logger.debug('[resolveEntity] Got response:', e.detail);
document.removeEventListener('chatResolveEntityResponse', responseHandler as EventListener); document.removeEventListener('chatResolveEntityResponse', responseHandler as EventListener);
resolve({ resolve({
id: e.detail.entityId, id: e.detail.entityId,
@ -53,6 +59,7 @@ function resolveEntity(target: string): Promise<ResolvedEntity> {
document.dispatchEvent(event); document.dispatchEvent(event);
setTimeout(() => { setTimeout(() => {
logger.warn('[resolveEntity] Timeout resolving:', target);
document.removeEventListener('chatResolveEntityResponse', responseHandler as EventListener); document.removeEventListener('chatResolveEntityResponse', responseHandler as EventListener);
resolve({id: null, label: null}); resolve({id: null, label: null});
}, 5000); }, 5000);
@ -60,6 +67,7 @@ function resolveEntity(target: string): Promise<ResolvedEntity> {
} }
export function createEntity(params: CreateEntityParams): ToolResult { export function createEntity(params: CreateEntityParams): ToolResult {
logger.debug('[createEntity] Creating entity:', params);
const id = 'id' + uuidv4(); const id = 'id' + uuidv4();
const template = SHAPE_TO_TEMPLATE[params.shape]; const template = SHAPE_TO_TEMPLATE[params.shape];
const color = resolveColor(params.color); const color = resolveColor(params.color);
@ -76,24 +84,29 @@ export function createEntity(params: CreateEntityParams): ToolResult {
scale: {x: 0.1, y: 0.1, z: 0.1}, scale: {x: 0.1, y: 0.1, z: 0.1},
}; };
logger.debug('[createEntity] Dispatching chatCreateEntity event:', entity);
const event = new CustomEvent('chatCreateEntity', { const event = new CustomEvent('chatCreateEntity', {
detail: {entity}, detail: {entity},
bubbles: true bubbles: true
}); });
document.dispatchEvent(event); document.dispatchEvent(event);
return { const result = {
toolName: 'create_entity', toolName: 'create_entity',
success: true, success: true,
message: `Created ${params.shape}${params.label ? ` labeled "${params.label}"` : ''} at position (${position.x.toFixed(1)}, ${position.y.toFixed(1)}, ${position.z.toFixed(1)})`, message: `Created ${params.shape}${params.label ? ` labeled "${params.label}"` : ''} at position (${position.x.toFixed(1)}, ${position.y.toFixed(1)}, ${position.z.toFixed(1)})`,
entityId: id entityId: id
}; };
logger.debug('[createEntity] Done:', result);
return result;
} }
export async function connectEntities(params: ConnectEntitiesParams): Promise<ToolResult> { export async function connectEntities(params: ConnectEntitiesParams): Promise<ToolResult> {
logger.debug('[connectEntities] Connecting:', params);
// Resolve labels to actual entity IDs and get their labels // Resolve labels to actual entity IDs and get their labels
const fromEntity = await resolveEntity(params.from); const fromEntity = await resolveEntity(params.from);
const toEntity = await resolveEntity(params.to); const toEntity = await resolveEntity(params.to);
logger.debug('[connectEntities] Resolved from:', fromEntity, 'to:', toEntity);
if (!fromEntity.id) { if (!fromEntity.id) {
return { return {
@ -184,6 +197,7 @@ export function modifyEntity(params: ModifyEntityParams): ToolResult {
} }
export function listEntities(): Promise<ToolResult> { export function listEntities(): Promise<ToolResult> {
logger.debug('[listEntities] Listing entities...');
return new Promise((resolve) => { return new Promise((resolve) => {
const responseHandler = (e: CustomEvent) => { const responseHandler = (e: CustomEvent) => {
document.removeEventListener('chatListEntitiesResponse', responseHandler as EventListener); document.removeEventListener('chatListEntitiesResponse', responseHandler as EventListener);
@ -193,6 +207,7 @@ export function listEntities(): Promise<ToolResult> {
text: string; text: string;
position: { x: number; y: number; z: number } position: { x: number; y: number; z: number }
}>; }>;
logger.debug('[listEntities] Got response, entities:', entities.length);
if (entities.length === 0) { if (entities.length === 0) {
resolve({ resolve({
@ -221,6 +236,7 @@ export function listEntities(): Promise<ToolResult> {
document.dispatchEvent(event); document.dispatchEvent(event);
setTimeout(() => { setTimeout(() => {
logger.warn('[listEntities] Timeout waiting for response');
document.removeEventListener('chatListEntitiesResponse', responseHandler as EventListener); document.removeEventListener('chatListEntitiesResponse', responseHandler as EventListener);
resolve({ resolve({
toolName: 'list_entities', toolName: 'list_entities',
@ -294,3 +310,45 @@ export async function clearDiagram(params: ClearDiagramParams): Promise<ToolResu
message: `Cleared ${entities.length} entities from the diagram.` message: `Cleared ${entities.length} entities from the diagram.`
}; };
} }
/**
* Get the current camera position and orientation
*/
export function getCameraPosition(): Promise<ToolResult> {
logger.debug('[getCameraPosition] Getting camera position...');
return new Promise((resolve) => {
const responseHandler = (e: CustomEvent) => {
document.removeEventListener('chatGetCameraResponse', responseHandler as EventListener);
const {position, forward, up} = e.detail;
logger.debug('[getCameraPosition] Got response:', e.detail);
const message = `Camera position: (${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)})
Forward direction: (${forward.x.toFixed(2)}, ${forward.y.toFixed(2)}, ${forward.z.toFixed(2)})
Up direction: (${up.x.toFixed(2)}, ${up.y.toFixed(2)}, ${up.z.toFixed(2)})
To place an entity in front of the camera, add the forward direction scaled by desired distance to the camera position.
Example: For an entity 2 units in front: position = (${(position.x + forward.x * 2).toFixed(2)}, ${(position.y + forward.y * 2).toFixed(2)}, ${(position.z + forward.z * 2).toFixed(2)})`;
resolve({
toolName: 'get_camera_position',
success: true,
message
});
};
document.addEventListener('chatGetCameraResponse', responseHandler as EventListener);
const event = new CustomEvent('chatGetCamera', {bubbles: true});
document.dispatchEvent(event);
setTimeout(() => {
logger.warn('[getCameraPosition] Timeout waiting for response');
document.removeEventListener('chatGetCameraResponse', responseHandler as EventListener);
resolve({
toolName: 'get_camera_position',
success: false,
message: 'Failed to get camera position (timeout)'
});
}, 5000);
});
}

View File

@ -52,7 +52,8 @@ export type DiagramToolCall =
| { name: 'remove_entity'; input: RemoveEntityParams } | { name: 'remove_entity'; input: RemoveEntityParams }
| { name: 'modify_entity'; input: ModifyEntityParams } | { name: 'modify_entity'; input: ModifyEntityParams }
| { name: 'list_entities'; input: Record<string, never> } | { name: 'list_entities'; input: Record<string, never> }
| { name: 'clear_diagram'; input: ClearDiagramParams }; | { name: 'clear_diagram'; input: ClearDiagramParams }
| { name: 'get_camera_position'; input: Record<string, never> };
export const SHAPE_TO_TEMPLATE: Record<CreateEntityParams['shape'], DiagramTemplates> = { export const SHAPE_TO_TEMPLATE: Record<CreateEntityParams['shape'], DiagramTemplates> = {
box: DiagramTemplates.BOX, box: DiagramTemplates.BOX,