diff --git a/frontend/src/pages/DevBranchingPage.tsx b/frontend/src/pages/DevBranchingPage.tsx new file mode 100644 index 00000000..db49bd7c --- /dev/null +++ b/frontend/src/pages/DevBranchingPage.tsx @@ -0,0 +1,577 @@ +/** + * Dev test page for Conversational Branching components. + * Route: /dev/branching + * + * Renders all branching components with mock data — no API calls. + * Use this to validate visual design before wiring into FlowPilotSessionPage. + */ +import { useState } from 'react' +import { BranchMap } from '@/components/session/BranchMap' +import { ForkCard } from '@/components/session/ForkCard' +import { BranchTransitionBar } from '@/components/session/BranchTransitionBar' +import { BranchRevivalCard } from '@/components/session/BranchRevivalCard' +import { HandoffModal } from '@/components/session/HandoffModal' +import type { + BranchResponse, + ForkPointResponse, + ResolutionOutputResponse, + ResolutionOutputType, +} from '@/types/branching' +import { cn } from '@/lib/utils' +import { toast } from '@/lib/toast' +import { + FileText, + BookOpen, + MessageSquare, + Pencil, + Copy, + Send, + Check, +} from 'lucide-react' + +// ── Mock Data ── + +const SESSION_ID = 'mock-session-001' +const ROOT_ID = 'branch-root' +const BRANCH_NET = 'branch-network' +const BRANCH_DNS = 'branch-dns' +const BRANCH_DHCP = 'branch-dhcp' +const BRANCH_REVIVED = 'branch-revived-firewall' + +const MOCK_BRANCHES: BranchResponse[] = [ + { + id: ROOT_ID, + session_id: SESSION_ID, + parent_branch_id: null, + fork_point_step_id: null, + branch_order: 1, + label: 'Root — Initial Investigation', + status: 'active', + status_reason: null, + status_changed_at: null, + context_summary: { + tried: ['Checked event logs', 'Ran ipconfig'], + concluded: 'Multiple possible causes identified', + artifacts: [], + }, + evidence_from_branch_id: null, + evidence_description: null, + step_count: 4, + created_at: '2026-03-24T10:00:00Z', + updated_at: '2026-03-24T10:05:00Z', + }, + { + id: BRANCH_NET, + session_id: SESSION_ID, + parent_branch_id: ROOT_ID, + fork_point_step_id: null, + branch_order: 1, + label: 'Network Connectivity', + status: 'dead_end', + status_reason: 'Ping to gateway successful, NIC is healthy', + status_changed_at: '2026-03-24T10:12:00Z', + context_summary: { + tried: ['Ping gateway', 'Check NIC status', 'Traceroute'], + concluded: 'Network layer is not the issue', + artifacts: ['traceroute.txt'], + }, + evidence_from_branch_id: null, + evidence_description: null, + step_count: 3, + created_at: '2026-03-24T10:05:00Z', + updated_at: '2026-03-24T10:12:00Z', + }, + { + id: BRANCH_DNS, + session_id: SESSION_ID, + parent_branch_id: ROOT_ID, + fork_point_step_id: null, + branch_order: 2, + label: 'DNS Resolution', + status: 'solved', + status_reason: 'DNS cache was poisoned — flushed and resolved', + status_changed_at: '2026-03-24T10:20:00Z', + context_summary: { + tried: ['nslookup', 'ipconfig /flushdns', 'Checked DNS server config'], + concluded: 'Root cause: stale DNS cache entry for internal domain', + artifacts: ['nslookup-output.txt'], + }, + evidence_from_branch_id: null, + evidence_description: null, + step_count: 5, + created_at: '2026-03-24T10:05:00Z', + updated_at: '2026-03-24T10:20:00Z', + }, + { + id: BRANCH_DHCP, + session_id: SESSION_ID, + parent_branch_id: ROOT_ID, + fork_point_step_id: null, + branch_order: 3, + label: 'DHCP Lease Issue', + status: 'untried', + status_reason: null, + status_changed_at: null, + context_summary: null, + evidence_from_branch_id: null, + evidence_description: null, + step_count: 0, + created_at: '2026-03-24T10:05:00Z', + updated_at: '2026-03-24T10:05:00Z', + }, + { + id: BRANCH_REVIVED, + session_id: SESSION_ID, + parent_branch_id: BRANCH_NET, + fork_point_step_id: null, + branch_order: 1, + label: 'Firewall Rules', + status: 'revived', + status_reason: null, + status_changed_at: '2026-03-24T10:25:00Z', + context_summary: { + tried: ['Checked Windows Firewall'], + concluded: 'Revisiting after DNS fix revealed blocked outbound on port 443', + artifacts: [], + }, + evidence_from_branch_id: BRANCH_DNS, + evidence_description: 'DNS fix revealed that port 443 is being blocked by a new firewall rule pushed via GPO', + step_count: 2, + created_at: '2026-03-24T10:15:00Z', + updated_at: '2026-03-24T10:25:00Z', + }, +] + +const MOCK_FORK: ForkPointResponse = { + id: 'fork-001', + session_id: SESSION_ID, + parent_branch_id: ROOT_ID, + trigger_step_id: null, + fork_reason: + "Based on the symptoms (intermittent connectivity loss affecting only this workstation), I see three possible causes. Let's investigate each:", + options: [ + { + label: 'Network Connectivity', + description: 'NIC driver issue, cable fault, or switch port problem', + branch_id: BRANCH_NET, + status: 'dead_end', + }, + { + label: 'DNS Resolution', + description: 'Stale cache, wrong DNS server, or poisoned entry', + branch_id: BRANCH_DNS, + status: 'solved', + }, + { + label: 'DHCP Lease Issue', + description: 'Lease expiry, scope exhaustion, or relay agent failure', + branch_id: BRANCH_DHCP, + status: 'untried', + }, + ], + created_at: '2026-03-24T10:05:00Z', +} + +const MOCK_OUTPUTS: ResolutionOutputResponse[] = [ + { + id: 'output-psa', + session_id: SESSION_ID, + output_type: 'psa_ticket_notes', + generated_content: `## Problem +User reported intermittent connectivity loss on workstation WS-1042 in the Accounting department. + +## Diagnostic Steps +1. **Network Connectivity** [Dead End] — Ping to gateway successful (avg 2ms), NIC healthy, traceroute clean. Network layer ruled out. +2. **DNS Resolution** [Solved] — nslookup for internal domain \`erp.contoso.local\` returned stale IP (10.0.1.50 instead of 10.0.2.50). DNS cache poisoned by a cached entry from before the ERP server migration last Tuesday. +3. **Firewall Rules** [Revived] — DNS fix revealed that a new GPO-pushed firewall rule was blocking outbound port 443. Rule was added by the security team on 3/22 without change advisory. + +## Resolution +- Flushed DNS cache: \`ipconfig /flushdns\` +- Cleared stale DNS record on domain controller +- Firewall rule exception added for ERP traffic +- Verified connectivity stable for 15 minutes post-fix + +## Recommendations +- Add DNS record validation to the ERP migration runbook +- Request change advisory process for GPO firewall rule changes`, + structured_data: null, + edited_content: null, + status: 'draft', + pushed_to: null, + pushed_at: null, + pushed_reference: null, + generated_by_model: 'claude-sonnet-4-6', + created_at: '2026-03-24T10:22:00Z', + updated_at: '2026-03-24T10:22:00Z', + }, + { + id: 'output-kb', + session_id: SESSION_ID, + output_type: 'knowledge_base', + generated_content: `# Intermittent Connectivity After Server Migration + +## Symptoms +- Workstation loses connectivity to internal applications intermittently +- External sites work normally +- Affects single workstation or small group + +## Root Cause +Stale DNS cache entries pointing to pre-migration IP addresses. Often occurs 1-7 days after internal server migrations when DNS TTLs haven't expired. + +## Things to Rule Out First +- **Network layer** — If ping to gateway succeeds and traceroute is clean, skip to DNS +- **DHCP** — Only investigate if ipconfig shows APIPA address (169.254.x.x) + +## Resolution Steps +1. Run \`nslookup \` — compare returned IP with current server IP +2. If mismatched: \`ipconfig /flushdns\` on the workstation +3. Check AD DNS for stale records: \`dnscmd /enumrecords contoso.local \` +4. Delete stale record if found +5. Verify: \`nslookup \` returns correct IP + +## Tags +DNS, migration, connectivity, cache, stale-record`, + structured_data: { + symptoms: ['Intermittent connectivity to internal apps', 'External sites work'], + root_cause: 'Stale DNS cache from server migration', + steps: ['nslookup', 'flushdns', 'Check AD DNS', 'Delete stale record'], + tags: ['DNS', 'migration', 'connectivity'], + }, + edited_content: null, + status: 'draft', + pushed_to: null, + pushed_at: null, + pushed_reference: null, + generated_by_model: 'claude-sonnet-4-6', + created_at: '2026-03-24T10:22:00Z', + updated_at: '2026-03-24T10:22:00Z', + }, + { + id: 'output-client', + session_id: SESSION_ID, + output_type: 'client_summary', + generated_content: `Hi, + +We've resolved the connectivity issue on your workstation. Here's what happened: + +After your company's ERP server was moved to a new address last week, your computer was still trying to reach it at the old address. This caused the intermittent connection drops you were experiencing when accessing internal applications. + +We've updated the address records and cleared the old cached information on your machine. We also found and fixed a secondary issue where a recently applied security policy was blocking some web traffic. + +Your connectivity should now be stable. If you experience any further issues, please don't hesitate to reach out.`, + structured_data: null, + edited_content: null, + status: 'draft', + pushed_to: null, + pushed_at: null, + pushed_reference: null, + generated_by_model: 'claude-sonnet-4-6', + created_at: '2026-03-24T10:22:00Z', + updated_at: '2026-03-24T10:22:00Z', + }, +] + +// ── Mock Resolution Panel (self-contained, no API calls) ── + +const TABS: Array<{ type: ResolutionOutputType; label: string; icon: React.ElementType }> = [ + { type: 'psa_ticket_notes', label: 'PSA Notes', icon: FileText }, + { type: 'knowledge_base', label: 'KB Article', icon: BookOpen }, + { type: 'client_summary', label: 'Client Summary', icon: MessageSquare }, +] + +function MockResolutionPanel({ outputs }: { outputs: ResolutionOutputResponse[] }) { + const [activeType, setActiveType] = useState('psa_ticket_notes') + const [isEditing, setIsEditing] = useState(false) + const [editValue, setEditValue] = useState('') + const [copied, setCopied] = useState(false) + + const activeOutput = outputs.find(o => o.output_type === activeType) ?? null + const displayContent = activeOutput?.edited_content ?? activeOutput?.generated_content ?? '' + + function handleEditToggle() { + if (!isEditing) { + setEditValue(displayContent) + setIsEditing(true) + } else { + setIsEditing(false) + setEditValue('') + } + } + + async function handleCopy() { + try { + await navigator.clipboard.writeText(displayContent) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error('Failed to copy') + } + } + + return ( +
+ {/* Tab bar */} +
+ {TABS.map(tab => { + const Icon = tab.icon + const isActive = tab.type === activeType + return ( + + ) + })} +
+ + {/* Content */} +
+ {!activeOutput ? ( +
+ No output generated yet. +
+ ) : isEditing ? ( +