From 3a47d91267a98d5dc51f95bdb5d8d9070ba19762 Mon Sep 17 00:00:00 2001 From: Geoffrey Litt Date: Wed, 23 Jul 2025 09:19:38 -0400 Subject: [PATCH] Enable AI to add cells anywhere in document --- packages/patchwork/patchwork.json | 2 +- packages/patchwork/src/ai-prompt.ts | 336 ++++++++++++++++++++++++---- 2 files changed, 289 insertions(+), 49 deletions(-) diff --git a/packages/patchwork/patchwork.json b/packages/patchwork/patchwork.json index a6bce21e4..629fa79f2 100644 --- a/packages/patchwork/patchwork.json +++ b/packages/patchwork/patchwork.json @@ -1 +1 @@ -{"projectFolderUrl":"automerge:4K1jm889e154FUcEsvMbtBvDLW6"} \ No newline at end of file +{"projectFolderUrl":"automerge:3QeuVkAiboujyGJzJeJouT3rG3Xj"} \ No newline at end of file diff --git a/packages/patchwork/src/ai-prompt.ts b/packages/patchwork/src/ai-prompt.ts index 49721e7c9..1fc71383f 100644 --- a/packages/patchwork/src/ai-prompt.ts +++ b/packages/patchwork/src/ai-prompt.ts @@ -2,6 +2,8 @@ import { DocHandle } from "@automerge/automerge-repo"; import { AIEditPrompt } from "@patchwork/sdk"; import { v7 } from "uuid"; +console.log("this is the full updated ai prompt"); + // Type definitions based on catlog-wasm structure interface ModelDocumentContent { name: string; @@ -40,10 +42,33 @@ type Ob = | { tag: "Basic"; content: string } | { tag: "Tabulated"; content: string }; +// Cell definition for add-cells operation +type CellDefinition = + | { cellType: "rich-text"; content: string } + | { cellType: "object"; name: string; obType: ObType } + | { + cellType: "morphism"; + name: string; + dom: string; + cod: string; + morType: MorType; + }; + // Edit operation types type EditOperation = - | { type: "add-cell"; cellType: "rich-text"; content: string } - | { type: "add-cell"; cellType: "object"; name: string; obType: ObType } + | { + type: "add-cell"; + cellType: "rich-text"; + content: string; + position: { after?: string; before?: string }; + } + | { + type: "add-cell"; + cellType: "object"; + name: string; + obType: ObType; + position: { after?: string; before?: string }; + } | { type: "add-cell"; cellType: "morphism"; @@ -51,6 +76,12 @@ type EditOperation = dom: string; cod: string; morType: MorType; + position: { after?: string; before?: string }; + } + | { + type: "add-cells"; + cells: CellDefinition[]; + position: { after?: string; before?: string }; } | { type: "edit-cell"; id: string; updates: any } | { type: "delete-cell"; id: string }; @@ -111,7 +142,7 @@ Common examples: epidemiological models (S-E-I-R-V), supply chains, economic mod Documents follow this JSON structure: -\`\`\`json +json: { "name": "Model Name", "theory": "primitive-stock-flow", @@ -150,7 +181,7 @@ Documents follow this JSON structure: ] } } -\`\`\` + **Key concepts**: - **dom** (domain) = source stock of a flow @@ -166,7 +197,7 @@ This model has 5 stocks (populations) and flows between them: **Flows**: exposure, vaccination, infection, recovery **Link**: Infectious population influences exposure rate -\`\`\`json +json: // Stock: Susceptible population { "tag": "object", @@ -191,7 +222,7 @@ This model has 5 stocks (populations) and flows between them: "cod": {"tag": "Tabulated", "content": "exposure-flow-id"}, "morType": {"tag": "Basic", "content": "Link"} } -\`\`\` + # Making Edits @@ -204,13 +235,15 @@ I'll add a Dead population and mortality flow from Infectious to Dead. { "type": "add-cell", "cellType": "rich-text", - "content": "New explanation text" + "content": "New explanation text", + "position": {"index": 10} }, { "type": "add-cell", "cellType": "object", "name": "Dead", - "obType": {"tag": "Basic", "content": "Object"} + "obType": {"tag": "Basic", "content": "Object"}, + "position": {"index": 11} }, { "type": "add-cell", @@ -218,7 +251,8 @@ I'll add a Dead population and mortality flow from Infectious to Dead. "name": "mortality", "dom": "infectious-population-name", "cod": "dead-population-name", - "morType": {"tag": "Hom", "content": {"tag": "Basic", "content": "Object"}} + "morType": {"tag": "Hom", "content": {"tag": "Basic", "content": "Object"}}, + "position": {"after": "some-cell-id"} }, { "type": "edit-cell", @@ -243,10 +277,49 @@ I'll add a Dead population and mortality flow from Infectious to Dead. - Always maintain logical flow: stocks should connect via meaningful processes - Use domain knowledge for realistic models (epidemiology, economics, etc.) +**Positioning Cells (REQUIRED)**: +- The "position" field is REQUIRED for all "add-cell" and "add-cells" operations +- You must specify one of these position options: + - "position": {"after": "cell-id-or-name"} - Insert after the cell with this ID or name + - "position": {"before": "cell-id-or-name"} - Insert before the cell with this ID or name + - "position": {"after": "_start"} - Insert at the beginning of the notebook +- For existing cells, use their ID (found in the document) +- For cells you created earlier in the same edit, use their name +- Example: {"type": "add-cell", "cellType": "rich-text", "content": "Text", "position": {"after": "abc-123"}} + +**Adding Multiple Consecutive Cells**: +- Use the "add-cells" operation to insert multiple cells at once at the same position +- This is much cleaner than using multiple "add-cell" operations +- The cells will be inserted consecutively in the order they appear in the "cells" array +- Example: +json: +{ + "type": "add-cells", + "cells": [ + {"cellType": "rich-text", "content": "First cell"}, + {"cellType": "object", "name": "MyStock", "obType": {"tag": "Basic", "content": "Object"}}, + {"cellType": "rich-text", "content": "Third cell"} + ], + "position": {"after": "some-cell-id"} +} + +- This inserts all three cells consecutively after the specified cell +- You can use the same position options: "after" or "before" + +**Inserting After New Cells**: +- When inserting after a cell you created earlier in the same edit, use the cell's name +- For objects, use the object name (e.g., "MyStock") +- For morphisms, use the morphism name (e.g., "birth_rate") +- For rich-text cells, you cannot reference them by name (they don't have names) + **Edit Examples**: -- Update rich-text content: \`{"type": "edit-cell", "id": "...", "updates": {"content": "New text"}}\` -- Update object name: \`{"type": "edit-cell", "id": "...", "updates": {"content": {"name": "New Name"}}}\` -- Update morphism domain: \`{"type": "edit-cell", "id": "...", "updates": {"content": {"dom": {"tag": "Basic", "content": "new-id"}}}}\` +- Update rich-text content: {"type": "edit-cell", "id": "...", "updates": {"content": "New text"}} +- Update object name: {"type": "edit-cell", "id": "...", "updates": {"content": {"name": "New Name"}}} +- Update morphism domain: {"type": "edit-cell", "id": "...", "updates": {"content": {"dom": {"tag": "Basic", "content": "new-id"}}}} +- Insert cell at beginning: {"type": "add-cell", "cellType": "rich-text", "content": "Text", "position": {"after": "_start"}} +- Insert after specific existing cell: {"type": "add-cell", "cellType": "object", "name": "Stock", "obType": {...}, "position": {"after": "cell-123"}} +- Insert after a new object you created: {"type": "add-cell", "cellType": "rich-text", "content": "Explanation", "position": {"after": "MyNewStock"}} +- Add multiple cells after a cell: {"type": "add-cells", "cells": [...], "position": {"after": "cell-456"}} You MUST provide a brief explanation followed by tags with valid JSON!`, @@ -266,67 +339,234 @@ You MUST provide a brief explanation followed by tags with valid JSON!`, Array.from(objectNameToId.entries()) ); + // Separate operations by type and prepare add operations + const addOps: Array<{ + op: EditOperation & { type: "add-cell" | "add-cells" }; + cell: NotebookCell; + position?: { after?: string; before?: string }; + }> = []; + const otherOps: EditOperation[] = []; + + // Track names of newly created cells for referencing + const newCellNames = new Map(); // name -> cell ID + + // First pass: prepare all cells and categorize operations for (const op of operations) { - console.log(`🔄 Processing operation:`, op); - switch (op.type) { - case "add-cell": + if (op.type === "add-cell") { + const newCellId = generateUUID(); + let newCell: NotebookCell | null = null; + + if (op.cellType === "rich-text") { + console.log( + `📝 Preparing rich-text cell: "${op.content.substring( + 0, + 50 + )}..."` + ); + newCell = { + tag: "rich-text", + id: newCellId, + content: op.content, + }; + } else if (op.cellType === "object") { + const newObjectId = generateUUID(); + console.log( + `📦 Preparing object: "${op.name}" with ID: ${newObjectId}` + ); + newCell = { + tag: "formal", + id: newCellId, + content: { + tag: "object", + id: newObjectId, + name: op.name, + obType: op.obType, + }, + }; + // Update the name-to-id mapping immediately + objectNameToId.set(op.name, newObjectId); + // Track the cell name for later referencing + newCellNames.set(op.name, newCellId); + } else if (op.cellType === "morphism") { + const newMorphismId = generateUUID(); + // Resolve dom and cod references + const domId = objectNameToId.get(op.dom) || op.dom; + const codId = objectNameToId.get(op.cod) || op.cod; + + console.log( + `🔗 Preparing morphism: "${op.name}" from "${op.dom}" (${domId}) to "${op.cod}" (${codId})` + ); + + newCell = { + tag: "formal", + id: newCellId, + content: { + tag: "morphism", + id: newMorphismId, + name: op.name, + dom: { tag: "Basic", content: domId }, + cod: { tag: "Basic", content: codId }, + morType: op.morType, + }, + }; + // Track the cell name for later referencing + newCellNames.set(op.name, newCellId); + } + + if (newCell) { + addOps.push({ op, cell: newCell, position: op.position }); + } + } else if (op.type === "add-cells") { + // Handle multiple cells being added at once + console.log(`📚 Preparing to add ${op.cells.length} cells`); + + for (const cellDef of op.cells) { const newCellId = generateUUID(); + let newCell: NotebookCell | null = null; - if (op.cellType === "rich-text") { - console.log( - `📝 Adding rich-text cell: "${op.content.substring( - 0, - 50 - )}..."` - ); - doc.notebook.cells.push({ + if (cellDef.cellType === "rich-text") { + newCell = { tag: "rich-text", id: newCellId, - content: op.content, - }); - } else if (op.cellType === "object") { + content: cellDef.content, + }; + } else if (cellDef.cellType === "object") { const newObjectId = generateUUID(); - console.log( - `📦 Adding object: "${op.name}" with ID: ${newObjectId}` - ); - doc.notebook.cells.push({ + newCell = { tag: "formal", id: newCellId, content: { tag: "object", id: newObjectId, - name: op.name, - obType: op.obType, + name: cellDef.name, + obType: cellDef.obType, }, - }); - // Update the name-to-id mapping - objectNameToId.set(op.name, newObjectId); - } else if (op.cellType === "morphism") { + }; + // Update the name-to-id mapping immediately + objectNameToId.set(cellDef.name, newObjectId); + // Track the cell name for later referencing + newCellNames.set(cellDef.name, newCellId); + } else if (cellDef.cellType === "morphism") { const newMorphismId = generateUUID(); - // Resolve dom and cod references - const domId = objectNameToId.get(op.dom) || op.dom; - const codId = objectNameToId.get(op.cod) || op.cod; - - console.log( - `🔗 Adding morphism: "${op.name}" from "${op.dom}" (${domId}) to "${op.cod}" (${codId})` - ); + const domId = objectNameToId.get(cellDef.dom) || cellDef.dom; + const codId = objectNameToId.get(cellDef.cod) || cellDef.cod; - doc.notebook.cells.push({ + newCell = { tag: "formal", id: newCellId, content: { tag: "morphism", id: newMorphismId, - name: op.name, + name: cellDef.name, dom: { tag: "Basic", content: domId }, cod: { tag: "Basic", content: codId }, - morType: op.morType, + morType: cellDef.morType, }, - }); + }; + // Track the cell name for later referencing + newCellNames.set(cellDef.name, newCellId); } - break; + if (newCell) { + addOps.push({ op, cell: newCell, position: op.position }); + } + } + } else { + otherOps.push(op); + } + } + + // Helper function to find cell index by ID or name + const findCellIndex = (idOrName: string): number => { + // Check for special "_start" case + if (idOrName === "_start") { + return -1; // Special marker for beginning + } + + // First try to find by cell ID + let index = doc.notebook.cells.findIndex(c => c.id === idOrName); + if (index >= 0) return index; + + // Then try to find by newly created cell name + const cellId = newCellNames.get(idOrName); + if (cellId) { + index = doc.notebook.cells.findIndex(c => c.id === cellId); + if (index >= 0) return index; + } + + // Finally try to find by object/morphism name in existing cells + index = doc.notebook.cells.findIndex(c => { + if (c.tag === "formal" && c.content.tag === "object") { + return c.content.name === idOrName; + } + if (c.tag === "formal" && c.content.tag === "morphism") { + return c.content.name === idOrName; + } + return false; + }); + + return index; + }; + + // Group cells by their parent operation + const cellGroups = new Map(); + for (const { op, cell } of addOps) { + if (!cellGroups.has(op)) { + cellGroups.set(op, []); + } + cellGroups.get(op)!.push(cell); + } + + // Process operations (no need for complex sorting since we don't use indexes) + for (const [op, cells] of cellGroups.entries()) { + if (op.position.after) { + // Insert after specific cell ID/name + const afterIndex = findCellIndex(op.position.after); + if (op.position.after === "_start") { + // Special case: insert at beginning + doc.notebook.cells.splice(0, 0, ...cells); + console.log( + `📍 Inserted ${cells.length} cell(s) at the beginning` + ); + } else if (afterIndex >= 0) { + doc.notebook.cells.splice(afterIndex + 1, 0, ...cells); + console.log( + `📍 Inserted ${cells.length} cell(s) after: ${op.position.after}` + ); + } else { + console.log( + `⚠️ Cell ${op.position.after} not found, adding at end` + ); + doc.notebook.cells.push(...cells); + } + } else if (op.position.before) { + // Insert before specific cell ID/name + const beforeIndex = findCellIndex(op.position.before); + if (beforeIndex >= 0) { + doc.notebook.cells.splice(beforeIndex, 0, ...cells); + console.log( + `📍 Inserted ${cells.length} cell(s) before: ${op.position.before}` + ); + } else { + console.log( + `⚠️ Cell ${op.position.before} not found, adding at end` + ); + doc.notebook.cells.push(...cells); + } + } else { + // Position is required, this should not happen + console.error(`❌ Position is required for add operations`); + throw new Error( + "Position is required for add-cell and add-cells operations" + ); + } + } + + // Process other operations (edit-cell, delete-cell) + for (const op of otherOps) { + console.log(`🔄 Processing ${op.type} operation`); + switch (op.type) { case "edit-cell": const cellIndex = doc.notebook.cells.findIndex( (c) => c.id === op.id