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 (