From 6ae46aa6f7eb1078253142795c59ecace0fd9f1c Mon Sep 17 00:00:00 2001
From: Ben Follington <5009316+bfollington@users.noreply.github.com>
Date: Wed, 30 Apr 2025 05:52:06 +1000
Subject: [PATCH 1/7] Handle direct string response from LLM as well as
`.content` unwrap
---
jumble/public/module/charm/sandbox/bootstrap.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/jumble/public/module/charm/sandbox/bootstrap.js b/jumble/public/module/charm/sandbox/bootstrap.js
index 6f6cfa132..1e3c8ecf6 100644
--- a/jumble/public/module/charm/sandbox/bootstrap.js
+++ b/jumble/public/module/charm/sandbox/bootstrap.js
@@ -106,7 +106,7 @@ window.generateText = function ({ system, messages }) {
return window.llm({
system,
messages,
- }).then(result => result.content)
+ }).then(result => typeof result === 'string' ? result : result?.content)
}
window.generateObject = function ({ system, messages }) {
From 502a356579f2c5141a1f5dc6a5454d15b5e0c22d Mon Sep 17 00:00:00 2001
From: Ben Follington <5009316+bfollington@users.noreply.github.com>
Date: Wed, 30 Apr 2025 07:39:06 +1000
Subject: [PATCH 2/7] Support keyPath for useReactiveCell
---
charm/src/iframe/static.ts | 34 +++---
.../public/module/charm/sandbox/bootstrap.js | 110 +++++++++++-------
2 files changed, 80 insertions(+), 64 deletions(-)
diff --git a/charm/src/iframe/static.ts b/charm/src/iframe/static.ts
index fe7e2173b..a32f14a29 100644
--- a/charm/src/iframe/static.ts
+++ b/charm/src/iframe/static.ts
@@ -114,21 +114,20 @@ Create an interactive React component that fulfills the user's request. Focus on
-- **useReactiveCell(key, defaultValue)** - Persistent data storage with reactive updates
+- **useReactiveCell(keyPath: string[])** - Persistent data storage with reactive updates
- **generateText({ system, messages })** - Generate text via Large Language Model
- **generateObject({ system, messages })** - Generate JSON object via Large Language Model
- **readWebpage(url)** - Fetch and parse external web content
- **generateImage(prompt)** - Create AI-generated images
-
- ## Important Note About useReactiveCell
+
+ ## Important Note About useReactiveCell(keyPath: string[])
- **useReactiveCell is a React Hook** and must follow all React hook rules
- It should only be used for persistent state and must draw from the provided schema
- For any ephemeral state, use \`React.useState\`
- Only call useReactiveCell at the top level of your function components or custom hooks
- Do not call useReactiveCell inside loops, conditions, or nested functions
- - useReactiveCell cannot be used outside of \`onReady\` components - it must be called during rendering
-
+
@@ -151,7 +150,7 @@ ${security()}
## 1. \`useReactiveCell\` Hook
-The \`useReactiveCell\` hook binds to a reactive cell given key and returns a tuple \`[doc, setDoc]\`:
+The \`useReactiveCell\` hook binds to a reactive cell given a key path and returns a tuple \`[doc, setDoc]\`:
Any keys from the view-model-schema are valid for useReactiveCell, any other keys will fail. Provide a default as the second argument, **do not set an initial value explicitly**.
@@ -175,12 +174,8 @@ For this schema:
\`\`\`jsx
function CounterComponent() {
// Correct: useReactiveCell called at top level of component
- const [counter, setCounter] = useReactiveCell("counter", -1); // default
-
- // Incorrect: would cause errors
- // if(something) {
- // const [data, setData] = useReactiveCell("data", {}); // Never do this!
- // }
+ const [counter, setCounter] = useReactiveCell("counter");
+ const [maxValue, setMaxValue] = useReactiveCell(['settings', 'maxValue']);
const onIncrement = useCallback(() => {
// writing to the cell automatically triggers a re-render
@@ -428,11 +423,9 @@ function onReady(mount, sourceData, libs) {
const { useSpring, animated } = libs['@react-spring/web']; // Access imported module
function MyApp() {
- const [count, setCount] = useReactiveCell('count', 0);
- const [todos, setTodos] = useReactiveCell('todos', [
- { id: 1, text: 'Learn React', completed: false },
- { id: 2, text: 'Build a Todo App', completed: false }
- ]);
+ const [count, setCount] = useReactiveCell('count');
+ const [todos, setTodos] = useReactiveCell('todos');
+
const props = useSpring({
from: { opacity: 0 },
to: { opacity: 1 }
@@ -486,19 +479,18 @@ Create an interactive React component that fulfills the user's request. Focus on
4. For form handling, use \`onClick\` handlers instead of \`onSubmit\`
## Available APIs
-- **useReactiveCell(key, defaultValue)** - Persistent data storage with reactive updates
+- **useReactiveCell(keyPath: string[])** - Persistent data storage with reactive updates
- **generateText({ system, messages })** - Generate text via Large Language Model
- **generateObject({ system, messages })** - Generate JSON object via Large Language Model
- **readWebpage(url)** - Fetch and parse external web content
- **generateImage(prompt)** - Create AI-generated images
-## Important Note About useReactiveCell
+## Important Note About useReactiveCell(keyPath: string[])
- **useReactiveCell is a React Hook** and must follow all React hook rules
- It should only be used for persistent state and must draw from the provided schema
- For any ephemeral state, use \`React.useState\`
- Only call useReactiveCell at the top level of your function components or custom hooks
- Do not call useReactiveCell inside loops, conditions, or nested functions
-- useReactiveCell cannot be used outside of \`onReady\` components - it must be called during rendering
## Library Usage
- Request additional libraries in \`onLoad\` by returning an array of module names
@@ -513,7 +505,7 @@ ${security()}
## 1. \`useReactiveCell\` Hook
-The \`useReactiveCell\` hook binds to a reactive cell given key and returns a tuple \`[doc, setDoc]\`:
+The \`useReactiveCell\` hook binds to a reactive cell given a key path and returns a tuple \`[doc, setDoc]\`:
Any keys from the schema are valid for useReactiveCell, any other keys will fail. Provide a default as the second argument, **do not set an initial value explicitly**.
diff --git a/jumble/public/module/charm/sandbox/bootstrap.js b/jumble/public/module/charm/sandbox/bootstrap.js
index 1e3c8ecf6..e0670a62d 100644
--- a/jumble/public/module/charm/sandbox/bootstrap.js
+++ b/jumble/public/module/charm/sandbox/bootstrap.js
@@ -8,60 +8,84 @@ window.React = React
window.ReactDOM = ReactDOM
window.Babel = Babel
-window.useReactiveCell = function (key) {
- // Track if we've received a response from the parent
- const [received, setReceived] = React.useState(false)
- // Initialize state with defaultValue
- const [doc, setDocState] = React.useState(undefined)
-
+window.useReactiveCell = function useReactiveCell(pathOrKey) {
+ const pathArr = Array.isArray(pathOrKey) ? pathOrKey : [pathOrKey];
+ const rootKey = pathArr[0]; // the key used for IPC
+ const nestedPath = pathArr.slice(1); // [] when we stay at the root
+
+ /* ------------------------------------------------------------------ utils */
+ const getNested = (obj, p = []) =>
+ p.reduce((acc, k) => (acc != null ? acc[k] : undefined), obj);
+
+ const setNested = (obj, p, val) => {
+ if (p.length === 0) return val;
+ const [k, ...rest] = p;
+ const clone =
+ typeof k === "number"
+ ? Array.isArray(obj) ? obj.slice() : [] // keep arrays as arrays
+ : obj && typeof obj === "object"
+ ? { ...obj }
+ : {};
+ clone[k] = setNested(clone[k], rest, val);
+ return clone;
+ };
+ /* ------------------------------------------------------------------------ */
+
+ // Last full root value we have seen.
+ const rootRef = React.useRef(
+ window.sourceData ? window.sourceData[rootKey] : undefined,
+ );
+
+ // Local state holds ONLY the nested value the caller cares about.
+ const [received, setReceived] = React.useState(false);
+ const [doc, setDocState] = React.useState(() =>
+ getNested(rootRef.current, nestedPath)
+ );
+
+ /* -------------------------------- listen for updates from the host ------ */
React.useEffect(() => {
- // Handler for document updates
- function handleMessage(event) {
+ function handleMessage(e) {
if (
- event.data &&
- event.data.type === "update" &&
- event.data.data[0] === key
+ e.data &&
+ e.data.type === "update" &&
+ e.data.data?.[0] === rootKey
) {
- // Mark that we've received a response
- setReceived(true)
-
- // Update the state with the received value or null if undefined
- const value = event.data.data[1]
- console.log("useReactiveCell", key, "updated", value)
- setDocState(value)
+ const newRoot = e.data.data[1];
+ rootRef.current = newRoot;
+ setDocState(getNested(newRoot, nestedPath));
+ setReceived(true);
}
}
- window.addEventListener("message", handleMessage)
-
- // Subscribe to the specific key
- window.parent.postMessage({ type: "subscribe", data: [key] }, "*")
- window.parent.postMessage({ type: "read", data: key }, "*")
+ window.addEventListener("message", handleMessage);
+ window.parent.postMessage({ type: "subscribe", data: [rootKey] }, "*");
+ window.parent.postMessage({ type: "read", data: rootKey }, "*");
return () => {
- window.removeEventListener("message", handleMessage)
- window.parent.postMessage({ type: "unsubscribe", data: [key] }, "*")
- }
- }, [key])
+ window.removeEventListener("message", handleMessage);
+ window.parent.postMessage({ type: "unsubscribe", data: [rootKey] }, "*");
+ };
+ }, [rootKey, nestedPath.join(".")]); // Dependency on stringified path
+ /* ------------------------------------------------------------------------ */
- // Update function
+ /* ----------------------------- setter ----------------------------------- */
const updateDoc = newValue => {
- if (typeof newValue === "function") {
- newValue = newValue(doc)
- }
- console.log("useReactiveCell", key, "written", newValue)
- setDocState(newValue)
- window.parent.postMessage({ type: "write", data: [key, newValue] }, "*")
- }
+ if (typeof newValue === "function") newValue = newValue(doc);
- // If we have not yet received response from the host we use field from the
- // sourceData which was preloaded via `subscribeToSource` during initialization
- // in the `initializeApp`.
- // ⚠️ Please note that value we prefetched still could be out of date because
- // `*` subscription is removed in the iframe-ctx.ts file which could lead
- // charm to make wrong conclusion and overwrite key.
- return [received ? doc : window.sourceData[key], updateDoc]
-}
+ // Build the new *root* object immutably
+ const newRoot = setNested(rootRef.current ?? {}, nestedPath, newValue);
+ rootRef.current = newRoot;
+ setDocState(newValue);
+
+ window.parent.postMessage({ type: "write", data: [rootKey, newRoot] }, "*");
+ };
+ /* ------------------------------------------------------------------------ */
+
+ // If we never received an explicit update yet, fall back to pre-loaded data
+ const fallback = getNested(window.sourceData?.[rootKey], nestedPath);
+
+ return [received ? doc : fallback, updateDoc];
+};
window.useDoc = window.useReactiveCell;
From 61cff6038fc43624e91ee4f5109033bfc47213dd Mon Sep 17 00:00:00 2001
From: Ben Follington <5009316+bfollington@users.noreply.github.com>
Date: Wed, 30 Apr 2025 07:39:15 +1000
Subject: [PATCH 3/7] Fix empty string boolean test
---
charm/src/iterate.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/charm/src/iterate.ts b/charm/src/iterate.ts
index f1a9ad091..613e72a32 100644
--- a/charm/src/iterate.ts
+++ b/charm/src/iterate.ts
@@ -105,7 +105,7 @@ export async function iterate(
const { iframe } = getIframeRecipe(charm);
const prevSpec = iframe?.spec;
- if (!plan?.description) {
+ if (plan?.description === undefined) {
throw new Error("No specification provided");
}
const newSpec = plan.description;
From 76b3bcf5b26d0d76e6a24151fc59b7d511d0a483 Mon Sep 17 00:00:00 2001
From: Ben Follington <5009316+bfollington@users.noreply.github.com>
Date: Wed, 30 Apr 2025 10:55:37 +1000
Subject: [PATCH 4/7] generateImage -> generateImageUrl
---
charm/src/iframe/static.ts | 10 +++++++---
jumble/public/module/charm/sandbox/bootstrap.js | 3 ++-
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/charm/src/iframe/static.ts b/charm/src/iframe/static.ts
index a32f14a29..236fd542f 100644
--- a/charm/src/iframe/static.ts
+++ b/charm/src/iframe/static.ts
@@ -299,14 +299,18 @@ async function fetchFromUrl() {
}
\`\`\`
-## 5. generateImage Function
+
+
+Synchronous, generates a URL that will load the image.
\`\`\`jsx
function ImageComponent() {
- return
;
+ return
;
}
-
\`\`\`
+
+
+
## 6. Using the Interface Functions
\`\`\`javascript
diff --git a/jumble/public/module/charm/sandbox/bootstrap.js b/jumble/public/module/charm/sandbox/bootstrap.js
index e0670a62d..f7247bfca 100644
--- a/jumble/public/module/charm/sandbox/bootstrap.js
+++ b/jumble/public/module/charm/sandbox/bootstrap.js
@@ -239,11 +239,12 @@ window.readWebpage = (function () {
return readWebpage
})()
-// Define generateImage utility with React available
window.generateImage = function (prompt) {
return "/api/ai/img?prompt=" + encodeURIComponent(prompt)
}
+window.generateImageUrl = window.generateImage;
+
// Error handling
window.onerror = function (message, source, lineno, colno, error) {
window.parent.postMessage(
From 5999695625c2b5747ade0846cd6780c9302ffce7 Mon Sep 17 00:00:00 2001
From: Ben Follington <5009316+bfollington@users.noreply.github.com>
Date: Wed, 30 Apr 2025 11:49:20 +1000
Subject: [PATCH 5/7] Try 4.1 nano for JSON
---
jumble/public/module/charm/sandbox/bootstrap.js | 7 +++++--
llm/src/types.ts | 2 +-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/jumble/public/module/charm/sandbox/bootstrap.js b/jumble/public/module/charm/sandbox/bootstrap.js
index f7247bfca..2cadbe42e 100644
--- a/jumble/public/module/charm/sandbox/bootstrap.js
+++ b/jumble/public/module/charm/sandbox/bootstrap.js
@@ -7,6 +7,9 @@ import * as Babel from "https://esm.sh/@babel/standalone"
window.React = React
window.ReactDOM = ReactDOM
window.Babel = Babel
+window.useState = window.React.useState
+window.useEffect = window.React.useEffect
+window.useCallback = window.React.useCallback
window.useReactiveCell = function useReactiveCell(pathOrKey) {
const pathArr = Array.isArray(pathOrKey) ? pathOrKey : [pathOrKey];
@@ -87,6 +90,7 @@ window.useReactiveCell = function useReactiveCell(pathOrKey) {
return [received ? doc : fallback, updateDoc];
};
+
window.useDoc = window.useReactiveCell;
// Define llm utility with React available
@@ -130,7 +134,7 @@ window.generateText = function ({ system, messages }) {
return window.llm({
system,
messages,
- }).then(result => typeof result === 'string' ? result : result?.content)
+ })
}
window.generateObject = function ({ system, messages }) {
@@ -139,7 +143,6 @@ window.generateObject = function ({ system, messages }) {
messages,
mode: 'json'
})
- .then(result => result.content)
.then(result => {
try {
// Handle possible control characters and escape sequences
diff --git a/llm/src/types.ts b/llm/src/types.ts
index 13bbe7d77..5ee47d02c 100644
--- a/llm/src/types.ts
+++ b/llm/src/types.ts
@@ -5,7 +5,7 @@ export const DEFAULT_MODEL_NAME: ModelName =
// NOTE(ja): This should be an array of models, the first model will be tried, if it
// fails, the second model will be tried, etc.
-export const DEFAULT_IFRAME_MODELS: ModelName = "google:gemini-2.0-flash";
+export const DEFAULT_IFRAME_MODELS: ModelName = "openai:gpt-4.1-nano";
export type LLMResponse = {
content: string;
From de43fa126e620eeaac13dfb711564ac7d7a51c55 Mon Sep 17 00:00:00 2001
From: Ben Follington <5009316+bfollington@users.noreply.github.com>
Date: Wed, 30 Apr 2025 12:29:21 +1000
Subject: [PATCH 6/7] Clarify prompt structure
---
charm/src/iframe/static.ts | 54 +++++++++++++++++++++++++-------------
1 file changed, 36 insertions(+), 18 deletions(-)
diff --git a/charm/src/iframe/static.ts b/charm/src/iframe/static.ts
index 236fd542f..9b5817dcf 100644
--- a/charm/src/iframe/static.ts
+++ b/charm/src/iframe/static.ts
@@ -148,6 +148,7 @@ ${security()}
# SDK Usage Guide
+
## 1. \`useReactiveCell\` Hook
The \`useReactiveCell\` hook binds to a reactive cell given a key path and returns a tuple \`[doc, setDoc]\`:
@@ -189,8 +190,13 @@ function CounterComponent() {
);
}
\`\`\`
+
-## 2. \`generateText\` Function
+
+Several APIs exist for generating text, JSON or image urls.
+
+
+\`generateText({ system, messages}): Promise\`
\`\`\`jsx
async function fetchLLMResponse() {
@@ -201,17 +207,21 @@ async function fetchLLMResponse() {
console.log('LLM responded:', result);
}
\`\`\`
+
-## 3. \`generateObject\` (JSON) Function
+
-Important: ensure you explain the intended schema of the response in the prompt.
+\`window.generateObject({ system, messages }): Promise
-\`\`\`jsx
-async function fetchFromUrl() {
- const url = 'https://twopm.studio';
- const result = await readWebpage(url);
- console.log('Markdown:', result.content);
-}
-\`\`\`
-
+
Synchronous, generates a URL that will load the image.
@@ -309,9 +313,23 @@ function ImageComponent() {
}
\`\`\`
-
+
+
+
+
+\`readWebpage(url: string): Promise\` Returns markdown format.
-## 6. Using the Interface Functions
+\`\`\`jsx
+async function fetchFromUrl() {
+ const url = 'https://twopm.studio';
+ const result = await readWebpage(url);
+ console.log('Markdown:', result.content);
+}
+\`\`\`
+
+
+
+All generated code must follow this pattern:
\`\`\`javascript
// Import from modern ESM libraries:
From 501a3f455a7f1d80f9a2919d5e312a243ba896e9 Mon Sep 17 00:00:00 2001
From: Ben Follington <5009316+bfollington@users.noreply.github.com>
Date: Wed, 30 Apr 2025 12:48:16 +1000
Subject: [PATCH 7/7] Update LLM cache
---
...3c86778ba0ab463300f5c4ccea4fd2e25799320d.json | 16 ----------------
...d385c7fbbcdf51eb92b77d1542532771f52b110c.json | 16 ----------------
...1cc83cf9ec59127763bea39a7428ec03e7ca2313.json | 2 +-
...81f7c92d486bb6069e2ae506b70d9679833c3ef4.json | 16 ----------------
...a7fd531b842043b4a4e21b3296f0a93f1b69e153.json | 16 ++++++++++++++++
5 files changed, 17 insertions(+), 49 deletions(-)
delete mode 100644 jumble/integration/cache/llm-api-cache/4d4e13fee4f15d7637d8fbfc3c86778ba0ab463300f5c4ccea4fd2e25799320d.json
delete mode 100644 jumble/integration/cache/llm-api-cache/626bfe00188f06706ffa67bdd385c7fbbcdf51eb92b77d1542532771f52b110c.json
delete mode 100644 jumble/integration/cache/llm-api-cache/e115e9055568f076618b75ba81f7c92d486bb6069e2ae506b70d9679833c3ef4.json
create mode 100644 jumble/integration/cache/llm-api-cache/fa94cc4941bcd6408c15eee3a7fd531b842043b4a4e21b3296f0a93f1b69e153.json
diff --git a/jumble/integration/cache/llm-api-cache/4d4e13fee4f15d7637d8fbfc3c86778ba0ab463300f5c4ccea4fd2e25799320d.json b/jumble/integration/cache/llm-api-cache/4d4e13fee4f15d7637d8fbfc3c86778ba0ab463300f5c4ccea4fd2e25799320d.json
deleted file mode 100644
index 4cee7faae..000000000
--- a/jumble/integration/cache/llm-api-cache/4d4e13fee4f15d7637d8fbfc3c86778ba0ab463300f5c4ccea4fd2e25799320d.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "model": "anthropic:claude-3-7-sonnet-latest",
- "system": "# React Component Builder\n\nCreate an interactive React component that fulfills the user's request. Focus on delivering a clean, useful implementation with appropriate features.\n\n## You Are Part of a Two-Phase Process\n\n1. First phase (already completed):\n - Analyzed the user's request\n - Created a detailed specification\n - Generated a structured data schema\n\n2. Your job (second phase):\n - Create a reactive UI component based on the provided specification and schema\n - Implement the UI exactly according to the specification\n - Strictly adhere to the data schema provided\n\n## Required Elements\n- Define a title with `const title = 'Your App Name';`\n- Implement both `onLoad` and `onReady` functions\n- Use Tailwind CSS for styling with tasteful defaults\n- Do not write