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:
@@ -187,7 +187,15 @@ async def session_websocket(websocket: WebSocket, session_id: str):
|
||||
continue
|
||||
|
||||
# Get canvas state from message or use stored state
|
||||
canvas_state = data.get("canvas_state") or current_canvas_state
|
||||
msg_canvas = data.get("canvas_state")
|
||||
canvas_state = msg_canvas if msg_canvas is not None else current_canvas_state
|
||||
|
||||
# Debug logging
|
||||
if canvas_state:
|
||||
node_count = len(canvas_state.get("nodes", []))
|
||||
print(f"[Claude WS] Sending message with canvas state: {node_count} nodes")
|
||||
else:
|
||||
print("[Claude WS] Sending message WITHOUT canvas state")
|
||||
|
||||
async for chunk in manager.send_message(
|
||||
session_id,
|
||||
@@ -401,6 +409,175 @@ async def websocket_chat(websocket: WebSocket):
|
||||
pass
|
||||
|
||||
|
||||
# ========== POWER MODE: Direct API with Write Tools ==========
|
||||
|
||||
@router.websocket("/sessions/{session_id}/ws/power")
|
||||
async def power_mode_websocket(websocket: WebSocket, session_id: str):
|
||||
"""
|
||||
WebSocket for power mode chat using direct Anthropic API with write tools.
|
||||
|
||||
Unlike the regular /ws endpoint which uses Claude CLI + MCP,
|
||||
this uses AtomizerClaudeAgent directly with built-in write tools.
|
||||
This allows immediate modifications without permission prompts.
|
||||
|
||||
Message formats (client -> server):
|
||||
{"type": "message", "content": "user message"}
|
||||
{"type": "set_study", "study_id": "study_name"}
|
||||
{"type": "ping"}
|
||||
|
||||
Message formats (server -> client):
|
||||
{"type": "text", "content": "..."}
|
||||
{"type": "tool_call", "tool": "...", "input": {...}}
|
||||
{"type": "tool_result", "result": "..."}
|
||||
{"type": "done", "tool_calls": [...]}
|
||||
{"type": "error", "message": "..."}
|
||||
{"type": "spec_modified", "changes": [...]}
|
||||
{"type": "pong"}
|
||||
"""
|
||||
await websocket.accept()
|
||||
|
||||
manager = get_session_manager()
|
||||
session = manager.get_session(session_id)
|
||||
|
||||
if not session:
|
||||
await websocket.send_json({"type": "error", "message": "Session not found"})
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
# Import AtomizerClaudeAgent for direct API access
|
||||
from api.services.claude_agent import AtomizerClaudeAgent
|
||||
|
||||
# Create agent with study context
|
||||
agent = AtomizerClaudeAgent(study_id=session.study_id)
|
||||
conversation_history: List[Dict[str, Any]] = []
|
||||
|
||||
# Load initial spec and set canvas state so Claude sees current canvas
|
||||
initial_spec = agent.load_current_spec()
|
||||
if initial_spec:
|
||||
# Send initial spec to frontend
|
||||
await websocket.send_json({
|
||||
"type": "spec_updated",
|
||||
"spec": initial_spec,
|
||||
"reason": "initial_load"
|
||||
})
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
|
||||
if data.get("type") == "message":
|
||||
content = data.get("content", "")
|
||||
if not content:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Use streaming API with tool support for real-time response
|
||||
last_tool_calls = []
|
||||
async for event in agent.chat_stream_with_tools(content, conversation_history):
|
||||
event_type = event.get("type")
|
||||
|
||||
if event_type == "text":
|
||||
# Stream text tokens to frontend immediately
|
||||
await websocket.send_json({
|
||||
"type": "text",
|
||||
"content": event.get("content", ""),
|
||||
})
|
||||
|
||||
elif event_type == "tool_call":
|
||||
# Tool is being called
|
||||
tool_info = event.get("tool", {})
|
||||
await websocket.send_json({
|
||||
"type": "tool_call",
|
||||
"tool": tool_info,
|
||||
})
|
||||
|
||||
elif event_type == "tool_result":
|
||||
# Tool finished executing
|
||||
tool_name = event.get("tool", "")
|
||||
await websocket.send_json({
|
||||
"type": "tool_result",
|
||||
"tool": tool_name,
|
||||
"result": event.get("result", ""),
|
||||
})
|
||||
|
||||
# If it was a write tool, send full updated spec
|
||||
if tool_name in ["add_design_variable", "add_extractor",
|
||||
"add_objective", "add_constraint",
|
||||
"update_spec_field", "remove_node",
|
||||
"create_study"]:
|
||||
# Load updated spec and update agent's canvas state
|
||||
updated_spec = agent.load_current_spec()
|
||||
if updated_spec:
|
||||
await websocket.send_json({
|
||||
"type": "spec_updated",
|
||||
"tool": tool_name,
|
||||
"spec": updated_spec, # Full spec for direct canvas update
|
||||
})
|
||||
|
||||
elif event_type == "done":
|
||||
# Streaming complete
|
||||
last_tool_calls = event.get("tool_calls", [])
|
||||
await websocket.send_json({
|
||||
"type": "done",
|
||||
"tool_calls": last_tool_calls,
|
||||
})
|
||||
|
||||
# Update conversation history for next message
|
||||
# Note: For proper history tracking, we'd need to store messages properly
|
||||
# For now, we append the user message and response
|
||||
conversation_history.append({"role": "user", "content": content})
|
||||
conversation_history.append({"role": "assistant", "content": event.get("response", "")})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
await websocket.send_json({
|
||||
"type": "error",
|
||||
"message": str(e),
|
||||
})
|
||||
|
||||
elif data.get("type") == "canvas_edit":
|
||||
# User made a manual edit to the canvas - update Claude's context
|
||||
spec = data.get("spec")
|
||||
if spec:
|
||||
agent.set_canvas_state(spec)
|
||||
await websocket.send_json({
|
||||
"type": "canvas_edit_received",
|
||||
"acknowledged": True
|
||||
})
|
||||
|
||||
elif data.get("type") == "set_study":
|
||||
study_id = data.get("study_id")
|
||||
if study_id:
|
||||
await manager.set_study_context(session_id, study_id)
|
||||
# Recreate agent with new study context
|
||||
agent = AtomizerClaudeAgent(study_id=study_id)
|
||||
conversation_history = [] # Clear history on study change
|
||||
# Load spec for new study
|
||||
new_spec = agent.load_current_spec()
|
||||
await websocket.send_json({
|
||||
"type": "context_updated",
|
||||
"study_id": study_id,
|
||||
})
|
||||
if new_spec:
|
||||
await websocket.send_json({
|
||||
"type": "spec_updated",
|
||||
"spec": new_spec,
|
||||
"reason": "study_change"
|
||||
})
|
||||
|
||||
elif data.get("type") == "ping":
|
||||
await websocket.send_json({"type": "pong"})
|
||||
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as e:
|
||||
try:
|
||||
await websocket.send_json({"type": "error", "message": str(e)})
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@router.get("/suggestions")
|
||||
async def get_chat_suggestions(study_id: Optional[str] = None):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user