feat(dashboard): Enhanced chat, spec management, and Claude integration
Backend: - spec.py: New AtomizerSpec REST API endpoints - spec_manager.py: SpecManager service for unified config - interview_engine.py: Study creation interview logic - claude.py: Enhanced Claude API with context - optimization.py: Extended optimization endpoints - context_builder.py, session_manager.py: Improved services Frontend: - Chat components: Enhanced message rendering, tool call cards - Hooks: useClaudeCode, useSpecWebSocket, improved useChat - Pages: Updated Dashboard, Analysis, Insights, Setup, Home - Components: ParallelCoordinatesPlot, ParetoPlot improvements - App.tsx: Route updates for canvas/studio Infrastructure: - vite.config.ts: Build configuration updates - start/stop-dashboard.bat: Script improvements
This commit is contained in:
@@ -219,6 +219,18 @@ class SessionManager:
|
||||
full_response = result["stdout"] or ""
|
||||
|
||||
if full_response:
|
||||
# Check if response contains canvas modifications (from MCP tools)
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
modifications = self._extract_canvas_modifications(full_response)
|
||||
logger.info(f"[SEND_MSG] Found {len(modifications)} canvas modifications to send")
|
||||
|
||||
for mod in modifications:
|
||||
logger.info(f"[SEND_MSG] Sending canvas_modification: {mod.get('action')} {mod.get('nodeType')}")
|
||||
yield {"type": "canvas_modification", "modification": mod}
|
||||
|
||||
# Always send the text response
|
||||
yield {"type": "text", "content": full_response}
|
||||
|
||||
if result["returncode"] != 0 and result["stderr"]:
|
||||
@@ -292,6 +304,90 @@ class SessionManager:
|
||||
**({} if not db_record else {"db_record": db_record}),
|
||||
}
|
||||
|
||||
def _extract_canvas_modifications(self, response: str) -> List[Dict]:
|
||||
"""
|
||||
Extract canvas modification objects from Claude's response.
|
||||
|
||||
MCP tools like canvas_add_node return JSON with a 'modification' field.
|
||||
This method finds and extracts those modifications so the frontend can apply them.
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
modifications = []
|
||||
|
||||
# Debug: log what we're searching
|
||||
logger.info(f"[CANVAS_MOD] Searching response ({len(response)} chars) for modifications")
|
||||
|
||||
# Check if "modification" even exists in the response
|
||||
if '"modification"' not in response:
|
||||
logger.info("[CANVAS_MOD] No 'modification' key found in response")
|
||||
return modifications
|
||||
|
||||
try:
|
||||
# Method 1: Look for JSON in code fences
|
||||
code_block_pattern = r'```(?:json)?\s*([\s\S]*?)```'
|
||||
for match in re.finditer(code_block_pattern, response):
|
||||
block_content = match.group(1).strip()
|
||||
try:
|
||||
obj = json.loads(block_content)
|
||||
if isinstance(obj, dict) and 'modification' in obj:
|
||||
logger.info(f"[CANVAS_MOD] Found modification in code fence: {obj['modification']}")
|
||||
modifications.append(obj['modification'])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Method 2: Find JSON objects using proper brace matching
|
||||
# This handles nested objects correctly
|
||||
i = 0
|
||||
while i < len(response):
|
||||
if response[i] == '{':
|
||||
# Found a potential JSON start, find matching close
|
||||
brace_count = 1
|
||||
j = i + 1
|
||||
in_string = False
|
||||
escape_next = False
|
||||
|
||||
while j < len(response) and brace_count > 0:
|
||||
char = response[j]
|
||||
|
||||
if escape_next:
|
||||
escape_next = False
|
||||
elif char == '\\':
|
||||
escape_next = True
|
||||
elif char == '"' and not escape_next:
|
||||
in_string = not in_string
|
||||
elif not in_string:
|
||||
if char == '{':
|
||||
brace_count += 1
|
||||
elif char == '}':
|
||||
brace_count -= 1
|
||||
j += 1
|
||||
|
||||
if brace_count == 0:
|
||||
potential_json = response[i:j]
|
||||
try:
|
||||
obj = json.loads(potential_json)
|
||||
if isinstance(obj, dict) and 'modification' in obj:
|
||||
mod = obj['modification']
|
||||
# Avoid duplicates
|
||||
if mod not in modifications:
|
||||
logger.info(f"[CANVAS_MOD] Found inline modification: action={mod.get('action')}, nodeType={mod.get('nodeType')}")
|
||||
modifications.append(mod)
|
||||
except json.JSONDecodeError as e:
|
||||
# Not valid JSON, skip
|
||||
pass
|
||||
i = j
|
||||
else:
|
||||
i += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[CANVAS_MOD] Error extracting modifications: {e}")
|
||||
|
||||
logger.info(f"[CANVAS_MOD] Extracted {len(modifications)} modification(s)")
|
||||
return modifications
|
||||
|
||||
def _build_mcp_config(self, mode: Literal["user", "power"]) -> dict:
|
||||
"""Build MCP configuration for Claude"""
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user