const { useState, useCallback, useEffect, useMemo, useRef } = React; const { ReactFlow, ReactFlowProvider, useNodesState, useEdgesState, useReactFlow, Handle, Position, MarkerType, addEdge, Background, Controls, MiniMap } = window.ReactFlow; // ─── Constants ───────────────────────────────────────────────────────────── const NODE_TYPES_CONFIG = { process: { bg: "#DBEAFE", border: "#3B82F6", icon: "⚙️", label: "Process" }, queue: { bg: "#FEF3C7", border: "#F59E0B", icon: "📦", label: "Queue" }, inspection: { bg: "#FEE2E2", border: "#EF4444", icon: "🔍", label: "Inspection" }, changeover: { bg: "#FCE7F3", border: "#EC4899", icon: "🔄", label: "Changeover" }, material: { bg: "#DCFCE7", border: "#22C55E", icon: "🏭", label: "Material" }, }; const NODE_SCHEMAS = { process: [ { name: "Step Name", dataType: "text", source: "interview", confidence: "high", question: "What do you call this step? What's happening to the part here?" }, { name: "Cycle Time", dataType: "duration", source: "interview", confidence: "medium", question: "If everything's going normally, how long from when material arrives to when it leaves?" }, { name: "Operator Count", dataType: "integer", source: "interview", confidence: "high", question: "How many people are working at this station at the same time?" }, { name: "Uptime / Availability", dataType: "percentage", source: "observation", confidence: "low", question: "How often does this step stop unexpectedly? What causes it?" }, { name: "Common Failure Modes", dataType: "text_list", source: "interview", confidence: "high", question: "What's the most common reason this step slows down or stops?" }, { name: "Output Unit Definition", dataType: "text", source: "interview", confidence: "high", question: "What does one finished piece look like when it leaves your station?" } ], queue: [ { name: "Location", dataType: "text", source: "interview", confidence: "high", question: "Where does material sit between this step and the next?" }, { name: "Typical WIP Volume", dataType: "count", source: "observation", confidence: "low", question: "How many pieces are usually sitting there waiting?" }, { name: "Wait Time", dataType: "duration", source: "observation", confidence: "low", question: "How long does a part usually wait here before moving forward?" }, { name: "Wait Cause", dataType: "text", source: "interview", confidence: "high", question: "Why does material pile up here — is the next step slower, or something else?" }, { name: "FIFO / LIFO Discipline", dataType: "enum", source: "interview", confidence: "high", question: "When you pick up material, do you take from the top, the bottom, or wherever is easiest?" } ], inspection: [ { name: "What Is Being Checked", dataType: "text_list", source: "interview", confidence: "high", question: "What are you looking for? What would make you reject a piece?" }, { name: "Inspection Method", dataType: "text", source: "interview", confidence: "high", question: "How do you check it — by eye, with a tool, a gauge, a machine?" }, { name: "Defect / Rejection Rate", dataType: "percentage", source: "records", confidence: "low", question: "Out of 100 pieces, roughly how many get rejected?" }, { name: "Rework Path", dataType: "text", source: "interview", confidence: "high", question: "When something gets rejected, where does it go? Is it fixed here, sent back, or scrapped?" }, { name: "Inspection Cycle Time", dataType: "duration", source: "observation", confidence: "medium", question: "How long does it take to check one piece?" } ], changeover: [ { name: "Changeover Trigger", dataType: "text", source: "interview", confidence: "high", question: "What causes this station to switch over — different product, size, material?" }, { name: "Estimated Duration", dataType: "duration", source: "observation", confidence: "low", question: "How long does it take to switch over?" }, { name: "Changeover Frequency", dataType: "frequency", source: "interview", confidence: "high", question: "How many times per shift does this switchover happen?" }, { name: "Internal vs External Tasks", dataType: "text_list", source: "interview", confidence: "high", question: "While the machine is stopped, what's happening? Are people gathering tools or adjusting the machine itself?" }, { name: "Tooling Required", dataType: "text_list", source: "interview", confidence: "high", question: "What tools or materials do you need ready before the changeover can start?" }, { name: "First-Pass Quality", dataType: "count", source: "records", confidence: "low", question: "How many pieces do you run after a changeover before you're confident the output is good?" }, { name: "Minimum Batch Size", dataType: "count", source: "interview", confidence: "high", question: "What's the smallest run you'd want before switching over again?" } ], material: [ { name: "Supplier / Source", dataType: "text", source: "interview", confidence: "high", question: "Where does this material come from — outside supplier, warehouse, or another part of the facility?" }, { name: "Delivery Frequency", dataType: "frequency", source: "records", confidence: "medium", question: "How often does this material arrive?" }, { name: "Lead Time", dataType: "duration", source: "interview", confidence: "medium", question: "If you ran out today and ordered more, how long before it arrives?" }, { name: "Incoming Quality Issues", dataType: "text", source: "interview", confidence: "high", question: "Does this material ever arrive in a condition that causes problems downstream?" }, { name: "Safety Stock Level", dataType: "count", source: "observation", confidence: "medium", question: "How much of this material is usually on hand at any given time?" } ] }; // ─── Helpers ──────────────────────────────────────────────────────────────── function makeNodeId() { return "node_" + Date.now() + "_" + Math.random().toString(36).slice(2, 7); } function makeEdgeId() { return "edge_" + Date.now() + "_" + Math.random().toString(36).slice(2, 7); } function buildDefaultFields(nodeType) { const schema = NODE_SCHEMAS[nodeType] || []; const fields = {}; schema.forEach(f => { fields[f.name] = { value: "", source: f.source, confidence: f.confidence, verified: false }; }); return fields; } function createNode(nodeType, position) { const cfg = NODE_TYPES_CONFIG[nodeType]; return { id: makeNodeId(), type: nodeType, position, data: { nodeType, label: cfg.label + " " + (Math.floor(Math.random() * 90) + 10), fields: buildDefaultFields(nodeType), interviewHistory: [], dismissedPrompts: [] } }; } function parseDurationToMinutes(val) { if (!val) return null; const s = String(val).toLowerCase().trim(); const num = parseFloat(s); if (isNaN(num)) return null; if (s.includes("hour") || s.includes("hr")) return num * 60; if (s.includes("sec")) return num / 60; return num; // assume minutes } function getNodeStatus(nodeData) { const schemaFields = NODE_SCHEMAS[nodeData.nodeType] || []; const interviewFields = schemaFields.filter(f => f.source === "interview" && f.confidence === "high"); if (interviewFields.length === 0) return "green"; const filled = interviewFields.filter(f => { const v = nodeData.fields[f.name]?.value; return v !== undefined && v !== null && String(v).trim() !== ""; }); if (filled.length === 0) return "red"; if (filled.length === interviewFields.length) return "green"; return "amber"; } function extractJSON(text) { const stripped = text.replace(/```json\n?/g, "").replace(/```\n?/g, ""); const match = stripped.match(/\{[\s\S]*\}/); if (!match) return null; try { return JSON.parse(match[0]); } catch { return null; } } // ─── Discovery helpers ────────────────────────────────────────────────────── const MODEL_OPTIONS = [ { id: "nvidia/nemotron-3-nano-30b-a3b:free", label: "Nemotron Nano (Free)" }, { id: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash" }, { id: "anthropic/claude-3.5-haiku", label: "Claude 3.5 Haiku" }, { id: "openai/gpt-4o-mini", label: "GPT-4o Mini" }, ]; function normalizeNodeType(rawType) { const t = (rawType || "").toLowerCase().trim(); if (t.includes("queue") || t.includes("wait") || t.includes("buffer")) return "queue"; if (t.includes("inspect") || t.includes("check") || t.includes("quality")) return "inspection"; if (t.includes("change") || t.includes("setup") || t.includes("switch")) return "changeover"; if (t.includes("material") || t.includes("supply") || t.includes("raw") || t.includes("input")) return "material"; return "process"; } function generateDiagramFromDiscovery(jsonData) { const rawSteps = jsonData.process_steps || jsonData.nodes || []; if (rawSteps.length === 0) return null; const X_SPACING = 280; const Y_MATERIAL = 100; const Y_MAIN = 300; const Y_SECONDARY = 500; // Separate by type for lane-based positioning let mainIndex = 0; let matIndex = 0; let secIndex = 0; const rfNodes = []; const idMap = {}; // step id/index -> nodeId rawSteps.forEach((raw, i) => { const nodeType = normalizeNodeType(raw.type); const nodeId = makeNodeId(); const stepKey = raw.id || String(i); idMap[stepKey] = nodeId; // Also map by index idMap[String(i)] = nodeId; let position; if (nodeType === "material") { position = { x: matIndex * X_SPACING, y: Y_MATERIAL }; matIndex++; } else if (nodeType === "inspection" || nodeType === "changeover") { position = { x: secIndex * X_SPACING, y: Y_SECONDARY }; secIndex++; } else { position = { x: mainIndex * X_SPACING, y: Y_MAIN }; mainIndex++; } const fields = buildDefaultFields(nodeType); // Merge details from LLM if (raw.details) { const schema = NODE_SCHEMAS[nodeType] || []; Object.entries(raw.details).forEach(([key, val]) => { if (val === null || val === undefined || String(val).trim() === "") return; // Exact match first if (fields[key] !== undefined) { fields[key] = { ...fields[key], value: val }; return; } // Fuzzy match const match = schema.find(f => f.name.toLowerCase() === key.toLowerCase() || f.name.toLowerCase().includes(key.toLowerCase()) || key.toLowerCase().includes(f.name.toLowerCase().split(" ")[0]) ); if (match) { fields[match.name] = { ...fields[match.name], value: val }; } }); } // Set name into the appropriate label field if (raw.name) { const nameField = nodeType === "process" ? "Step Name" : nodeType === "queue" ? "Location" : nodeType === "inspection" ? "What Is Being Checked" : nodeType === "changeover" ? "Changeover Trigger" : nodeType === "material" ? "Supplier / Source" : null; if (nameField && fields[nameField] && !fields[nameField].value) { fields[nameField] = { ...fields[nameField], value: raw.name }; } } const cfg = NODE_TYPES_CONFIG[nodeType]; rfNodes.push({ id: nodeId, type: nodeType, position, data: { nodeType, label: raw.name || (cfg.label + " " + (i + 1)), fields, interviewHistory: [], dismissedPrompts: [] } }); }); // Build edges const rfEdges = []; const addedEdges = new Set(); // From explicit edges array if (jsonData.edges && Array.isArray(jsonData.edges)) { jsonData.edges.forEach(e => { const srcKey = String(e.from_index !== undefined ? e.from_index : e.from); const tgtKey = String(e.to_index !== undefined ? e.to_index : e.to); const sourceId = idMap[srcKey]; const targetId = idMap[tgtKey]; if (sourceId && targetId) { const key = sourceId + "->" + targetId; if (!addedEdges.has(key)) { addedEdges.add(key); rfEdges.push({ id: makeEdgeId(), source: sourceId, target: targetId, markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 } }); } } }); } // From connects_from in process_steps format if (rfEdges.length === 0) { rawSteps.forEach((raw, i) => { const targetId = idMap[raw.id || String(i)]; (raw.connects_from || []).forEach(fromRef => { const sourceId = idMap[String(fromRef)]; if (sourceId && targetId) { const key = sourceId + "->" + targetId; if (!addedEdges.has(key)) { addedEdges.add(key); rfEdges.push({ id: makeEdgeId(), source: sourceId, target: targetId, markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 } }); } } }); }); } // Fallback: if still no edges and all main-flow nodes, chain them sequentially if (rfEdges.length === 0 && rfNodes.length > 1) { const mainFlowNodes = rfNodes.filter(n => n.data.nodeType === "process" || n.data.nodeType === "queue" ); for (let i = 0; i < mainFlowNodes.length - 1; i++) { rfEdges.push({ id: makeEdgeId(), source: mainFlowNodes[i].id, target: mainFlowNodes[i + 1].id, markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 } }); } } return { nodes: rfNodes, edges: rfEdges }; } function buildDiscoverySystemPrompt() { return `You are a friendly process documentation assistant helping a manufacturing plant employee describe their production process so it can be mapped visually. YOUR GOAL: Understand their manufacturing process well enough to create a process flow diagram. You need to identify each step, what happens between steps, quality checks, equipment changeovers, and material inputs. CONVERSATION STRATEGY: 1. First, understand what they make and which product line to map. 2. Ask them to walk through the process from start to finish at a high level. 3. Then go back through each step and ask clarifying questions: - What exactly happens at each step? - How long does each step take and how many people work there? - Are there any waits or pile-ups of material between steps? - Are there any quality checks or inspections? - Does any equipment need to be switched over between product types? - What raw materials or components feed into the process? 4. For each step, try to learn approximate timing, staffing, and common problems. RULES: - Ask ONE question at a time. Keep questions short and conversational. - Never use technical lean/manufacturing jargon (no "VSM", "takt time", "WIP", "SMED", "kaizen", "muda", "kanban", "poka-yoke"). - Use plain language: "wait", "pile-up", "check", "switch over", "bottleneck". - Be encouraging and acknowledge what the user tells you. - Build up your understanding progressively. After each user reply, update your running model of the process. AFTER EACH USER MESSAGE, you MUST append a JSON code block (hidden from the user) with your current process model: \`\`\`json { "process_steps": [ { "id": "step_1", "type": "process", "name": "Descriptive step name", "connects_from": [], "details": {} } ], "ready_to_generate": false } \`\`\` STEP TYPES (use exactly these): - "process" — a value-adding work step - "queue" — a wait, buffer, or pile-up between steps - "inspection" — a quality check or test - "changeover" — equipment switching between products - "material" — a raw material or component input DETAILS FIELDS by type (use these exact keys when you have data): - process: "Step Name", "Cycle Time", "Operator Count", "Uptime / Availability", "Common Failure Modes", "Output Unit Definition" - queue: "Location", "Typical WIP Volume", "Wait Time", "Wait Cause", "FIFO / LIFO Discipline" - inspection: "What Is Being Checked", "Inspection Method", "Defect / Rejection Rate", "Rework Path", "Inspection Cycle Time" - changeover: "Changeover Trigger", "Estimated Duration", "Changeover Frequency", "Internal vs External Tasks", "Tooling Required", "First-Pass Quality", "Minimum Batch Size" - material: "Supplier / Source", "Delivery Frequency", "Lead Time", "Incoming Quality Issues", "Safety Stock Level" CONNECTIONS: Use "connects_from" to show which step(s) feed into each step. Use the step id values. The first step has an empty array. Set "ready_to_generate" to true ONLY when ALL of these are met: - You have identified at least 3 process steps - You have a clear flow from start to finish - You have asked about waits/queues between major steps - You have asked about quality checks - The user seems to have described the full process When ready_to_generate is true, tell the user something like: "Great, I think I have a good picture of your process! I've identified [N] steps from [first step] to [last step]. Click 'Generate Map' whenever you're ready, or keep telling me more details."`; } // ─── Toast Component ──────────────────────────────────────────────────────── function ToastContainer({ toasts, onDismiss }) { return (
{toasts.map(t => (
{t.message}
{t.actions && t.actions.map((a, i) => ( ))}
))}
); } // ─── Settings Modal ───────────────────────────────────────────────────────── function SettingsModal({ apiKey, modelId, onSave, onClose }) { const [key, setKey] = useState(apiKey); const [model, setModel] = useState(modelId); const [customModel, setCustomModel] = useState( MODEL_OPTIONS.some(m => m.id === modelId) ? "" : modelId ); const isCustom = !MODEL_OPTIONS.some(m => m.id === model); function handleSave() { onSave(key, isCustom && customModel ? customModel : model); onClose(); } return (
e.stopPropagation()}>

Settings

Configure your OpenRouter API key and model to enable AI features. Your key is stored only in browser memory.

setKey(e.target.value)} placeholder="sk-or-..." style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 8, padding: "10px 12px", fontSize: 14, boxSizing: "border-box", outline: "none" }} />

Get a free key at openrouter.ai

{(model === "__custom__" || isCustom) && ( { setCustomModel(e.target.value); setModel("__custom__"); }} placeholder="e.g. meta-llama/llama-3-70b" style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 8, padding: "10px 12px", fontSize: 14, boxSizing: "border-box", outline: "none", marginTop: 8 }} /> )}

Free tier: Nemotron Nano. For better results in Discovery, try a paid model.

); } // ─── Custom Node Components ───────────────────────────────────────────────── function CustomNodeBase({ data, selected, isBottleneck }) { const cfg = NODE_TYPES_CONFIG[data.nodeType] || NODE_TYPES_CONFIG.process; const status = getNodeStatus(data); const statusColor = status === "green" ? "#22C55E" : status === "amber" ? "#F59E0B" : "#EF4444"; return (
{isBottleneck && (
⚠️
)}
{cfg.icon}
{cfg.label}
{data.label}
{data.nodeType === "process" && data.fields["Cycle Time"]?.value && (
CT: {data.fields["Cycle Time"].value}
)} {data.nodeType === "queue" && data.fields["Wait Time"]?.value && (
Wait: {data.fields["Wait Time"].value}
)}
); } // We need a way to pass bottleneck info into nodes. We'll use a context. const BottleneckContext = React.createContext(new Set()); function ProcessNode(props) { const bottleneckIds = React.useContext(BottleneckContext); return ; } function QueueNode(props) { return ; } function InspectionNode(props) { return ; } function ChangeoverNode(props) { return ; } function MaterialNode(props) { return ; } const nodeTypes = { process: ProcessNode, queue: QueueNode, inspection: InspectionNode, changeover: ChangeoverNode, material: MaterialNode, }; // ─── Right Panel ──────────────────────────────────────────────────────────── function SourceBadge({ source }) { const colors = { interview: ["#DBEAFE", "#1D4ED8"], observation: ["#FEF3C7", "#92400E"], records: ["#F3E8FF", "#6B21A8"] }; const [bg, text] = colors[source] || ["#F3F4F6", "#374151"]; return {source}; } function ConfidenceBadge({ confidence }) { const colors = { high: ["#DCFCE7", "#15803D"], medium: ["#FEF3C7", "#92400E"], low: ["#FEE2E2", "#991B1B"] }; const [bg, text] = colors[confidence] || ["#F3F4F6", "#374151"]; return {confidence}; } function ManualEditTab({ node, onChange }) { const schema = NODE_SCHEMAS[node.data.nodeType] || []; const [localLabel, setLocalLabel] = useState(node.data.label); useEffect(() => { setLocalLabel(node.data.label); }, [node.id]); function handleFieldChange(fieldName, key, val) { const newFields = { ...node.data.fields, [fieldName]: { ...node.data.fields[fieldName], [key]: val } }; onChange({ fields: newFields }); } function handleLabelBlur() { if (localLabel !== node.data.label) onChange({ label: localLabel }); } return (
setLocalLabel(e.target.value)} onBlur={handleLabelBlur} style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 8, padding: "8px 10px", fontSize: 14, boxSizing: "border-box" }} />
{schema.map(field => { const fieldData = node.data.fields[field.name] || { value: "", source: field.source, confidence: field.confidence, verified: false }; const isLowConf = field.confidence === "low"; return (
{field.name}

{field.question}

{field.dataType === "text_list" ? (