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:
2026-01-20 13:10:47 -05:00
parent b05412f807
commit ba0b9a1fae
31 changed files with 4836 additions and 349 deletions

View File

@@ -26,8 +26,8 @@ interface DesignVariable {
name: string;
parameter?: string; // Optional: the actual parameter name if different from name
unit?: string;
min: number;
max: number;
min?: number;
max?: number;
}
interface Constraint {

View File

@@ -8,14 +8,15 @@ import { ScatterChart, Scatter, Line, XAxis, YAxis, CartesianGrid, Tooltip, Cell
interface ParetoTrial {
trial_number: number;
values: [number, number];
values: number[]; // Support variable number of objectives
params: Record<string, number>;
constraint_satisfied?: boolean;
}
interface Objective {
name: string;
type: 'minimize' | 'maximize';
type?: 'minimize' | 'maximize';
direction?: 'minimize' | 'maximize'; // Alternative field used by some configs
unit?: string;
}

View File

@@ -1,5 +1,6 @@
// Main Canvas Component
export { AtomizerCanvas } from './AtomizerCanvas';
export { SpecRenderer } from './SpecRenderer';
// Palette
export { NodePalette } from './palette/NodePalette';

View File

@@ -5,7 +5,7 @@ import { ToolCallCard, ToolCall } from './ToolCallCard';
export interface Message {
id: string;
role: 'user' | 'assistant';
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
isStreaming?: boolean;
@@ -18,6 +18,18 @@ interface ChatMessageProps {
export const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
const isAssistant = message.role === 'assistant';
const isSystem = message.role === 'system';
// System messages are displayed centered with special styling
if (isSystem) {
return (
<div className="flex justify-center my-2">
<div className="px-3 py-1 bg-dark-700/50 rounded-full text-xs text-dark-400 border border-dark-600">
{message.content}
</div>
</div>
);
}
return (
<div

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect, useState } from 'react';
import React, { useRef, useEffect, useState, useMemo } from 'react';
import {
MessageSquare,
ChevronRight,
@@ -13,8 +13,10 @@ import { ChatMessage } from './ChatMessage';
import { ChatInput } from './ChatInput';
import { ThinkingIndicator } from './ThinkingIndicator';
import { ModeToggle } from './ModeToggle';
import { useChat } from '../../hooks/useChat';
import { useChat, CanvasState, CanvasModification } from '../../hooks/useChat';
import { useStudy } from '../../context/StudyContext';
import { useCanvasStore } from '../../hooks/useCanvasStore';
import { NodeType } from '../../lib/canvas/schema';
interface ChatPaneProps {
isOpen: boolean;
@@ -31,6 +33,76 @@ export const ChatPane: React.FC<ChatPaneProps> = ({
const messagesEndRef = useRef<HTMLDivElement>(null);
const [isExpanded, setIsExpanded] = useState(false);
// Get canvas state and modification functions from the store
const { nodes, edges, addNode, updateNodeData, selectNode, deleteSelected } = useCanvasStore();
// Build canvas state for chat context
const canvasState: CanvasState | null = useMemo(() => {
if (nodes.length === 0) return null;
return {
nodes: nodes.map(n => ({
id: n.id,
type: n.type,
data: n.data,
position: n.position,
})),
edges: edges.map(e => ({
id: e.id,
source: e.source,
target: e.target,
})),
studyName: selectedStudy?.name || selectedStudy?.id,
};
}, [nodes, edges, selectedStudy]);
// Track position offset for multiple node additions
const nodeAddCountRef = useRef(0);
// Handle canvas modifications from the assistant
const handleCanvasModification = React.useCallback((modification: CanvasModification) => {
console.log('Canvas modification from assistant:', modification);
switch (modification.action) {
case 'add_node':
if (modification.nodeType) {
const nodeType = modification.nodeType as NodeType;
// Calculate position: offset each new node so they don't stack
const basePosition = modification.position || { x: 100, y: 100 };
const offset = nodeAddCountRef.current * 120;
const position = {
x: basePosition.x,
y: basePosition.y + offset,
};
nodeAddCountRef.current += 1;
// Reset counter after a delay (for batch operations)
setTimeout(() => { nodeAddCountRef.current = 0; }, 2000);
addNode(nodeType, position, modification.data);
console.log(`Added ${nodeType} node at position:`, position);
}
break;
case 'update_node':
if (modification.nodeId && modification.data) {
updateNodeData(modification.nodeId, modification.data);
}
break;
case 'remove_node':
if (modification.nodeId) {
selectNode(modification.nodeId);
deleteSelected();
}
break;
// Edge operations would need additional store methods
case 'add_edge':
case 'remove_edge':
console.warn('Edge modification not yet implemented:', modification);
break;
}
}, [addNode, updateNodeData, selectNode, deleteSelected]);
const {
messages,
isThinking,
@@ -41,22 +113,38 @@ export const ChatPane: React.FC<ChatPaneProps> = ({
sendMessage,
clearMessages,
switchMode,
updateCanvasState,
} = useChat({
studyId: selectedStudy?.id,
mode: 'user',
useWebSocket: true,
canvasState,
onError: (err) => console.error('Chat error:', err),
onCanvasModification: handleCanvasModification,
});
// Keep canvas state synced with chat
useEffect(() => {
updateCanvasState(canvasState);
}, [canvasState, updateCanvasState]);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isThinking]);
// Welcome message based on study context
const welcomeMessage = selectedStudy
? `Ready to help with **${selectedStudy.name || selectedStudy.id}**. Ask me about optimization progress, results analysis, or how to improve your design.`
: 'Select a study to get started, or ask me to help you create a new one.';
// Welcome message based on study and canvas context
const welcomeMessage = useMemo(() => {
if (selectedStudy) {
return `Ready to help with **${selectedStudy.name || selectedStudy.id}**. Ask me about optimization progress, results analysis, or how to improve your design.`;
}
if (nodes.length > 0) {
const dvCount = nodes.filter(n => n.type === 'designVar').length;
const objCount = nodes.filter(n => n.type === 'objective').length;
return `I can see your canvas with ${dvCount} design variables and ${objCount} objectives. Ask me to analyze, validate, or create a study from this setup.`;
}
return 'Select a study to get started, or build an optimization in the Canvas Builder.';
}, [selectedStudy, nodes]);
// Collapsed state - just show toggle button
if (!isOpen) {

View File

@@ -30,22 +30,25 @@ interface ToolCallCardProps {
}
// Map tool names to friendly labels and icons
const TOOL_INFO: Record<string, { label: string; icon: React.ComponentType<{ className?: string }> }> = {
const TOOL_INFO: Record<string, { label: string; icon: React.ComponentType<{ className?: string }>; color?: string }> = {
// Study tools
list_studies: { label: 'Listing Studies', icon: Database },
get_study_status: { label: 'Getting Status', icon: FileSearch },
create_study: { label: 'Creating Study', icon: Settings },
create_study: { label: 'Creating Study', icon: Settings, color: 'text-green-400' },
// Optimization tools
run_optimization: { label: 'Starting Optimization', icon: Play },
run_optimization: { label: 'Starting Optimization', icon: Play, color: 'text-blue-400' },
stop_optimization: { label: 'Stopping Optimization', icon: XCircle },
get_optimization_status: { label: 'Checking Progress', icon: BarChart2 },
// Analysis tools
get_trial_data: { label: 'Querying Trials', icon: Database },
query_trials: { label: 'Querying Trials', icon: Database },
get_trial_details: { label: 'Getting Trial Details', icon: FileSearch },
analyze_convergence: { label: 'Analyzing Convergence', icon: BarChart2 },
compare_trials: { label: 'Comparing Trials', icon: BarChart2 },
get_best_design: { label: 'Getting Best Design', icon: CheckCircle },
get_optimization_summary: { label: 'Getting Summary', icon: BarChart2 },
// Reporting tools
generate_report: { label: 'Generating Report', icon: FileText },
@@ -56,6 +59,25 @@ const TOOL_INFO: Record<string, { label: string; icon: React.ComponentType<{ cla
recommend_method: { label: 'Recommending Method', icon: Settings },
query_extractors: { label: 'Listing Extractors', icon: Database },
// Config tools (read)
read_study_config: { label: 'Reading Config', icon: FileSearch },
read_study_readme: { label: 'Reading README', icon: FileText },
// === WRITE TOOLS (Power Mode) ===
add_design_variable: { label: 'Adding Design Variable', icon: Settings, color: 'text-amber-400' },
add_extractor: { label: 'Adding Extractor', icon: Settings, color: 'text-amber-400' },
add_objective: { label: 'Adding Objective', icon: Settings, color: 'text-amber-400' },
add_constraint: { label: 'Adding Constraint', icon: Settings, color: 'text-amber-400' },
update_spec_field: { label: 'Updating Field', icon: Settings, color: 'text-amber-400' },
remove_node: { label: 'Removing Node', icon: XCircle, color: 'text-red-400' },
// === INTERVIEW TOOLS ===
start_interview: { label: 'Starting Interview', icon: HelpCircle, color: 'text-purple-400' },
interview_record: { label: 'Recording Answer', icon: CheckCircle, color: 'text-purple-400' },
interview_advance: { label: 'Advancing Interview', icon: Play, color: 'text-purple-400' },
interview_status: { label: 'Checking Progress', icon: BarChart2, color: 'text-purple-400' },
interview_finalize: { label: 'Creating Study', icon: CheckCircle, color: 'text-green-400' },
// Admin tools (power mode)
edit_file: { label: 'Editing File', icon: FileText },
create_file: { label: 'Creating File', icon: FileText },
@@ -104,7 +126,7 @@ export const ToolCallCard: React.FC<ToolCallCardProps> = ({ toolCall }) => {
)}
{/* Tool icon */}
<Icon className="w-4 h-4 text-dark-400 flex-shrink-0" />
<Icon className={`w-4 h-4 flex-shrink-0 ${info.color || 'text-dark-400'}`} />
{/* Label */}
<span className="flex-1 text-sm text-dark-200 truncate">{info.label}</span>