Files
Atomizer/docs/plans/SAAS_ATOMIZER_ROADMAP.md
Anto01 ea437d360e docs: Major documentation overhaul - restructure folders, update tagline, add Getting Started guide
- Restructure docs/ folder (remove numeric prefixes):
  - 04_USER_GUIDES -> guides/
  - 05_API_REFERENCE -> api/
  - 06_PHYSICS -> physics/
  - 07_DEVELOPMENT -> development/
  - 08_ARCHIVE -> archive/
  - 09_DIAGRAMS -> diagrams/

- Replace tagline 'Talk, don't click' with 'LLM-driven optimization framework' in 9 files

- Create comprehensive docs/GETTING_STARTED.md:
  - Prerequisites and quick setup
  - Project structure overview
  - First study tutorial (Claude or manual)
  - Dashboard usage guide
  - Neural acceleration introduction

- Rewrite docs/00_INDEX.md with correct paths and modern structure

- Archive obsolete files:
  - 01_PROTOCOLS.md -> archive/historical/01_PROTOCOLS_legacy.md
  - 03_GETTING_STARTED.md -> archive/historical/
  - ATOMIZER_PODCAST_BRIEFING.md -> archive/marketing/

- Update timestamps to 2026-01-20 across all key files

- Update .gitignore to exclude docs/generated/

- Version bump: ATOMIZER_CONTEXT v1.8 -> v2.0
2026-01-20 10:03:45 -05:00

29 KiB

SaaS-Level Atomizer Roadmap (Revised)

Executive Summary

This roadmap transforms Atomizer into a SaaS-grade, LLM-assisted structural optimization platform with the core innovation being side-by-side Claude + Canvas integration where:

  1. Claude talks → Canvas updates in real-time (user sees nodes appear/change)
  2. User tweaks Canvas → Claude sees changes (bi-directional sync)
  3. Full Claude Code-level power through the dashboard chat
  4. Interview-driven study creation entirely through conversation

Vision: An engineer opens Atomizer, describes their optimization goal, watches the canvas build itself, makes quick tweaks, and starts optimization—all through natural conversation with full visual feedback.


The Core Innovation: Unified Claude + Canvas

The power of Atomizer is the side-by-side experience:

┌───────────────────────────────────────────────────────────────────┐
│                    ATOMIZER DASHBOARD                              │
├────────────────────────────┬──────────────────────────────────────┤
│     CHAT PANEL             │           CANVAS                      │
│     (Atomizer Assistant)   │         (SpecRenderer)                │
│                            │                                       │
│  "Create a bracket         │    [DV: thickness]                   │
│   optimization with        │          │                           │
│   mass and stiffness"      │          ▼                           │
│         │                  │    [Model Node]                      │
│         ▼                  │          │                           │
│  🔧 Adding thickness       │          ▼                           │
│  🔧 Adding mass ext    ◄───┼──►[Ext: mass]──>[Obj: min mass]     │
│  🔧 Adding stiffness   ◄───┼──►[Ext: disp]──>[Obj: min disp]     │
│  ✓ Study configured!       │                                       │
│                            │    (nodes appear in real-time)        │
│  User clicks a node ───────┼──► Claude sees the edit              │
│                            │                                       │
└────────────────────────────┴──────────────────────────────────────┘

Current State vs Target

What We Have

Component Status Notes
Power Mode WebSocket Implemented /ws/power endpoint with write tools
Write Tools Implemented add_design_variable, add_extractor, etc.
spec_modified Events Implemented Frontend receives notifications
Canvas Auto-reload Implemented Triggers on spec_modified
Streaming Responses Missing Currently waits for full response
Canvas State → Claude Missing Claude doesn't see current canvas
Interview Engine Missing No guided creation
Unified WebSocket Missing Separate connections for chat/spec

Target Architecture

                    ┌─────────────────────┐
                    │   Unified WebSocket │
                    │  /api/atomizer/ws   │
                    └─────────┬───────────┘
                              │
                     Bi-directional Sync
                              │
         ┌────────────────────┼────────────────────┐
         │                    │                    │
         ▼                    ▼                    ▼
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│   Chat Panel    │  │     Canvas      │  │   Spec Store    │
│                 │  │                 │  │                 │
│ Send messages   │  │ User edits →    │  │ Single source   │
│ Stream text     │  │ Notify Claude   │  │ of truth        │
│ See tool calls  │  │ Receive updates │  │ Validates all   │
└─────────────────┘  └─────────────────┘  └─────────────────┘

Phase 1: Unified WebSocket Hub (1-2 weeks)

Goal: Single connection for chat + canvas + spec sync

1.1 Backend: Unified WebSocket Endpoint

# atomizer-dashboard/backend/api/routes/atomizer_ws.py

@router.websocket("/api/atomizer/ws")
async def atomizer_websocket(websocket: WebSocket):
    """
    Unified WebSocket for Atomizer Dashboard.

    Handles:
    - Chat messages with streaming responses
    - Spec modifications with real-time canvas updates
    - Canvas edit notifications from user
    - Study context switching
    """
    await websocket.accept()

    agent = AtomizerClaudeAgent()
    conversation: List[Dict] = []
    current_spec: Optional[Dict] = None

    try:
        while True:
            data = await websocket.receive_json()

            if data["type"] == "message":
                # Chat message - stream response
                async for event in agent.chat_stream(
                    message=data["content"],
                    conversation=conversation,
                    canvas_state=current_spec  # Claude sees current canvas!
                ):
                    await websocket.send_json(event)

                    # If spec changed, update our local copy
                    if event.get("type") == "spec_updated":
                        current_spec = event["spec"]

            elif data["type"] == "canvas_edit":
                # User made a manual edit - update spec and tell Claude
                current_spec = apply_patch(current_spec, data["patch"])
                # Next message to Claude will include updated spec

            elif data["type"] == "set_study":
                # Switch study context
                agent.set_study(data["study_id"])
                current_spec = agent.load_spec()
                await websocket.send_json({
                    "type": "spec_updated",
                    "spec": current_spec
                })

    except WebSocketDisconnect:
        pass

1.2 Enhanced Claude Agent with Streaming

class AtomizerClaudeAgent:
    """Full-power Claude agent with Claude Code-like capabilities"""

    async def chat_stream(
        self,
        message: str,
        conversation: List[Dict],
        canvas_state: Optional[Dict] = None
    ) -> AsyncGenerator[Dict, None]:
        """Stream responses with tool calls"""

        # Build system prompt with current canvas state
        system = self._build_system_prompt()
        if canvas_state:
            system += self._format_canvas_context(canvas_state)

        messages = conversation + [{"role": "user", "content": message}]

        # Use streaming API
        with self.client.messages.stream(
            model="claude-sonnet-4-20250514",
            max_tokens=8192,
            system=system,
            messages=messages,
            tools=self.tools
        ) as stream:
            current_text = ""

            for event in stream:
                if event.type == "content_block_delta":
                    if hasattr(event.delta, "text"):
                        current_text += event.delta.text
                        yield {"type": "text", "content": event.delta.text, "done": False}

            # Get final message for tool calls
            response = stream.get_final_message()

            # Process tool calls
            for block in response.content:
                if block.type == "tool_use":
                    yield {"type": "tool_start", "tool": block.name, "input": block.input}

                    result = await self._execute_tool(block.name, block.input)

                    yield {"type": "tool_result", "tool": block.name, "result": result["preview"]}

                    if result.get("spec_changed"):
                        yield {"type": "spec_updated", "spec": self.spec_store.get()}

            yield {"type": "done"}

    def _format_canvas_context(self, spec: Dict) -> str:
        """Format current canvas state for Claude's context"""
        lines = ["\n## Current Canvas State\n"]

        if spec.get("design_variables"):
            lines.append(f"**Design Variables ({len(spec['design_variables'])}):**")
            for dv in spec["design_variables"]:
                lines.append(f"  - {dv['name']}: [{dv['bounds']['min']}, {dv['bounds']['max']}]")

        if spec.get("extractors"):
            lines.append(f"\n**Extractors ({len(spec['extractors'])}):**")
            for ext in spec["extractors"]:
                lines.append(f"  - {ext['name']} ({ext['type']})")

        if spec.get("objectives"):
            lines.append(f"\n**Objectives ({len(spec['objectives'])}):**")
            for obj in spec["objectives"]:
                lines.append(f"  - {obj['name']} ({obj['direction']})")

        if spec.get("constraints"):
            lines.append(f"\n**Constraints ({len(spec['constraints'])}):**")
            for con in spec["constraints"]:
                lines.append(f"  - {con['name']} {con['operator']} {con['threshold']}")

        lines.append("\nThe user can see this canvas. When you modify it, they see changes in real-time.")

        return "\n".join(lines)

1.3 Frontend: Unified Hook

// hooks/useAtomizerSocket.ts
export function useAtomizerSocket(studyId?: string) {
  const [spec, setSpec] = useState<AtomizerSpec | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const [isThinking, setIsThinking] = useState(false);
  const [streamingText, setStreamingText] = useState("");
  const [activeTool, setActiveTool] = useState<string | null>(null);

  const ws = useRef<WebSocket | null>(null);

  useEffect(() => {
    const url = `ws://localhost:8001/api/atomizer/ws`;
    ws.current = new WebSocket(url);

    ws.current.onopen = () => {
      if (studyId) {
        ws.current?.send(JSON.stringify({ type: "set_study", study_id: studyId }));
      }
    };

    ws.current.onmessage = (event) => {
      const data = JSON.parse(event.data);

      switch (data.type) {
        case "text":
          setStreamingText(prev => prev + data.content);
          break;

        case "tool_start":
          setActiveTool(data.tool);
          break;

        case "tool_result":
          setActiveTool(null);
          break;

        case "spec_updated":
          setSpec(data.spec);  // Canvas updates automatically!
          break;

        case "done":
          // Finalize message
          setMessages(prev => [...prev, {
            role: "assistant",
            content: streamingText
          }]);
          setStreamingText("");
          setIsThinking(false);
          break;
      }
    };

    return () => ws.current?.close();
  }, [studyId]);

  const sendMessage = useCallback((content: string) => {
    setIsThinking(true);
    setMessages(prev => [...prev, { role: "user", content }]);
    ws.current?.send(JSON.stringify({ type: "message", content }));
  }, []);

  const notifyCanvasEdit = useCallback((path: string, value: any) => {
    ws.current?.send(JSON.stringify({
      type: "canvas_edit",
      patch: { path, value }
    }));
  }, []);

  return {
    spec,
    messages,
    streamingText,
    isThinking,
    activeTool,
    sendMessage,
    notifyCanvasEdit
  };
}

Phase 2: Full Tool Set (1-2 weeks)

Goal: Claude Code-level power through dashboard

2.1 Tool Categories

ATOMIZER_TOOLS = {
    # === SPEC MODIFICATION (Already Implemented) ===
    "add_design_variable": "Add a design variable to the optimization",
    "add_extractor": "Add a physics extractor (mass, stress, displacement, custom)",
    "add_objective": "Add an optimization objective",
    "add_constraint": "Add a constraint",
    "update_spec_field": "Update any field in the spec by JSON path",
    "remove_node": "Remove a node from the spec",

    # === READ/QUERY ===
    "read_study_config": "Read the full atomizer_spec.json",
    "query_trials": "Query optimization trial data",
    "list_studies": "List all available studies",
    "get_optimization_status": "Check if optimization is running",

    # === STUDY MANAGEMENT (New) ===
    "create_study": "Create a new study directory with atomizer_spec.json",
    "clone_study": "Clone an existing study as a starting point",
    "validate_spec": "Validate the current spec for errors",

    # === NX INTEGRATION (New) ===
    "introspect_model": "Analyze NX model for expressions and features",
    "suggest_design_vars": "AI-suggest design variables from model",
    "list_model_expressions": "List all expressions in the NX model",

    # === OPTIMIZATION CONTROL (New) ===
    "start_optimization": "Start the optimization run",
    "stop_optimization": "Stop a running optimization",

    # === INTERVIEW (New) ===
    "start_interview": "Begin guided study creation interview",
    "get_interview_progress": "Get current interview state",
}

2.2 Create Study Tool

async def _tool_create_study(self, params: Dict) -> Dict:
    """Create a new study with atomizer_spec.json"""
    study_name = params["name"]
    study_dir = STUDIES_DIR / study_name

    # Create directory structure
    study_dir.mkdir(parents=True, exist_ok=True)
    (study_dir / "1_setup").mkdir(exist_ok=True)
    (study_dir / "2_iterations").mkdir(exist_ok=True)
    (study_dir / "3_results").mkdir(exist_ok=True)

    # Create initial spec
    spec = {
        "meta": {
            "version": "2.0",
            "study_name": study_name,
            "created_at": datetime.now().isoformat(),
            "created_by": "claude_agent"
        },
        "model": {
            "sim": {"path": params.get("model_path", ""), "solver": "nastran"}
        },
        "design_variables": [],
        "extractors": [],
        "objectives": [],
        "constraints": [],
        "optimization": {
            "algorithm": {"type": "TPE"},
            "budget": {"max_trials": 100}
        },
        "canvas": {"edges": [], "layout_version": "2.0"}
    }

    # Save spec
    spec_path = study_dir / "atomizer_spec.json"
    with open(spec_path, "w") as f:
        json.dump(spec, f, indent=2)

    # Update agent's study context
    self.study_id = study_name
    self.study_dir = study_dir

    return {
        "preview": f"✓ Created study '{study_name}' at {study_dir}",
        "spec_changed": True,
        "study_id": study_name
    }

2.3 NX Introspection Tool

async def _tool_introspect_model(self, params: Dict) -> Dict:
    """Analyze NX model for design variable candidates"""
    model_path = params.get("model_path") or self._find_model_path()

    if not model_path or not Path(model_path).exists():
        return {"preview": "✗ Model file not found", "spec_changed": False}

    # Use NX session to get expressions
    expressions = await self._get_nx_expressions(model_path)

    # Classify expressions as potential DVs
    candidates = []
    for expr in expressions:
        score = self._score_dv_candidate(expr)
        if score > 0.5:
            candidates.append({
                "name": expr["name"],
                "value": expr["value"],
                "formula": expr.get("formula", ""),
                "score": score,
                "suggested_bounds": self._suggest_bounds(expr)
            })

    # Sort by score
    candidates.sort(key=lambda x: x["score"], reverse=True)

    return {
        "preview": f"Found {len(expressions)} expressions, {len(candidates)} are DV candidates",
        "expressions": expressions,
        "candidates": candidates[:10],  # Top 10
        "spec_changed": False
    }

def _score_dv_candidate(self, expr: Dict) -> float:
    """Score expression as design variable candidate"""
    score = 0.0
    name = expr["name"].lower()

    # Geometric parameters score high
    if any(kw in name for kw in ["thickness", "width", "height", "radius", "diameter", "depth"]):
        score += 0.4

    # Numeric with reasonable value
    if isinstance(expr["value"], (int, float)) and expr["value"] > 0:
        score += 0.2

    # Not a formula (pure number)
    if not expr.get("formula") or expr["formula"] == str(expr["value"]):
        score += 0.2

    # Common design parameter names
    if any(kw in name for kw in ["rib", "web", "flange", "support", "angle"]):
        score += 0.2

    return min(score, 1.0)

Phase 3: Interview Engine (1-2 weeks)

Goal: Guided study creation through conversation

3.1 Interview Engine

class InterviewEngine:
    """Guided study creation through conversation"""

    PHASES = [
        ("welcome", "What kind of optimization do you want to set up?"),
        ("model", "What's the path to your NX simulation file (.sim)?"),
        ("objectives", "What do you want to optimize? (e.g., minimize mass, minimize displacement)"),
        ("design_vars", "Which parameters should vary? I can suggest some from your model."),
        ("constraints", "Any constraints to respect? (e.g., max stress ≤ 200 MPa)"),
        ("method", "I recommend {method} for this. Sound good?"),
        ("review", "Here's your configuration. Ready to create the study?"),
    ]

    def __init__(self):
        self.phase_index = 0
        self.collected = {}
        self.spec_builder = SpecBuilder()

    def get_current_question(self) -> str:
        phase, question = self.PHASES[self.phase_index]

        # Dynamic question customization
        if phase == "method":
            method = self._recommend_method()
            question = question.format(method=method)
        elif phase == "design_vars" and self.collected.get("model_expressions"):
            candidates = self.collected["model_expressions"][:5]
            question += f"\n\nI found these candidates: {', '.join(c['name'] for c in candidates)}"

        return question

    def process_answer(self, answer: str) -> Dict:
        """Process user's answer and advance interview"""
        phase, _ = self.PHASES[self.phase_index]

        # Extract structured data based on phase
        extracted = self._extract_for_phase(phase, answer)
        self.collected[phase] = extracted

        # Build spec incrementally
        spec_changes = self.spec_builder.apply(phase, extracted)

        # Advance
        self.phase_index += 1
        complete = self.phase_index >= len(self.PHASES)

        return {
            "phase_completed": phase,
            "extracted": extracted,
            "spec_changes": spec_changes,
            "next_question": None if complete else self.get_current_question(),
            "complete": complete,
            "spec": self.spec_builder.get_spec() if complete else None
        }

    def _extract_for_phase(self, phase: str, answer: str) -> Dict:
        """Extract structured data from natural language answer"""
        if phase == "model":
            # Extract file path
            return {"path": self._extract_path(answer)}

        elif phase == "objectives":
            # Extract objectives
            objectives = []
            if "mass" in answer.lower() or "weight" in answer.lower():
                direction = "minimize" if "minimize" in answer.lower() or "reduce" in answer.lower() else "minimize"
                objectives.append({"name": "mass", "direction": direction})
            if "displacement" in answer.lower() or "stiff" in answer.lower():
                objectives.append({"name": "max_displacement", "direction": "minimize"})
            if "stress" in answer.lower():
                objectives.append({"name": "max_stress", "direction": "minimize"})
            if "wfe" in answer.lower() or "wavefront" in answer.lower():
                objectives.append({"name": "wfe", "direction": "minimize"})
            return {"objectives": objectives}

        elif phase == "constraints":
            # Extract constraints
            constraints = []
            import re
            # Pattern: "stress < 200 MPa" or "max stress <= 200"
            stress_match = re.search(r'stress[^0-9]*([<>=]+)\s*(\d+)', answer.lower())
            if stress_match:
                constraints.append({
                    "name": "max_stress",
                    "operator": stress_match.group(1),
                    "threshold": float(stress_match.group(2))
                })
            return {"constraints": constraints}

        return {"raw": answer}

    def _recommend_method(self) -> str:
        """Recommend optimization method based on collected info"""
        objectives = self.collected.get("objectives", {}).get("objectives", [])
        if len(objectives) > 1:
            return "NSGA-II (multi-objective)"
        return "TPE (Bayesian optimization)"

3.2 Interview Tool Integration

async def _tool_start_interview(self, params: Dict) -> Dict:
    """Start guided study creation"""
    self.interview = InterviewEngine()
    question = self.interview.get_current_question()

    return {
        "preview": f"Starting interview.\n\n{question}",
        "interview_started": True,
        "spec_changed": False
    }

async def _tool_interview_answer(self, params: Dict) -> Dict:
    """Process interview answer"""
    if not self.interview:
        return {"preview": "No interview in progress", "spec_changed": False}

    result = self.interview.process_answer(params["answer"])

    response = f"Got it: {result['phase_completed']}\n\n"

    if result["spec_changes"]:
        response += "Updated configuration:\n"
        for change in result["spec_changes"]:
            response += f"  ✓ {change}\n"

    if result["next_question"]:
        response += f"\n{result['next_question']}"
    elif result["complete"]:
        response += "\n✓ Interview complete! Creating study..."
        # Auto-create the study
        self.spec_store.set(result["spec"])

    return {
        "preview": response,
        "spec_changed": result["complete"],
        "complete": result["complete"]
    }

Phase 4: Visual Polish (1 week)

Goal: Beautiful, responsive canvas updates

4.1 Tool Call Visualization

// components/chat/ToolCallIndicator.tsx
function ToolCallIndicator({ tool, status }: { tool: string; status: 'running' | 'complete' }) {
  const icons: Record<string, JSX.Element> = {
    add_design_variable: <Variable className="w-4 h-4" />,
    add_extractor: <Cpu className="w-4 h-4" />,
    add_objective: <Target className="w-4 h-4" />,
    add_constraint: <Lock className="w-4 h-4" />,
    create_study: <FolderPlus className="w-4 h-4" />,
    introspect_model: <Search className="w-4 h-4" />,
  };

  return (
    <div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${
      status === 'running'
        ? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
        : 'bg-green-500/10 text-green-400 border border-green-500/20'
    }`}>
      {status === 'running' ? (
        <Loader2 className="w-4 h-4 animate-spin" />
      ) : (
        <Check className="w-4 h-4" />
      )}
      {icons[tool] || <Wrench className="w-4 h-4" />}
      <span className="text-sm font-medium">
        {formatToolName(tool)}
      </span>
    </div>
  );
}

4.2 Canvas Node Animation

// components/canvas/AnimatedNode.tsx
function AnimatedNode({ data, isNew, isHighlighted }) {
  return (
    <motion.div
      initial={isNew ? { scale: 0, opacity: 0 } : false}
      animate={{
        scale: 1,
        opacity: 1,
        boxShadow: isHighlighted
          ? '0 0 0 3px rgba(245, 158, 11, 0.5)'
          : 'none'
      }}
      transition={{ type: 'spring', stiffness: 500, damping: 30 }}
      className="node-container"
    >
      {/* Node content */}
    </motion.div>
  );
}

4.3 Connection Line Animation

// Draw animated line when Claude adds an edge
function AnimatedEdge({ source, target, isNew }) {
  return (
    <motion.path
      d={getBezierPath({ source, target })}
      initial={isNew ? { pathLength: 0, opacity: 0 } : false}
      animate={{ pathLength: 1, opacity: 1 }}
      transition={{ duration: 0.5, ease: 'easeOut' }}
      stroke="currentColor"
      strokeWidth={2}
      fill="none"
    />
  );
}

User Experience Flows

Flow 1: Quick Creation (Experienced User)

User: "Create bracket optimization, minimize mass, thickness 2-10mm, max stress 200 MPa"

Claude: [Parses complete intent]
        🔧 Creating study "bracket_optimization"
        🔧 Adding design variable: thickness [2mm - 10mm]
        🔧 Adding extractor: mass
        🔧 Adding extractor: max_stress
        🔧 Adding objective: minimize mass
        🔧 Adding constraint: stress ≤ 200 MPa

        ✓ Created! Canvas shows your setup. Click any node to adjust.

[Canvas animates: DV appears → Model → Extractors → Objectives/Constraints]
[Total time: ~5 seconds]

Flow 2: Guided Interview (New User)

User: "Help me set up an optimization"

Claude: What kind of optimization do you want to set up?
        (e.g., bracket stiffness, mirror WFE, beam stress)

User: "Mirror surface quality optimization"

Claude: Got it - mirror optimization!

        What's the path to your NX simulation file?
        (You can drag & drop or paste the path)

User: "studies/M1_Mirror/model.sim"

Claude: 🔧 Loading model...

        Found your model with 15 expressions. These look like good design variables:
        - rib_thickness (8mm)
        - mirror_depth (50mm)
        - support_angle (45°)

        What are you trying to optimize?

[Canvas: Model node appears]

User: "Minimize wavefront error and mass"

Claude: 🔧 Adding Zernike WFE extractor
        🔧 Adding mass extractor
        🔧 Adding objectives (multi-objective)

        Multi-objective optimization! I recommend NSGA-II.

        Which parameters should vary?

[Canvas: Extractors and objectives appear, connected to model]

User: "Use rib_thickness and mirror_depth, 50% range on each"

Claude: 🔧 Adding rib_thickness [4mm - 12mm]
        🔧 Adding mirror_depth [25mm - 75mm]

        Any constraints?

[Canvas: DVs appear, connected to model]

User: "Keep stress under 150 MPa"

Claude: 🔧 Adding stress extractor
        🔧 Adding constraint: stress ≤ 150 MPa

        ✓ Configuration complete!

        Your setup:
        - 2 design variables
        - 3 extractors (WFE, mass, stress)
        - 2 objectives (multi-objective)
        - 1 constraint

        Ready to run? Say "start" or adjust the canvas first.

[Canvas: Full optimization flow visible]

Flow 3: User Edits, Claude Adapts

[User clicks rib_thickness node, changes max from 12mm to 20mm]

User: "Is this a good range?"

Claude: I see you expanded rib_thickness to [4mm - 20mm].

        That's a wide range! Considerations:
        - Thicker ribs = more mass, more stiffness
        - 20mm might exceed typical manufacturing limits
        - Wider range = more trials to explore

        If you're exploring, this is fine. For production,
        I'd suggest [4mm - 15mm].

        Want me to adjust, or keep your change?

Implementation Priority

Week 1-2: Unified WebSocket

  • Create /api/atomizer/ws endpoint
  • Implement streaming in AtomizerClaudeAgent
  • Create useAtomizerSocket hook
  • Wire canvas to receive spec updates
  • Add canvas edit notifications

Week 3-4: Tools & Interview

  • Add create_study tool
  • Add introspect_model tool
  • Implement InterviewEngine
  • Add interview tools
  • Test guided creation flow

Week 5: Polish

  • Tool call indicators in chat
  • Node appear/highlight animations
  • Edge draw animations
  • Error recovery & reconnection
  • Performance optimization

Success Metrics

Metric Target
Study creation time (experienced) < 30 seconds
Study creation time (interview) < 3 minutes
Canvas update latency < 200ms
User edit → Claude context < 100ms
Interview completion rate > 90%

Key Files to Modify

Backend

  • atomizer-dashboard/backend/api/routes/atomizer_ws.py (new)
  • atomizer-dashboard/backend/api/services/claude_agent.py (enhance)
  • atomizer-dashboard/backend/api/services/interview_engine.py (new)

Frontend

  • atomizer-dashboard/frontend/src/hooks/useAtomizerSocket.ts (new)
  • atomizer-dashboard/frontend/src/pages/CanvasView.tsx (update)
  • atomizer-dashboard/frontend/src/components/canvas/SpecRenderer.tsx (update)
  • atomizer-dashboard/frontend/src/components/chat/ToolCallIndicator.tsx (new)

This architecture makes Atomizer uniquely powerful: natural language + visual feedback + full control, all in one seamless experience.