From f86e16661a514f17be894ba71f5d0af1d8eb3f30 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 6 Mar 2026 02:20:14 -0500 Subject: [PATCH] feat: add procedural flow support to AI chat builder (Flow Assist) - Add procedural-specific system prompts (schema, interview protocol, response format) - Dispatch prompts by flow_type: procedural/maintenance use flat steps schema, troubleshooting uses decision tree schema - Parse [STEPS_UPDATE] and [INTAKE_FORM] markers in AI responses - Add validate_generated_procedural_steps() validator - Handle intake form extraction in AI chat import endpoint - Add StaticStepsPreview component for procedural flow preview - Update store and page to render correct preview by flow type Co-Authored-By: Claude Opus 4.6 --- backend/app/api/endpoints/ai_chat.py | 7 + backend/app/core/ai_chat_service.py | 247 +++++- backend/app/core/ai_tree_validator.py | 93 ++ .../2026-03-06-procedural-flow-assist.md | 835 ++++++++++++++++++ .../components/ai-chat/StaticStepsPreview.tsx | 112 +++ frontend/src/pages/AIChatBuilderPage.tsx | 25 +- frontend/src/store/aiChatStore.ts | 17 +- 7 files changed, 1298 insertions(+), 38 deletions(-) create mode 100644 docs/plans/2026-03-06-procedural-flow-assist.md create mode 100644 frontend/src/components/ai-chat/StaticStepsPreview.tsx diff --git a/backend/app/api/endpoints/ai_chat.py b/backend/app/api/endpoints/ai_chat.py index defebd5e..b95fa1bd 100644 --- a/backend/app/api/endpoints/ai_chat.py +++ b/backend/app/api/endpoints/ai_chat.py @@ -390,11 +390,18 @@ async def import_tree( # Always create a new Tree record (no duplicate check — user may # want multiple copies or re-import after edits) metadata = session.tree_metadata or {} + + # Extract intake form from metadata if present (procedural flows) + intake_form = None + if isinstance(metadata.get("intake_form"), list): + intake_form = metadata.pop("intake_form") + tree = Tree( name=data.name or metadata.get("name", "AI-Generated Flow"), description=data.description or metadata.get("description", ""), tree_type=session.flow_type, tree_structure=session.working_tree, + intake_form=intake_form, author_id=current_user.id, account_id=current_user.account_id, category_id=data.category_id, diff --git a/backend/app/core/ai_chat_service.py b/backend/app/core/ai_chat_service.py index cd960a90..cab333f2 100644 --- a/backend/app/core/ai_chat_service.py +++ b/backend/app/core/ai_chat_service.py @@ -15,7 +15,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.ai_provider import get_ai_provider -from app.core.ai_tree_validator import validate_generated_tree +from app.core.ai_tree_validator import validate_generated_tree, validate_generated_procedural_steps from app.core.config import settings from app.models.ai_chat_session import AIChatSession @@ -140,18 +140,139 @@ IMPORTANT: """ +PROCEDURAL_SCHEMA_CONTEXT = """ +PROCEDURAL STEP SCHEMA — This is what you are building: + +The flow is an ordered array of steps in a JSON object: {"steps": [...]} + +Each step has a "type" field: + +1. procedure_step — A concrete step the engineer performs + Required: id (string), type ("procedure_step"), title (string), description (string) + Optional: + - content_type ("action"|"informational"|"verification"|"warning") — default "action" + - estimated_minutes (number) + - commands (array of objects: {code: string, label?: string, language?: string}) — exact CLI/PowerShell syntax + - expected_outcome (string) — what success looks like + - verification_prompt (string) — question to confirm completion + - verification_type ("checkbox"|"text_input") — how the engineer confirms + - warning_text (string) — caution or prerequisite info + - notes_enabled (boolean) — allow engineer to capture notes on this step + - reference_url (string) — link to documentation + +2. section_header — Groups steps into logical phases + Required: id (string), type ("section_header"), title (string) + Section headers apply to all subsequent steps until the next section_header. + +3. procedure_end — Terminal marker (always the last step) + Required: id (string), type ("procedure_end"), title (string) + +STRUCTURAL RULES: +- Steps are executed in array order (flat list, no branching) +- All IDs must be unique descriptive slugs (e.g., "check-dns-resolution", not UUIDs) +- The last step MUST be type "procedure_end" +- Use section_headers to organize steps into logical phases +- Commands are arrays of objects: [{"code": "Get-Service ADSync", "label": "Check sync service", "language": "powershell"}] +- Descriptions support [VAR:variable_name] interpolation for intake form variables (e.g., "Connect to [VAR:server_name] via RDP") + +VARIABLE INTERPOLATION: +When the procedure needs per-execution input (server name, IP address, client name, etc.), use [VAR:variable_name] syntax in descriptions and commands. These map to intake form fields that the engineer fills in before starting. +""" + +PROCEDURAL_INTERVIEW_PROTOCOL = """ +INTERVIEW PHASES — Follow this progression: + +PHASE 1 - SCOPING (current_phase: scoping): +Understand the process being documented: +- What process or procedure is this flow for? +- Who will execute it? (Tier 1 help desk, Tier 2, senior engineers?) +- What environment context? (Specific vendor, on-prem vs cloud, tools available?) +- Will this need per-execution input? (server name, client info, IP addresses → intake form fields) +Demonstrate domain expertise: if the user says "Exchange Online mailbox migration," show understanding: "Are we covering full tenant-to-tenant migration, on-prem to Exchange Online cutover, or individual mailbox moves with hybrid?" +DO NOT emit [STEPS_UPDATE] during scoping. You are still understanding the process. + +PHASE 2 - DISCOVERY (current_phase: discovery): +Build the procedure step by step IN ORDER: +- Start with prerequisites and initial verification +- Walk through each step sequentially — ask what happens first, then next, then next +- Suggest section headers to organize logical phases (e.g., "Pre-Flight Checks", "Migration", "Verification") +- Capture specific commands, tools, and expected outcomes for each step +- Identify where [VAR:variable_name] placeholders are needed +EMIT [STEPS_UPDATE] when you and the user have agreed on concrete steps. Build progressively — emit partial step lists as you go. + +PHASE 3 - ENRICHMENT (current_phase: enrichment): +Circle back to enrich existing steps: +- Add exact PowerShell/CLI commands with full syntax +- Add verification prompts for critical steps +- Add warning_text for steps with risk (data loss, downtime, etc.) +- Add estimated_minutes for time-critical procedures +- Add expected_outcome for action steps +- Suggest reference_url links to documentation +- Identify missing edge cases or safety checks +EMIT [STEPS_UPDATE] when enriching steps with additional detail. + +PHASE 4 - REVIEW (current_phase: review): +Present a summary: +- Total step count by content_type +- Outline of sections and steps +- List of intake form variables ([VAR:...]) used +- Flag any steps missing commands or verification +- Offer chance to reorder, add, or remove steps +EMIT [STEPS_UPDATE] only if the user requests changes. + +TRANSITION between phases by emitting [PHASE:phase_name] when the conversation naturally moves to the next stage. You decide when enough information has been gathered for each phase. +""" + +PROCEDURAL_RESPONSE_FORMAT = """ +RESPONSE FORMAT: + +Your response is natural conversational text. When the step structure changes, include structured markers that will be parsed by the system (the user will NOT see these markers): + +1. Steps update (only when structure changes — see phase rules above): +[STEPS_UPDATE] +{"steps": [...valid steps array...]} +[/STEPS_UPDATE] + +2. Phase transition (when moving to next phase): +[PHASE:discovery] + +3. Metadata capture (when you learn the flow's name, description, or tags): +[METADATA] +{"name": "...", "description": "...", "tags": ["..."]} +[/METADATA] + +4. Intake form suggestion (when intake form fields are identified): +[INTAKE_FORM] +[{"variable_name": "server_name", "label": "Server Name", "field_type": "text", "required": true, "placeholder": "e.g., DC01", "group_name": "Server Details", "display_order": 1}] +[/INTAKE_FORM] + +IMPORTANT: +- Include [STEPS_UPDATE] sparingly. Only when concrete steps are established or modified. +- The steps update should be the COMPLETE working step list, not a diff. +- Always include conversational text OUTSIDE the markers — never respond with only markers. +- The procedure_end step is always included as the last step. +""" + + def _build_system_prompt(flow_type: str) -> str: """Assemble the full system prompt for the chat builder.""" - flow_context = ( - "The user wants to build a TROUBLESHOOTING flow — a diagnostic decision tree " - "that guides engineers through symptom identification, diagnostic checks, and " - "resolution steps." - if flow_type == "troubleshooting" - else "The user wants to build a PROCEDURAL flow — a step-by-step process guide " - "with phases, checklists, and verification steps." - ) - - return f"{ROLE_PERSONA}\n\n{flow_context}\n\n{SCHEMA_CONTEXT}\n\n{INTERVIEW_PROTOCOL}\n\n{RESPONSE_FORMAT}" + if flow_type in ("procedural", "maintenance"): + flow_context = ( + "The user wants to build a PROCEDURAL flow — a step-by-step process guide " + "with ordered phases, verification checkpoints, and optional intake form variables. " + "This is NOT a branching decision tree — it is a flat, sequential procedure." + ) + return ( + f"{ROLE_PERSONA}\n\n{flow_context}\n\n" + f"{PROCEDURAL_SCHEMA_CONTEXT}\n\n{PROCEDURAL_INTERVIEW_PROTOCOL}\n\n{PROCEDURAL_RESPONSE_FORMAT}" + ) + else: + flow_context = ( + "The user wants to build a TROUBLESHOOTING flow — a diagnostic decision tree " + "that guides engineers through symptom identification, diagnostic checks, and " + "resolution steps." + ) + return f"{ROLE_PERSONA}\n\n{flow_context}\n\n{SCHEMA_CONTEXT}\n\n{INTERVIEW_PROTOCOL}\n\n{RESPONSE_FORMAT}" def _strip_markdown_fences(text: str) -> str: @@ -177,6 +298,7 @@ def _parse_ai_response(raw_response: str) -> dict[str, Any]: "tree_update": None, "phase": None, "metadata": None, + "intake_form": None, } # Extract [TREE_UPDATE]...[/TREE_UPDATE] @@ -198,6 +320,40 @@ def _parse_ai_response(raw_response: str) -> dict[str, Any]: logger.warning("Truncated [TREE_UPDATE] block detected (no closing tag) — stripping from display") result["content"] = raw_response[: truncated_match.start()] + # Extract [STEPS_UPDATE]...[/STEPS_UPDATE] (procedural flows) + steps_match = re.search( + r"\[STEPS_UPDATE\]\s*([\s\S]*?)\s*\[/STEPS_UPDATE\]", result["content"] + ) + if steps_match: + try: + raw_json = _strip_markdown_fences(steps_match.group(1)) + result["tree_update"] = json.loads(raw_json) + except (json.JSONDecodeError, ValueError) as e: + logger.warning("Failed to parse steps update JSON: %s", e) + result["content"] = result["content"][: steps_match.start()] + result["content"][steps_match.end() :] + else: + truncated_steps = re.search(r"\[STEPS_UPDATE\][\s\S]*$", result["content"]) + if truncated_steps: + logger.warning("Truncated [STEPS_UPDATE] block detected (no closing tag) — stripping from display") + result["content"] = result["content"][: truncated_steps.start()] + + # Extract [INTAKE_FORM]...[/INTAKE_FORM] (procedural flows) + intake_match = re.search( + r"\[INTAKE_FORM\]\s*([\s\S]*?)\s*\[/INTAKE_FORM\]", result["content"] + ) + if intake_match: + try: + raw_json = _strip_markdown_fences(intake_match.group(1)) + result["intake_form"] = json.loads(raw_json) + except (json.JSONDecodeError, ValueError) as e: + logger.warning("Failed to parse intake form JSON: %s", e) + result["content"] = result["content"][: intake_match.start()] + result["content"][intake_match.end() :] + else: + truncated_intake = re.search(r"\[INTAKE_FORM\][\s\S]*$", result["content"]) + if truncated_intake: + logger.warning("Truncated [INTAKE_FORM] block detected — stripping from display") + result["content"] = result["content"][: truncated_intake.start()] + # Extract [PHASE:name] phase_match = re.search(r"\[PHASE:(\w+)\]", result["content"]) if phase_match: @@ -318,12 +474,19 @@ async def send_message( # only require valid root structure, not min node counts) tree_update = parsed["tree_update"] if tree_update: - if not isinstance(tree_update, dict) or tree_update.get("type") != "decision": - logger.warning("AI tree update rejected: root must be a decision node") - tree_update = None - elif not tree_update.get("id"): - logger.warning("AI tree update rejected: root node missing id") - tree_update = None + if session.flow_type in ("procedural", "maintenance"): + # Procedural: must be a dict with a "steps" list + if not isinstance(tree_update, dict) or not isinstance(tree_update.get("steps"), list): + logger.warning("AI steps update rejected: must be a dict with a 'steps' list") + tree_update = None + else: + # Troubleshooting: root must be a decision node + if not isinstance(tree_update, dict) or tree_update.get("type") != "decision": + logger.warning("AI tree update rejected: root must be a decision node") + tree_update = None + elif not tree_update.get("id"): + logger.warning("AI tree update rejected: root node missing id") + tree_update = None # Update session state history.append({"role": "assistant", "content": parsed["content"], "timestamp": now_iso}) @@ -345,6 +508,11 @@ async def send_message( merged.update(parsed["metadata"]) session.tree_metadata = merged + if parsed.get("intake_form"): + merged = dict(session.tree_metadata) + merged["intake_form"] = parsed["intake_form"] + session.tree_metadata = merged + session.updated_at = datetime.now(timezone.utc) return parsed["content"], tree_update, parsed["phase"], parsed["metadata"] @@ -367,7 +535,33 @@ async def generate_final_tree( for msg in session.conversation_history ] - generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL TreeStructure JSON for this flow. + if session.flow_type in ("procedural", "maintenance"): + generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL procedural steps JSON for this flow. + +Requirements: +- Output format: {"steps": [...]} — a JSON object with a "steps" array +- Include ALL steps, section headers, and details we discussed +- Use descriptive step IDs (slugs, not UUIDs) +- Steps are in execution order (flat list, no branching) +- Use section_header steps to organize into logical phases +- Every procedure_step should have commands with exact syntax where discussed +- Every procedure_step should have expected_outcome and verification_prompt where discussed +- Include content_type, estimated_minutes, warning_text, and reference_url where discussed +- Use [VAR:variable_name] syntax in descriptions/commands for intake form variables +- The LAST step MUST be type "procedure_end" +- Respond with ONLY the JSON — no conversational text, no markdown fences + +Also provide metadata as a separate JSON object after the steps: +[METADATA] +{"name": "...", "description": "...", "tags": ["..."]} +[/METADATA] + +If we discussed intake form fields, also include: +[INTAKE_FORM] +[{"variable_name": "server_name", "label": "Server Name", "field_type": "text", "required": true, "placeholder": "e.g., DC01", "group_name": "Server Details", "display_order": 1}] +[/INTAKE_FORM]""" + else: + generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL TreeStructure JSON for this flow. Requirements: - Include ALL branches, steps, and solutions we discussed @@ -421,21 +615,30 @@ Also provide metadata as a separate JSON object after the tree: continue raise ValueError("AI failed to produce valid JSON after retry") - errors = validate_generated_tree(tree) - if errors: + if session.flow_type in ("procedural", "maintenance"): + val_errors = validate_generated_procedural_steps(tree) + else: + val_errors = validate_generated_tree(tree) + + if val_errors: if attempt == 0: provider_messages.append({"role": "assistant", "content": response_text}) correction = ( - f"The tree has validation errors: {'; '.join(errors)}. " + f"The generated structure has validation errors: {'; '.join(val_errors)}. " "Please fix these issues and respond with the corrected JSON only." ) provider_messages.append({"role": "user", "content": correction}) continue - raise ValueError(f"Generated tree failed validation: {'; '.join(errors)}") + raise ValueError(f"Generated structure failed validation: {'; '.join(val_errors)}") # Success session.working_tree = tree session.tree_metadata = metadata + if parsed.get("intake_form"): + merged = dict(session.tree_metadata) + merged["intake_form"] = parsed["intake_form"] + session.tree_metadata = merged + metadata = session.tree_metadata session.current_phase = "generation" session.updated_at = datetime.now(timezone.utc) diff --git a/backend/app/core/ai_tree_validator.py b/backend/app/core/ai_tree_validator.py index 351a223f..850aa219 100644 --- a/backend/app/core/ai_tree_validator.py +++ b/backend/app/core/ai_tree_validator.py @@ -230,3 +230,96 @@ def count_tree_stats(tree: dict[str, Any]) -> dict[str, int]: _count(tree, 1) return stats + + +# --- Procedural flow validation --- + +VALID_PROCEDURAL_STEP_TYPES = {"procedure_step", "procedure_end", "section_header"} +VALID_CONTENT_TYPES = {"action", "informational", "verification", "warning"} + + +def validate_generated_procedural_steps(tree: dict[str, Any]) -> list[str]: + """Validate an AI-generated procedural step array. + + Expects a dict with a 'steps' key containing a list of step objects. + Returns a list of error strings. Empty list means valid. + """ + errors: list[str] = [] + + if not isinstance(tree, dict): + return ["Procedural flow must be a JSON object"] + + steps = tree.get("steps") + if not isinstance(steps, list) or len(steps) == 0: + return ["Procedural flow must have a non-empty 'steps' array"] + + if len(steps) > 100: + errors.append( + f"Procedural flow has {len(steps)} steps. Maximum 100 allowed." + ) + + all_ids: set[str] = set() + procedure_step_count = 0 + procedure_end_count = 0 + + for i, step in enumerate(steps): + if not isinstance(step, dict): + errors.append(f"Step at index {i} is not an object") + continue + + # Check required fields + step_id = step.get("id") + step_type = step.get("type") + step_title = step.get("title") + + if not step_id or not isinstance(step_id, str): + errors.append(f"Step at index {i} missing or invalid 'id' (must be a string)") + elif step_id in all_ids: + errors.append(f"Duplicate step ID: '{step_id}'") + else: + all_ids.add(step_id) + + if not step_type or step_type not in VALID_PROCEDURAL_STEP_TYPES: + errors.append( + f"Step '{step_id or f'index {i}'}' has invalid type '{step_type}'. " + f"Must be one of: {', '.join(sorted(VALID_PROCEDURAL_STEP_TYPES))}" + ) + else: + if step_type == "procedure_step": + procedure_step_count += 1 + elif step_type == "procedure_end": + procedure_end_count += 1 + + if not step_title or not isinstance(step_title, str): + errors.append(f"Step '{step_id or f'index {i}'}' missing or invalid 'title' (must be a string)") + + # Validate content_type if present + content_type = step.get("content_type") + if content_type is not None and content_type not in VALID_CONTENT_TYPES: + errors.append( + f"Step '{step_id or f'index {i}'}' has invalid content_type '{content_type}'. " + f"Must be one of: {', '.join(sorted(VALID_CONTENT_TYPES))}" + ) + + # Must have exactly one procedure_end as the last step + if procedure_end_count == 0: + errors.append("Procedural flow must have exactly one 'procedure_end' step") + elif procedure_end_count > 1: + errors.append( + f"Procedural flow has {procedure_end_count} 'procedure_end' steps. " + "Must have exactly one." + ) + else: + # Exactly one — check it's the last step + last_step = steps[-1] + if isinstance(last_step, dict) and last_step.get("type") != "procedure_end": + errors.append("The 'procedure_end' step must be the last step in the array") + + # Need at least 2 procedure_step items + if procedure_step_count < 2: + errors.append( + f"Procedural flow has only {procedure_step_count} 'procedure_step' items. " + "Need at least 2 for a useful procedure." + ) + + return errors diff --git a/docs/plans/2026-03-06-procedural-flow-assist.md b/docs/plans/2026-03-06-procedural-flow-assist.md new file mode 100644 index 00000000..7b87055a --- /dev/null +++ b/docs/plans/2026-03-06-procedural-flow-assist.md @@ -0,0 +1,835 @@ +# Procedural Flow Assist — AI Chat Builder Support + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Make the Flow Assist AI chat builder correctly generate procedural/maintenance flows using the flat steps-array schema instead of the troubleshooting decision-tree schema. + +**Architecture:** The AI chat service (`ai_chat_service.py`) currently has hardcoded troubleshooting-specific prompts (schema, interview protocol, response format). We add parallel procedural versions and dispatch based on `flow_type`. The AI validator gets a procedural counterpart. The frontend gets a procedural steps preview component and the store/page handle the different data shape. + +**Tech Stack:** Python/FastAPI (backend), React/TypeScript (frontend), Zustand (state), Tailwind CSS (styling) + +--- + +## Context: Procedural vs Troubleshooting Structure + +**Troubleshooting** flows use a recursive tree: `{ id, type: "decision", question, options, children: [...] }` — branching paths ending in solution nodes. + +**Procedural** flows use a flat ordered array: `{ steps: [{ id, type, title, ... }, ...] }` — sequential steps with a `procedure_end` as the final step. + +### Procedural Step Schema +```json +{ + "id": "unique-slug", + "type": "procedure_step | procedure_end | section_header", + "title": "Step title", + "description": "Detailed instructions (supports [VAR:variable_name] interpolation)", + "content_type": "action | informational | verification | warning", + "estimated_minutes": 5, + "commands": [{ "code": "Get-Service ...", "label": "Check service", "language": "powershell" }], + "expected_outcome": "What success looks like", + "verification_prompt": "Confirm the service is running", + "verification_type": "checkbox | text_input", + "warning_text": "Caution text for warning content_type", + "notes_enabled": true, + "reference_url": "https://docs.microsoft.com/...", + "section_header": "Optional section label" +} +``` + +### Structural Rules +- `steps` array must be non-empty +- Each step needs `id`, `type`, `title` +- Valid types: `procedure_step`, `procedure_end`, `section_header` +- Exactly ONE `procedure_end` as the LAST step +- No duplicate step IDs +- `content_type` if present must be: `action`, `informational`, `verification`, `warning` +- Commands can be a string or array of `{ code, label?, language? }` + +### Intake Form (Optional) +Procedural flows can have an intake form that captures variables before execution. Fields use `variable_name` (e.g., `server_name`) referenced in step descriptions as `[VAR:server_name]`. + +--- + +## Task 1: Add Procedural System Prompts to AI Chat Service + +**Files:** +- Modify: `backend/app/core/ai_chat_service.py` + +**Step 1: Add procedural schema context constant** + +After the existing `SCHEMA_CONTEXT` constant (~line 78), add: + +```python +PROCEDURAL_SCHEMA_CONTEXT = """ +PROCEDURAL STEP SCHEMA — This is what you are building: + +Procedural flows are a FLAT ORDERED ARRAY of steps (NOT a branching tree). The structure is: +{"steps": [step1, step2, ..., end_step]} + +Each step has a "type" field: + +1. procedure_step — A task the engineer performs + Required: id (string), type ("procedure_step"), title (string) + Optional: description (string — detailed instructions, supports [VAR:variable_name] interpolation), + content_type ("action" | "informational" | "verification" | "warning"), + estimated_minutes (integer), + commands (array of {code: string, label?: string, language?: string}), + expected_outcome (string), + verification_prompt (string — question to confirm step completion), + verification_type ("checkbox" | "text_input"), + warning_text (string — caution text, used with content_type "warning"), + notes_enabled (boolean, default true), + reference_url (string — documentation link) + +2. section_header — A visual divider to organize steps into phases + Required: id (string), type ("section_header"), title (string) + Optional: description (string) + +3. procedure_end — The final completion marker (exactly ONE, always LAST) + Required: id (string), type ("procedure_end"), title (string) + Optional: description (string — completion summary text) + +CONTENT TYPES for procedure_step: +- "action" (default): Executable task with commands — shows terminal icon +- "informational": Read-only context or reference info — shows info icon +- "verification": Requires engineer confirmation before proceeding — shows checkmark icon +- "warning": Highlighted caution/danger step — shows alert icon + +STRUCTURAL RULES: +- Steps are executed in array order — position determines sequence +- All IDs must be unique strings (use descriptive slugs like "install-ad-ds-role") +- The LAST step MUST be type "procedure_end" +- Section headers group related steps visually but don't affect execution order +- Use [VAR:variable_name] in descriptions to reference intake form variables (e.g., "Configure IP on [VAR:server_name]") + +COMMAND FORMAT: +Commands are arrays of objects, each with: +- code (required): The exact command syntax (PowerShell, CMD, bash, etc.) +- label (optional): Short description of what the command does +- language (optional): "powershell", "cmd", "bash", etc. +""" + +PROCEDURAL_INTERVIEW_PROTOCOL = """ +INTERVIEW PHASES — Follow this progression: + +PHASE 1 - SCOPING (current_phase: scoping): +Understand what procedure this flow covers: +- What process is this flow for? (e.g., "new domain controller build", "Exchange migration", "firewall replacement") +- What is the target environment? (on-prem, hybrid, cloud, specific vendors?) +- Who will execute this? (Tier level, experience assumptions) +- What information will the engineer need before starting? (This becomes the intake form — server name, IP, domain, credentials, etc.) +Demonstrate expertise: "For a DC build, we'd typically need server name, IP, subnet, gateway, domain name, DSRM password, and whether this is the first DC or joining an existing domain." +DO NOT emit [STEPS_UPDATE] during scoping. + +PHASE 2 - DISCOVERY (current_phase: discovery): +Build out the procedure step by step: +- Establish the major phases (these become section_headers) +- For each phase, work through the steps in execution order +- Capture specific commands with exact syntax +- Add verification steps where the engineer should confirm something before proceeding +- Add warning steps for anything destructive or irreversible +EMIT [STEPS_UPDATE] when you and the user have agreed on concrete steps. Include ALL steps discussed so far. + +PHASE 3 - ENRICHMENT (current_phase: enrichment): +Circle back to improve existing steps: +- Add exact PowerShell/CLI commands with syntax +- Add expected_outcome for action steps +- Add verification prompts for critical checkpoints +- Add estimated_minutes for time-sensitive procedures +- Add reference_url links to relevant documentation +- Add warning_text for dangerous operations +- Suggest intake form variables for values that change per execution +EMIT [STEPS_UPDATE] when enriching steps. + +PHASE 4 - REVIEW (current_phase: review): +Present a summary: +- Total step count by content_type +- Section-by-section outline +- Estimated total time +- List of intake form variables suggested +- Flag any gaps or areas needing more detail +EMIT [STEPS_UPDATE] only if the user requests changes. + +TRANSITION between phases by emitting [PHASE:phase_name] when the conversation naturally moves to the next stage. +""" + +PROCEDURAL_RESPONSE_FORMAT = """ +RESPONSE FORMAT: + +Your response is natural conversational text. When the step structure changes, include structured markers that will be parsed by the system (the user will NOT see these markers): + +1. Steps update (only when structure changes — see phase rules above): +[STEPS_UPDATE] +{"steps": [...valid steps array...]} +[/STEPS_UPDATE] + +2. Phase transition (when moving to next phase): +[PHASE:discovery] + +3. Metadata capture (when you learn the flow's name, description, or tags): +[METADATA] +{"name": "...", "description": "...", "tags": ["..."]} +[/METADATA] + +4. Intake form suggestion (when you identify variables the engineer will need): +[INTAKE_FORM] +[{"variable_name": "server_name", "label": "Server Name", "field_type": "text", "required": true, "placeholder": "e.g., DC01", "group_name": "Server Details", "display_order": 1}] +[/INTAKE_FORM] + +IMPORTANT: +- Include [STEPS_UPDATE] sparingly. Only when concrete steps are established or modified. +- The steps update should be the COMPLETE working steps array, not a diff. +- Always include conversational text OUTSIDE the markers — never respond with only markers. +- The last step in the array MUST always be type "procedure_end". +""" +``` + +**Step 2: Update `_build_system_prompt` to dispatch by flow_type** + +Replace the existing `_build_system_prompt` function: + +```python +def _build_system_prompt(flow_type: str) -> str: + """Assemble the full system prompt for the chat builder.""" + if flow_type in ("procedural", "maintenance"): + flow_context = ( + f"The user wants to build a {'MAINTENANCE' if flow_type == 'maintenance' else 'PROCEDURAL'} flow — " + "a step-by-step process guide that walks engineers through a procedure in sequence. " + "Steps are executed in order, not branching paths." + ) + return f"{ROLE_PERSONA}\n\n{flow_context}\n\n{PROCEDURAL_SCHEMA_CONTEXT}\n\n{PROCEDURAL_INTERVIEW_PROTOCOL}\n\n{PROCEDURAL_RESPONSE_FORMAT}" + else: + flow_context = ( + "The user wants to build a TROUBLESHOOTING flow — a diagnostic decision tree " + "that guides engineers through symptom identification, diagnostic checks, and " + "resolution steps." + ) + return f"{ROLE_PERSONA}\n\n{flow_context}\n\n{SCHEMA_CONTEXT}\n\n{INTERVIEW_PROTOCOL}\n\n{RESPONSE_FORMAT}" +``` + +**Step 3: Update `_parse_ai_response` to handle `[STEPS_UPDATE]` and `[INTAKE_FORM]`** + +Add extraction for the new markers. After the `[METADATA]` extraction block: + +```python + # Extract [STEPS_UPDATE]...[/STEPS_UPDATE] + steps_match = re.search( + r"\[STEPS_UPDATE\]\s*([\s\S]*?)\s*\[/STEPS_UPDATE\]", result["content"] + ) + if steps_match: + try: + raw_json = _strip_markdown_fences(steps_match.group(1)) + result["tree_update"] = json.loads(raw_json) + except (json.JSONDecodeError, ValueError) as e: + logger.warning("Failed to parse steps update JSON: %s", e) + result["content"] = result["content"][: steps_match.start()] + result["content"][steps_match.end() :] + else: + truncated_steps = re.search(r"\[STEPS_UPDATE\][\s\S]*$", result["content"]) + if truncated_steps: + logger.warning("Truncated [STEPS_UPDATE] block detected — stripping from display") + result["content"] = result["content"][: truncated_steps.start()] + + # Extract [INTAKE_FORM]...[/INTAKE_FORM] + intake_match = re.search( + r"\[INTAKE_FORM\]\s*([\s\S]*?)\s*\[/INTAKE_FORM\]", result["content"] + ) + if intake_match: + try: + raw_json = _strip_markdown_fences(intake_match.group(1)) + result["intake_form"] = json.loads(raw_json) + except (json.JSONDecodeError, ValueError) as e: + logger.warning("Failed to parse intake form JSON: %s", e) + result["content"] = result["content"][: intake_match.start()] + result["content"][intake_match.end() :] + else: + truncated_intake = re.search(r"\[INTAKE_FORM\][\s\S]*$", result["content"]) + if truncated_intake: + logger.warning("Truncated [INTAKE_FORM] block detected — stripping from display") + result["content"] = result["content"][: truncated_intake.start()] +``` + +Also add `"intake_form": None` to the initial `result` dict. + +**Step 4: Update `send_message` to validate procedural structure** + +In `send_message()`, replace the tree_update validation block (~line 320-326): + +```python + # Validate tree update if present + tree_update = parsed["tree_update"] + if tree_update: + if session.flow_type in ("procedural", "maintenance"): + # Procedural: must have a steps array + if not isinstance(tree_update, dict) or not isinstance(tree_update.get("steps"), list): + logger.warning("AI steps update rejected: must have a steps array") + tree_update = None + else: + # Troubleshooting: root must be a decision node + if not isinstance(tree_update, dict) or tree_update.get("type") != "decision": + logger.warning("AI tree update rejected: root must be a decision node") + tree_update = None + elif not tree_update.get("id"): + logger.warning("AI tree update rejected: root node missing id") + tree_update = None +``` + +Also handle intake_form persistence after the metadata block: + +```python + if parsed.get("intake_form"): + session.intake_form_draft = parsed["intake_form"] +``` + +Wait — `AIChatSession` may not have an `intake_form_draft` field. We'll store it in `tree_metadata` instead: + +```python + if parsed.get("intake_form"): + merged = dict(session.tree_metadata) if session.tree_metadata else {} + merged["intake_form"] = parsed["intake_form"] + session.tree_metadata = merged +``` + +**Step 5: Update `generate_final_tree` for procedural flows** + +Replace the `generation_instruction` string with flow-type-aware instructions: + +```python + if session.flow_type in ("procedural", "maintenance"): + generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL procedural steps JSON for this flow. + +Requirements: +- Include ALL steps we discussed, organized into sections +- Use descriptive step IDs (slugs, not UUIDs) +- Each step needs: id, type, title, description +- Include commands with exact syntax where discussed +- Include content_type for each step (action, informational, verification, warning) +- Include estimated_minutes where discussed +- Include verification_prompt for verification steps +- Include warning_text for warning steps +- The LAST step MUST be type "procedure_end" +- Respond with ONLY the JSON — no conversational text, no markdown fences + +Format: {"steps": [step1, step2, ..., end_step]} + +Also provide metadata as a separate JSON object after the steps: +[METADATA] +{"name": "...", "description": "...", "tags": ["..."]} +[/METADATA] + +If we discussed intake form variables, include them: +[INTAKE_FORM] +[{"variable_name": "...", "label": "...", "field_type": "text", "required": true, "display_order": 1}] +[/INTAKE_FORM]""" + else: + generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL TreeStructure JSON for this flow. +...existing troubleshooting instruction...""" +``` + +Update the validation inside the generation loop to handle procedural: + +```python + if not tree: + # ... existing retry logic ... + + if session.flow_type in ("procedural", "maintenance"): + # Validate procedural structure + p_errors = validate_generated_procedural_steps(tree) + if p_errors: + if attempt == 0: + # ... retry with correction ... + continue + raise ValueError(f"Generated steps failed validation: {'; '.join(p_errors)}") + else: + errors = validate_generated_tree(tree) + if errors: + # ... existing retry logic ... +``` + +**Step 6: Run backend tests** + +Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts="` +Expected: All existing tests pass (they test troubleshooting flow). + +**Step 7: Commit** + +```bash +git add backend/app/core/ai_chat_service.py +git commit -m "feat: add procedural flow prompts to AI chat builder" +``` + +--- + +## Task 2: Add Procedural Validation to AI Tree Validator + +**Files:** +- Modify: `backend/app/core/ai_tree_validator.py` + +**Step 1: Add `validate_generated_procedural_steps` function** + +Add after the existing `count_tree_stats` function: + +```python +VALID_PROCEDURAL_STEP_TYPES = {"procedure_step", "procedure_end", "section_header"} +VALID_CONTENT_TYPES = {"action", "informational", "verification", "warning"} + + +def validate_generated_procedural_steps(tree: dict[str, Any]) -> list[str]: + """Validate an AI-generated procedural steps structure. + + Returns a list of error strings. Empty list means valid. + """ + errors: list[str] = [] + + if not isinstance(tree, dict): + return ["Steps structure must be a JSON object"] + + steps = tree.get("steps") + if not isinstance(steps, list) or len(steps) == 0: + return ["Must have a non-empty 'steps' array"] + + seen_ids: set[str] = set() + end_count = 0 + step_count = 0 + + for i, step in enumerate(steps): + if not isinstance(step, dict): + errors.append(f"Step at index {i} is not an object") + continue + + step_id = step.get("id") + step_type = step.get("type") + + # Check ID + if not step_id: + errors.append(f"Step at index {i} missing 'id'") + elif step_id in seen_ids: + errors.append(f"Duplicate step ID: '{step_id}'") + else: + seen_ids.add(step_id) + + # Check type + if step_type not in VALID_PROCEDURAL_STEP_TYPES: + errors.append( + f"Step '{step_id or i}' has invalid type '{step_type}'. " + f"Must be one of: {', '.join(sorted(VALID_PROCEDURAL_STEP_TYPES))}" + ) + continue + + # Check title + if not step.get("title"): + errors.append(f"Step '{step_id}' missing 'title'") + + # Content type validation + content_type = step.get("content_type") + if content_type and content_type not in VALID_CONTENT_TYPES: + errors.append( + f"Step '{step_id}' has invalid content_type '{content_type}'. " + f"Must be one of: {', '.join(sorted(VALID_CONTENT_TYPES))}" + ) + + if step_type == "procedure_step": + step_count += 1 + elif step_type == "procedure_end": + end_count += 1 + + # Structural checks + if end_count == 0: + errors.append("Must have exactly one 'procedure_end' step as the last step") + elif end_count > 1: + errors.append(f"Found {end_count} procedure_end steps, must have exactly 1") + + if end_count == 1 and steps[-1].get("type") != "procedure_end": + errors.append("The procedure_end step must be the last step in the array") + + if step_count < 2: + errors.append( + f"Flow has only {step_count} procedure steps. " + "Need at least 2 for a useful procedure." + ) + + if len(steps) > 100: + errors.append(f"Flow has {len(steps)} steps. Maximum 100 allowed.") + + return errors +``` + +**Step 2: Run tests** + +Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/ -k "procedural" -v --override-ini="addopts="` +Expected: Existing procedural tests pass. + +**Step 3: Commit** + +```bash +git add backend/app/core/ai_tree_validator.py +git commit -m "feat: add procedural steps validator for AI-generated flows" +``` + +--- + +## Task 3: Handle Intake Form + Procedural Import in AI Chat Endpoint + +**Files:** +- Modify: `backend/app/api/endpoints/ai_chat.py` + +**Step 1: Update the `import_tree` endpoint to handle intake form from metadata** + +In the `import_tree` function (~line 393), after building the Tree object, check for intake form: + +```python + # Extract intake form from metadata if present + intake_form = None + if metadata.get("intake_form"): + intake_form = metadata.pop("intake_form") + + tree = Tree( + name=data.name or metadata.get("name", "AI-Generated Flow"), + description=data.description or metadata.get("description", ""), + tree_type=session.flow_type, + tree_structure=session.working_tree, + intake_form=intake_form, + author_id=current_user.id, + account_id=current_user.account_id, + category_id=data.category_id, + is_public=False, + ) +``` + +**Step 2: Run tests** + +Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts="` +Expected: All tests pass. + +**Step 3: Commit** + +```bash +git add backend/app/api/endpoints/ai_chat.py +git commit -m "feat: handle intake form in AI chat procedural import" +``` + +--- + +## Task 4: Add Procedural Steps Preview Component (Frontend) + +**Files:** +- Create: `frontend/src/components/ai-chat/StaticStepsPreview.tsx` + +**Step 1: Create the procedural steps preview component** + +```tsx +import type { ProceduralStep } from '@/types' +import { Terminal, Info, CheckSquare, AlertTriangle, LayoutList } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface StaticStepsPreviewProps { + steps: ProceduralStep[] + name?: string +} + +const CONTENT_TYPE_ICONS: Record = { + action: Terminal, + informational: Info, + verification: CheckSquare, + warning: AlertTriangle, +} + +export function StaticStepsPreview({ steps, name }: StaticStepsPreviewProps) { + let stepNumber = 0 + + return ( +
+
+

+ Preview: {name || 'Untitled Flow'} +

+

+ {steps.filter((s) => s.type === 'procedure_step').length} steps +

+
+
+
+ {steps.map((step) => { + if (step.type === 'section_header') { + return ( +
+
+ + + {step.title} + +
+
+ ) + } + + if (step.type === 'procedure_end') { + return ( +
+ + {step.title || 'Procedure Complete'} + +
+ ) + } + + stepNumber++ + const contentType = step.content_type || 'action' + const Icon = CONTENT_TYPE_ICONS[contentType] || Terminal + + return ( +
+
+ + {stepNumber} + +
+
+ + + {step.title} + +
+ {step.commands && ( +
+ + + {Array.isArray(step.commands) ? step.commands.length : 1} command{(Array.isArray(step.commands) ? step.commands.length : 1) !== 1 ? 's' : ''} + +
+ )} +
+ {step.estimated_minutes && ( + + ~{step.estimated_minutes}m + + )} +
+
+ ) + })} +
+
+
+ ) +} +``` + +**Step 2: Run build** + +Run: `cd /home/michaelchihlas/dev/patherly/frontend && npm run build` +Expected: Build passes. + +**Step 3: Commit** + +```bash +git add frontend/src/components/ai-chat/StaticStepsPreview.tsx +git commit -m "feat: add procedural steps preview component for AI chat builder" +``` + +--- + +## Task 5: Update AI Chat Store + Page for Procedural Flows + +**Files:** +- Modify: `frontend/src/store/aiChatStore.ts` +- Modify: `frontend/src/pages/AIChatBuilderPage.tsx` + +**Step 1: Update `AIChatState` interface and `sendMessage` handler in store** + +In `aiChatStore.ts`, update the `workingTree` type to also accept procedural structure: + +```typescript +// Change line 29: +workingTree: TreeStructure | { steps: ProceduralStep[] } | null +// Change line 33: +generatedTree: TreeStructure | { steps: ProceduralStep[] } | null +``` + +Add `ProceduralStep` to the imports: + +```typescript +import type { + ChatMessage, + InterviewPhase, + TreeStructure, + ProceduralStep, +} from '@/types' +``` + +Update `sendMessage` (~line 121-127) — the response handling already works because `working_tree` is stored as-is from the API. The cast just needs updating: + +```typescript +workingTree: (response.working_tree as TreeStructure | { steps: ProceduralStep[] } | null) ?? state.workingTree, +``` + +And in `generateTree` (~line 142-143): + +```typescript +generatedTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] }, +workingTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] }, +``` + +And in `resumeSession` (~line 185-187): + +```typescript +workingTree: session.working_tree as TreeStructure | { steps: ProceduralStep[] } | null, +generatedTree: session.generated_tree as TreeStructure | { steps: ProceduralStep[] } | null, +``` + +**Step 2: Update `AIChatBuilderPage.tsx` to render correct preview** + +Add import for `StaticStepsPreview` and `ProceduralStep`: + +```typescript +import { StaticStepsPreview } from '@/components/ai-chat/StaticStepsPreview' +import type { ProceduralStep } from '@/types' +``` + +Replace the preview tree logic (~line 116) and preview render (~line 143-151): + +```typescript + const previewData = generatedTree || workingTree + + // Determine if this is a procedural preview + const isProceduralPreview = previewData && 'steps' in previewData + + // ... in the JSX: + {previewData ? ( + isProceduralPreview ? ( + + ) : ( + + ) + ) : ( + + )} +``` + +Remove the now-unused `const previewTree = (generatedTree || workingTree) as TreeStructure | null` line. + +**Step 3: Run build** + +Run: `cd /home/michaelchihlas/dev/patherly/frontend && npm run build` +Expected: Build passes. + +**Step 4: Commit** + +```bash +git add frontend/src/store/aiChatStore.ts frontend/src/pages/AIChatBuilderPage.tsx +git commit -m "feat: wire procedural steps preview into AI chat builder page" +``` + +--- + +## Task 6: Update `generate_final_tree` Generation + Validation Wiring + +**Files:** +- Modify: `backend/app/core/ai_chat_service.py` + +This task ensures the full `generate_final_tree` function properly handles the procedural path end-to-end, including the retry loop and validation import. + +**Step 1: Add import for the new validator** + +```python +from app.core.ai_tree_validator import validate_generated_tree, validate_generated_procedural_steps +``` + +**Step 2: Update the validation block in `generate_final_tree`** + +Inside the `for attempt in range(2)` loop, after tree is extracted, replace the validation block: + +```python + if session.flow_type in ("procedural", "maintenance"): + val_errors = validate_generated_procedural_steps(tree) + else: + val_errors = validate_generated_tree(tree) + + if val_errors: + if attempt == 0: + provider_messages.append({"role": "assistant", "content": response_text}) + correction = ( + f"The generated structure has validation errors: {'; '.join(val_errors)}. " + "Please fix these issues and respond with the corrected JSON only." + ) + provider_messages.append({"role": "user", "content": correction}) + continue + raise ValueError(f"Generated structure failed validation: {'; '.join(val_errors)}") +``` + +**Step 3: Handle intake form from final generation** + +After the `# Success` comment, before returning: + +```python + # Extract intake form from metadata if present + if parsed.get("intake_form") and isinstance(parsed["intake_form"], list): + metadata["intake_form"] = parsed["intake_form"] +``` + +**Step 4: Run backend tests** + +Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts="` +Expected: All tests pass. + +**Step 5: Commit** + +```bash +git add backend/app/core/ai_chat_service.py +git commit -m "feat: wire procedural validation into AI chat generate flow" +``` + +--- + +## Task 7: Final Integration Test + Build Verification + +**Step 1: Run full backend test suite** + +Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/ --override-ini="addopts=" -v` +Expected: All tests pass. + +**Step 2: Run frontend build** + +Run: `cd /home/michaelchihlas/dev/patherly/frontend && npm run build` +Expected: Build passes with zero errors. + +**Step 3: Final commit (if any remaining changes)** + +```bash +git add -A +git commit -m "chore: final cleanup for procedural Flow Assist support" +``` + +--- + +## Summary of Changes + +| File | Change | +|------|--------| +| `backend/app/core/ai_chat_service.py` | Add procedural schema/protocol/format prompts, dispatch by flow_type, parse `[STEPS_UPDATE]` + `[INTAKE_FORM]`, validate procedural structure, procedural generation instruction | +| `backend/app/core/ai_tree_validator.py` | Add `validate_generated_procedural_steps()` function | +| `backend/app/api/endpoints/ai_chat.py` | Handle intake form in import endpoint | +| `frontend/src/components/ai-chat/StaticStepsPreview.tsx` | New procedural steps preview component | +| `frontend/src/store/aiChatStore.ts` | Widen `workingTree`/`generatedTree` types for procedural | +| `frontend/src/pages/AIChatBuilderPage.tsx` | Render `StaticStepsPreview` for procedural flows | diff --git a/frontend/src/components/ai-chat/StaticStepsPreview.tsx b/frontend/src/components/ai-chat/StaticStepsPreview.tsx new file mode 100644 index 00000000..4f62140e --- /dev/null +++ b/frontend/src/components/ai-chat/StaticStepsPreview.tsx @@ -0,0 +1,112 @@ +import type { ProceduralStep } from '@/types' +import { Terminal, Info, CheckSquare, AlertTriangle, LayoutList } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface StaticStepsPreviewProps { + steps: ProceduralStep[] + name?: string +} + +const CONTENT_TYPE_ICONS: Record = { + action: Terminal, + informational: Info, + verification: CheckSquare, + warning: AlertTriangle, +} + +export function StaticStepsPreview({ steps, name }: StaticStepsPreviewProps) { + let stepNumber = 0 + + return ( +
+
+

+ Preview: {name || 'Untitled Flow'} +

+

+ {steps.filter((s) => s.type === 'procedure_step').length} steps +

+
+
+
+ {steps.map((step) => { + if (step.type === 'section_header') { + return ( +
+
+ + + {step.title} + +
+
+ ) + } + + if (step.type === 'procedure_end') { + return ( +
+ + {step.title || 'Procedure Complete'} + +
+ ) + } + + stepNumber++ + const contentType = step.content_type || 'action' + const Icon = CONTENT_TYPE_ICONS[contentType] || Terminal + + return ( +
+
+ + {stepNumber} + +
+
+ + + {step.title} + +
+ {step.commands && ( +
+ + + {Array.isArray(step.commands) ? step.commands.length : 1} command{(Array.isArray(step.commands) ? step.commands.length : 1) !== 1 ? 's' : ''} + +
+ )} +
+ {step.estimated_minutes && ( + + ~{step.estimated_minutes}m + + )} +
+
+ ) + })} +
+
+
+ ) +} diff --git a/frontend/src/pages/AIChatBuilderPage.tsx b/frontend/src/pages/AIChatBuilderPage.tsx index bcd3ba9b..20505ac9 100644 --- a/frontend/src/pages/AIChatBuilderPage.tsx +++ b/frontend/src/pages/AIChatBuilderPage.tsx @@ -5,10 +5,11 @@ import { ChatPanel } from '@/components/ai-chat/ChatPanel' import { ChatToolbar } from '@/components/ai-chat/ChatToolbar' import { EmptyPreview } from '@/components/ai-chat/EmptyPreview' import { StaticTreePreview } from '@/components/ai-chat/StaticTreePreview' +import { StaticStepsPreview } from '@/components/ai-chat/StaticStepsPreview' import { Spinner } from '@/components/common/Spinner' import { getTreeEditorPath } from '@/lib/routing' import { toast } from '@/lib/toast' -import type { TreeStructure } from '@/types' +import type { TreeStructure, ProceduralStep } from '@/types' export function AIChatBuilderPage() { const navigate = useNavigate() @@ -113,7 +114,8 @@ export function AIChatBuilderPage() { ) } - const previewTree = (generatedTree || workingTree) as TreeStructure | null + const previewData = generatedTree || workingTree + const isProceduralPreview = previewData && 'steps' in previewData return (
@@ -139,13 +141,20 @@ export function AIChatBuilderPage() { />
- {/* Right panel: Tree preview (40%) — hidden below 1024px */} + {/* Right panel: Preview (40%) — hidden below 1024px */}
- {previewTree ? ( - + {previewData ? ( + isProceduralPreview ? ( + + ) : ( + + ) ) : ( )} diff --git a/frontend/src/store/aiChatStore.ts b/frontend/src/store/aiChatStore.ts index 987c8ff0..338811ec 100644 --- a/frontend/src/store/aiChatStore.ts +++ b/frontend/src/store/aiChatStore.ts @@ -5,6 +5,7 @@ import type { ChatMessage, InterviewPhase, TreeStructure, + ProceduralStep, } from '@/types' interface TreeMetadata { @@ -25,12 +26,12 @@ interface AIChatState { messages: ChatMessage[] isResponding: boolean - // Progressive tree - workingTree: TreeStructure | null + // Progressive tree (troubleshooting = TreeStructure, procedural = {steps}) + workingTree: TreeStructure | { steps: ProceduralStep[] } | null treeMetadata: TreeMetadata | null // Final generation - generatedTree: TreeStructure | null + generatedTree: TreeStructure | { steps: ProceduralStep[] } | null isGenerating: boolean importedTreeId: string | null @@ -121,7 +122,7 @@ export const useAIChatStore = create((set, get) => ({ set((state) => ({ messages: [...state.messages, aiMessage], currentPhase: response.current_phase, - workingTree: (response.working_tree as TreeStructure | null) ?? state.workingTree, + workingTree: (response.working_tree as TreeStructure | { steps: ProceduralStep[] } | null) ?? state.workingTree, treeMetadata: (response.tree_metadata as TreeMetadata | null) ?? state.treeMetadata, isResponding: false, })) @@ -139,8 +140,8 @@ export const useAIChatStore = create((set, get) => ({ try { const response = await aiChatApi.generateTree(sessionId) set({ - generatedTree: response.tree_structure as unknown as TreeStructure, - workingTree: response.tree_structure as unknown as TreeStructure, + generatedTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] }, + workingTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] }, treeMetadata: response.tree_metadata as TreeMetadata, status: 'completed', isGenerating: false, @@ -182,9 +183,9 @@ export const useAIChatStore = create((set, get) => ({ currentPhase: session.current_phase, flowType: session.flow_type, messages: session.conversation_history as ChatMessage[], - workingTree: session.working_tree as TreeStructure | null, + workingTree: session.working_tree as TreeStructure | { steps: ProceduralStep[] } | null, treeMetadata: session.tree_metadata as TreeMetadata | null, - generatedTree: session.generated_tree as TreeStructure | null, + generatedTree: session.generated_tree as TreeStructure | { steps: ProceduralStep[] } | null, isResponding: false, }) } catch (e: unknown) {