diff --git a/charm/src/charm.ts b/charm/src/charm.ts index d033ca572..e51536bde 100644 --- a/charm/src/charm.ts +++ b/charm/src/charm.ts @@ -161,6 +161,9 @@ export class CharmManager { return this.pinnedCharms; } + // FIXME(ja): this says it returns a list of charm, but it isn't! you will + // have to call .get() to get the actual charm (this is missing the schema) + // how can we fix the type here? getCharms(): Cell[]> { // Start syncing if not already syncing. Will trigger a change to the list // once loaded. @@ -168,7 +171,7 @@ export class CharmManager { return this.charms; } - async add(newCharms: Cell[]) { + private async add(newCharms: Cell[]) { await storage.syncCell(this.charmsDoc); await idle(); @@ -181,6 +184,8 @@ export class CharmManager { await idle(); } + // FIXME(ja): if we are already running the charm, can we just return it? + // if a charm has sideeffects we might multiple versions... async get( id: string | Cell, runIt: boolean = true, @@ -291,46 +296,6 @@ export class CharmManager { ): Promise> { await idle(); - // Fill in missing parameters from other charms. It's a simple match on - // hashtags: For each top-level argument prop that has a hashtag in the - // description, look for a charm that has a top-level output prop with the - // same hashtag in the description, or has the hashtag in its own description. - // If there is a match, assign the first one to the input property. - - // TODO(seefeld,ben): This should be in spellcaster. - /* - if ( - !isDoc(inputs) && // Adding to a cell input is not supported yet - !isDocLink(inputs) && // Neither for cell reference - recipe.argumentSchema && - (recipe.argumentSchema as any).type === "object" - ) { - const properties = (recipe.argumentSchema as any).properties; - const inputProperties = - typeof inputs === "object" && inputs !== null ? Object.keys(inputs) : []; - for (const key in properties) { - if (!(key in inputProperties) && properties[key].description?.includes("#")) { - const hashtag = properties[key].description.match(/#(\w+)/)?.[1]; - if (hashtag) { - this.charms.get().forEach((charm) => { - const type = charm.getAsDocLink().cell?.sourceCell?.get()?.[TYPE]; - const recipe = getRecipe(type); - const charmProperties = (recipe?.resultSchema as any)?.properties as any; - const matchingProperty = Object.keys(charmProperties ?? {}).find((property) => - charmProperties[property].description?.includes(`#${hashtag}`), - ); - if (matchingProperty) { - inputs = { - ...inputs, - [key]: { $alias: { cell: charm.getAsDocLink().cell, path: [matchingProperty] } }, - }; - } - }); - } - } - } - }*/ - const syncAllMentionedCells = ( value: any, promises: any[] = [], @@ -360,7 +325,7 @@ export class CharmManager { } // FIXME(JA): this really really really needs to be revisited - async syncRecipe(charm: Cell): Promise { + async syncRecipe(charm: Cell): Promise { const recipeId = charm.getSourceCell()?.get()?.[TYPE]; if (!recipeId) throw new Error("charm missing recipe ID"); @@ -372,7 +337,7 @@ export class CharmManager { } async syncRecipeCells(recipeId: string) { - // NOTE(ja): I don't think this actually syncs the recipe + // NOTE(ja): this doesn't sync recipe to storage if (recipeId) await storage.syncCellById(this.space, { "/": recipeId }); } diff --git a/charm/src/iframe/index.ts b/charm/src/iframe/index.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/charm/src/iframe/recipe.ts b/charm/src/iframe/recipe.ts index fd6982ae6..7a50f8e65 100644 --- a/charm/src/iframe/recipe.ts +++ b/charm/src/iframe/recipe.ts @@ -41,54 +41,28 @@ export const buildFullRecipe = (iframe: IFrameRecipe) => { `; }; -function parseIframeRecipe(source: string): IFrameRecipe | undefined { +function parseIframeRecipe(source: string): IFrameRecipe { // Extract content between IFRAME-V0 comments const match = source.match( /\/\* IFRAME-V0 \*\/([\s\S]*?)\/\* IFRAME-V0 \*\//, ); - if (!match) { - console.warn("no IFRAME-V0 section in source"); - return undefined; + + if (!match || !match[1]) { + throw new Error("Could not find IFRAME-V0 recipe content in source"); } return JSON.parse(match[1]) as IFrameRecipe; } export const getIframeRecipe = (charm: Cell) => { - const recipeId = charm.getSourceCell(processSchema)?.get()?.[TYPE]; - if (!recipeId) { - console.error("FIXME, no recipeId, what should we do?"); - return {}; - } - - const recipe = getRecipe(recipeId); - if (!recipe) { - console.error("FIXME, no recipe, what should we do?"); - return {}; - } - const src = getRecipeSrc(recipeId); - if (!src) { - console.error("FIXME, no src, what should we do?"); - return {}; - } - + const { src, recipeId } = getRecipeFrom(charm); return { recipeId, iframe: parseIframeRecipe(src) }; }; export const getRecipeFrom = (charm: Cell) => { const recipeId = charm.getSourceCell(processSchema)?.get()?.[TYPE]; - if (!recipeId) { - throw new Error("No recipeId found"); - } - - const recipe = getRecipe(recipeId); - if (!recipe) { - throw new Error("No recipe found for recipeId"); - } - const src = getRecipeSrc(recipeId); - if (!src) { - throw new Error("No source found for recipeId"); - } + const recipe = getRecipe(recipeId)!; + const src = getRecipeSrc(recipeId)!; return { recipeId, recipe, src }; }; diff --git a/charm/src/index.ts b/charm/src/index.ts index 60f61a746..76543360e 100644 --- a/charm/src/index.ts +++ b/charm/src/index.ts @@ -4,8 +4,7 @@ export { castNewRecipe, compileAndRunRecipe, compileRecipe, - extend, + generateNewRecipeVersion, iterate, - saveNewRecipeVersion, } from "./iterate.ts"; export { getIframeRecipe, type IFrameRecipe } from "./iframe/recipe.ts"; diff --git a/charm/src/iterate.ts b/charm/src/iterate.ts index 9d68d3b22..c6be2808c 100644 --- a/charm/src/iterate.ts +++ b/charm/src/iterate.ts @@ -7,6 +7,7 @@ import { Charm, CharmManager } from "./charm.ts"; import { buildFullRecipe, getIframeRecipe } from "./iframe/recipe.ts"; import { buildPrompt, RESPONSE_PREFILL } from "./iframe/prompt.ts"; import { injectUserCode } from "./iframe/static.ts"; +import { isCell } from "../../runner/src/cell.ts"; const llm = new LLMClient(LLMClient.DEFAULT_URL); @@ -32,31 +33,25 @@ const genSrc = async ({ response = RESPONSE_PREFILL + response; } - const source = injectUserCode(response.split(RESPONSE_PREFILL)[1].split("\n```")[0]); + const source = injectUserCode( + response.split(RESPONSE_PREFILL)[1].split("\n```")[0], + ); return source; }; export async function iterate( charmManager: CharmManager, - charm: Cell | null, - value: string, + charm: Cell, + spec: string, shiftKey: boolean, model?: string, -): Promise { - if (!charm) { - console.error("FIXME, no charm, what should we do?"); - return; - } - +): Promise> { const { iframe } = getIframeRecipe(charm); if (!iframe) { - console.error( - "Cannot iterate on a non-iframe. Must extend instead.", - ); - return; + throw new Error("Cannot iterate on a non-iframe. Must extend instead."); } - const newSpec = shiftKey ? iframe.spec + "\n" + value : value; + const newSpec = shiftKey ? iframe.spec + "\n" + spec : spec; const newIFrameSrc = await genSrc({ src: iframe.src, @@ -66,21 +61,7 @@ export async function iterate( model: model, }); - return saveNewRecipeVersion(charmManager, charm, newIFrameSrc, newSpec); -} - -export async function extend( - charmManager: CharmManager, - charm: Cell | null, - value: string, - model?: string, -): Promise { - if (!charm) { - console.error("FIXME, no charm, what should we do?"); - return; - } - - return await castRecipeOnCell(charmManager, charm, value); + return generateNewRecipeVersion(charmManager, charm, newIFrameSrc, newSpec); } export function extractTitle(src: string, defaultTitle: string): string { @@ -89,7 +70,7 @@ export function extractTitle(src: string, defaultTitle: string): string { return htmlTitleMatch || jsTitleMatch || defaultTitle; } -export const saveNewRecipeVersion = async ( +export const generateNewRecipeVersion = ( charmManager: CharmManager, charm: Cell, newIFrameSrc: string, @@ -98,11 +79,10 @@ export const saveNewRecipeVersion = async ( const { recipeId, iframe } = getIframeRecipe(charm); if (!recipeId || !iframe) { - console.error("FIXME, no recipeId or iframe, what should we do?"); - return; + throw new Error("FIXME, no recipeId or iframe, what should we do?"); } - const name = extractTitle(newIFrameSrc, ''); + const name = extractTitle(newIFrameSrc, ""); const newRecipeSrc = buildFullRecipe({ ...iframe, src: newIFrameSrc, @@ -110,7 +90,7 @@ export const saveNewRecipeVersion = async ( name, }); - return await compileAndRunRecipe( + return compileAndRunRecipe( charmManager, newRecipeSrc, newSpec, @@ -119,38 +99,17 @@ export const saveNewRecipeVersion = async ( ); }; -export async function castRecipeOnCell( - charmManager: CharmManager, - cell: Cell, - newSpec: string, -): Promise { - const schema = { ...cell.schema, description: newSpec }; - console.log("schema", schema); - - const newIFrameSrc = await genSrc({ newSpec, schema }); - const name = extractTitle(newIFrameSrc, ''); - const newRecipeSrc = buildFullRecipe({ - src: newIFrameSrc, - spec: newSpec, - argumentSchema: schema, - resultSchema: {}, - name, - }); - - return await compileAndRunRecipe(charmManager, newRecipeSrc, newSpec, cell); -} - export async function castNewRecipe( charmManager: CharmManager, data: any, newSpec: string, -): Promise { - const schema = createJsonSchema({}, data); +): Promise> { + const schema = isCell(data) ? { ...data.schema } : createJsonSchema({}, data); schema.description = newSpec; console.log("schema", schema); const newIFrameSrc = await genSrc({ newSpec, schema }); - const name = extractTitle(newIFrameSrc, ''); + const name = extractTitle(newIFrameSrc, ""); const newRecipeSrc = buildFullRecipe({ src: newIFrameSrc, spec: newSpec, @@ -159,7 +118,7 @@ export async function castNewRecipe( name, }); - return await compileAndRunRecipe(charmManager, newRecipeSrc, newSpec, data); + return compileAndRunRecipe(charmManager, newRecipeSrc, newSpec, data); } export async function compileRecipe( @@ -169,13 +128,11 @@ export async function compileRecipe( ) { const { exports, errors } = await tsToExports(recipeSrc); if (errors) { - console.error("Compilation errors in recipe:", errors); - return; + throw new Error("Compilation errors in recipe"); } const recipe = exports.default; if (!recipe) { - console.error("No default recipe found in the compiled exports."); - return; + throw new Error("No default recipe found in the compiled exports."); } const parentsIds = parents?.map((id) => id.toString()); addRecipe(recipe, recipeSrc, spec, parentsIds); @@ -188,15 +145,11 @@ export async function compileAndRunRecipe( spec: string, runOptions: any, parents?: string[], -): Promise { +): Promise> { const recipe = await compileRecipe(recipeSrc, spec, parents); if (!recipe) { - return; + throw new Error("Failed to compile recipe"); } - const newCharm = await charmManager.runPersistent(recipe, runOptions); - await charmManager.add([newCharm]); - await charmManager.syncRecipe(newCharm); - - return newCharm.entityId; + return charmManager.runPersistent(recipe, runOptions); } diff --git a/cli/charm_demo.ts b/cli/charm_demo.ts index 09c0f7733..f7dfb926e 100644 --- a/cli/charm_demo.ts +++ b/cli/charm_demo.ts @@ -58,8 +58,8 @@ async function main() { ); // let's add the cell to the charmManager - await charmManager.add([cell]); - log(charmManager, "charmmanager after adding cell"); + // await charmManager.add([cell]); + // log(charmManager, "charmmanager after adding cell"); } main(); diff --git a/cli/main.ts b/cli/main.ts index f755d7850..da289f267 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -72,8 +72,6 @@ async function main() { const recipeSrc = await Deno.readTextFile(recipeFile); const recipe = await compileRecipe(recipeSrc, "recipe", []); const charm = await manager.runPersistent(recipe, undefined, cause); - await manager.syncRecipe(charm); - manager.add([charm]); const charmWithSchema = (await manager.get(charm))!; charmWithSchema.sink((value) => { console.log("running charm:", getEntityId(charm), value); diff --git a/jumble/integration/basic-flow.test.ts b/jumble/integration/basic-flow.test.ts index 559f3d2f3..0f3781dd1 100644 --- a/jumble/integration/basic-flow.test.ts +++ b/jumble/integration/basic-flow.test.ts @@ -1,12 +1,12 @@ -import { Browser, launch, Page } from "@astral/astral"; import { - afterAll, - afterEach, - beforeAll, - beforeEach, - describe, - it, -} from "@std/testing/bdd"; + Browser, + ConsoleEvent, + DialogEvent, + launch, + Page, + PageErrorEvent, +} from "@astral/astral"; +import { assert } from "@std/assert"; import { addCharm, inspectCharm, @@ -15,7 +15,6 @@ import { snapshot, waitForSelectorWithText, } from "./utils.ts"; -import { assert } from "@std/assert"; const TOOLSHED_API_URL = Deno.env.get("TOOLSHED_API_URL") ?? "http://localhost:8000/"; @@ -25,96 +24,171 @@ const HEADLESS = true; console.log(`TOOLSHED_API_URL=${TOOLSHED_API_URL}`); console.log(`FRONTEND_URL=${FRONTEND_URL}`); -describe("integration", () => { - let browser: Browser | void = undefined; - let page: Page | void = undefined; - let testCharm: { charmId: string; name: string } | void = undefined; - - beforeAll(async () => { - testCharm = await addCharm(TOOLSHED_API_URL); - console.log(`Charm added`, testCharm); - browser = await launch({ headless: HEADLESS }); - }); - beforeEach(async () => { - console.log(`Waiting to open website at ${FRONTEND_URL}`); - page = await browser!.newPage(FRONTEND_URL); - console.log(`Opened website at ${FRONTEND_URL}`); - await login(page); - }); - afterEach(async () => { - await page!.close(); - }); - afterAll(async () => { - await browser!.close(); - }); - - it("renders a new charm", async () => { - assert(page, "Page should be defined"); - assert(testCharm, "Test charm should be defined"); - - await snapshot(page, "Initial state"); - - const anchor = await page.waitForSelector("nav a"); - assert( - (await anchor.innerText()) === "common-knowledge", - "Logged in and Common Knowledge title renders", - ); - - await page.goto( - `${FRONTEND_URL}${testCharm.name}/${testCharm.charmId}`, - ); - await snapshot(page, "Waiting for charm to render"); - - await waitForSelectorWithText( - page, - "a[aria-roledescription='charm-link']", - "Simple Value: 1", - ); - await snapshot(page, "Charm rendered."); - assert( - true, - "Charm rendered successfully", - ); - - console.log("Clicking button"); - // Sometimes clicking this button throws: - // https://jsr.io/@astral/astral/0.5.2/src/element_handle.ts#L192 - // As if the reference was invalidated by a spurious re-render between - // getting an element handle, and clicking it. - await sleep(1000); - const button = await page.waitForSelector( - "div[aria-label='charm-content'] button", - ); - await button.click(); - await snapshot(page, "Button clicked"); - - console.log("Checking if title changed"); - await waitForSelectorWithText( - page, - "a[aria-roledescription='charm-link']", - "Simple Value: 2", - ); - - await snapshot(page, "Title changed"); - - console.log("Inspecting charm to verify updates propagated from browser."); - const charm = await inspectCharm( - TOOLSHED_API_URL, - testCharm.name, - testCharm.charmId, - ); - - console.log("Charm:", charm); - assert( - charm.includes("Simple Value: 2"), - "Charm updates propagated.", - ); - }); - - // Placeholder test ensuring browser can be used - // across multiple tests (replace when we have more integration tests!) - it("[placeholder]", () => { - assert(page, "Page should be defined"); - assert(testCharm, "Test charm should be defined"); - }); +let browser: Browser | void = undefined; +let testCharm: { charmId: string; name: string } | void = undefined; + +Deno.test({ + name: "integration tests", + fn: async (t) => { + let page: Page | void = undefined; + let failed = false; + const exceptions: string[] = []; + + try { + failed = !await t.step({ + name: "add charm via cli", + ignore: failed || exceptions.length > 0, + fn: async () => { + testCharm = await addCharm(TOOLSHED_API_URL); + console.log(`Charm added`, testCharm); + }, + }); + + failed = !await t.step({ + name: "renders homepage", + ignore: failed || exceptions.length > 0, + fn: async () => { + browser = await launch({ headless: HEADLESS }); + + console.log(`Waiting to open website at ${FRONTEND_URL}`); + page = await browser!.newPage(FRONTEND_URL); + + // Add console log listeners + page.addEventListener("console", (e: ConsoleEvent) => { + console.log(`Browser Console [${e.detail.type}]: ${e.detail.text}`); + }); + + // Add error listeners + page.addEventListener("pageerror", (e: PageErrorEvent) => { + console.error("Browser Page Error:", e.detail.message); + exceptions.push(e.detail.message); + }); + + // Add dialog listeners (for alerts, confirms, etc.) + page.addEventListener("dialog", async (e: DialogEvent) => { + const dialog = e.detail; + console.log(`Browser Dialog: ${dialog.type} - ${dialog.message}`); + await dialog.dismiss(); + }); + + console.log(`Opened website at ${FRONTEND_URL}`); + }, + }); + + failed = !await t.step({ + name: "able to login to the app", + ignore: failed || exceptions.length > 0, + fn: async () => { + assert(page, "Page should be defined"); + await login(page); + }, + }); + + failed = !await t.step({ + name: "renders charm and verifies initial state", + ignore: failed || exceptions.length > 0, + fn: async () => { + assert(page, "Page should be defined"); + assert(testCharm, "Test charm should be defined"); + + await snapshot(page, "Initial state"); + + const anchor = await page.waitForSelector("nav a"); + assert( + (await anchor.innerText()) === "common-knowledge", + "Logged in and Common Knowledge title renders", + ); + + await page.goto( + `${FRONTEND_URL}${testCharm.name}/${testCharm.charmId}`, + ); + await snapshot(page, "Waiting for charm to render"); + + await waitForSelectorWithText( + page, + "a[aria-roledescription='charm-link']", + "Simple Value: 1", + ); + await snapshot(page, "Charm rendered."); + assert(true, "Charm rendered successfully"); + }, + }); + + failed = !await t.step({ + name: "updates charm value via button click", + ignore: failed || exceptions.length > 0, + fn: async () => { + assert(page, "Page should be defined"); + assert(testCharm, "Test charm should be defined"); + + await page.goto( + `${FRONTEND_URL}${testCharm.name}/${testCharm.charmId}`, + ); + + // Wait for initial render + await waitForSelectorWithText( + page, + "a[aria-roledescription='charm-link']", + "Simple Value: 1", + ); + + await sleep(1000); + console.log("Clicking button"); + + const button = await page.waitForSelector( + "div[aria-label='charm-content'] button", + ); + await button.click(); + await snapshot(page, "Button clicked"); + + // Add more wait time after click + await sleep(2000); + + console.log("Checking if title changed"); + await waitForSelectorWithText( + page, + "a[aria-roledescription='charm-link']", + "Simple Value: 2", + ); + + await snapshot(page, "Title changed"); + + // Add additional wait time for persistence + await sleep(2000); + }, + }); + + failed = !await t.step({ + name: "verifies charm updates are persisted", + ignore: failed || exceptions.length > 0, + fn: async () => { + assert(page, "Page should be defined"); + assert(testCharm, "Test charm should be defined"); + + // Add initial wait time before checking + await sleep(1000); + + console.log( + "Inspecting charm to verify updates propagated from browser.", + ); + const charm = await inspectCharm( + TOOLSHED_API_URL, + testCharm.name, + testCharm.charmId, + ); + + console.log("Charm:", charm); + assert( + charm.includes("Simple Value: 2"), + "Charm updates propagated.", + ); + }, + }); + } finally { + exceptions.forEach((exception) => { + console.error("Failure due to browser error:", exception); + }); + await browser!.close(); + } + }, }); diff --git a/jumble/src/components/CharmRunner.tsx b/jumble/src/components/CharmRunner.tsx index de62eb240..bdaefe6a1 100644 --- a/jumble/src/components/CharmRunner.tsx +++ b/jumble/src/components/CharmRunner.tsx @@ -6,7 +6,7 @@ import { fixItCharm } from "@/utils/charm-operations.ts"; import { LuX } from "react-icons/lu"; import { DitheredCube } from "@/components/DitherCube.tsx"; import { createPath } from "@/routes.ts"; - +import { charmId } from "@/utils/charms.ts"; interface CharmLoaderProps { charmImport: () => Promise; argument?: any; @@ -51,10 +51,6 @@ function useCharmLoader({ const charm = await charmManager.runPersistent(factory, argument); if (currentMountKey !== mountingKey.current) return; - charmManager.add([charm]); - - if (currentMountKey !== mountingKey.current) return; - onCharmReadyCallback(charm); } catch (err) { if (currentMountKey === mountingKey.current) { @@ -90,16 +86,14 @@ function RawCharmRenderer({ charm, className = "" }: CharmRendererProps) { if (!runtimeError || isFixing) return; setIsFixing(true); try { - const newPath = await fixItCharm(charmManager, charm, runtimeError); - if (newPath) { - setRuntimeError(null); - navigate( - createPath("charmShow", { - charmId: newPath, - replicaName: currentReplica, - }), - ); - } + const newCharm = await fixItCharm(charmManager, charm, runtimeError); + setRuntimeError(null); + navigate( + createPath("charmShow", { + charmId: charmId(newCharm)!, + replicaName: currentReplica, + }), + ); } catch (error) { console.error("Fix it error:", error); } finally { diff --git a/jumble/src/components/commands.ts b/jumble/src/components/commands.ts index 0b67db75c..e5bfbfd43 100644 --- a/jumble/src/components/commands.ts +++ b/jumble/src/components/commands.ts @@ -135,8 +135,7 @@ export const castSpellAsCharm = async ( recipe, argument, ); - charmManager.add([charm]); - return charm.entityId; + return charm; } console.log("Failed to cast"); return null; @@ -293,25 +292,28 @@ async function handleSearchCharms(deps: CommandContext) { } } -async function handleEditRecipe( +function handleEditRecipe( deps: CommandContext, input: string | undefined, ) { if (!input || !deps.focusedCharmId || !deps.focusedReplicaId) return; deps.setLoading(true); - const newCharmPath = await iterateCharm( + iterateCharm( deps.charmManager, deps.focusedCharmId, - deps.focusedReplicaId, input, - false, deps.preferredModel, - ); - if (newCharmPath) { - deps.navigate(newCharmPath); - } - deps.setLoading(false); - deps.setOpen(false); + ).then((newCharm) => { + deps.navigate(createPath("charmShow", { + charmId: charmId(newCharm)!, + replicaName: deps.focusedReplicaId!, + })); + }).catch((error) => { + console.error("Error editing recipe:", error); + }).finally(() => { + deps.setLoading(false); // FIXME(ja): load status should update on exception + deps.setOpen(false); + }); } async function handleExtendRecipe( @@ -320,17 +322,15 @@ async function handleExtendRecipe( ) { if (!input || !deps.focusedCharmId || !deps.focusedReplicaId) return; deps.setLoading(true); - const newCharmPath = await extendCharm( + const newCharm = await extendCharm( deps.charmManager, deps.focusedCharmId, - deps.focusedReplicaId, input, - false, - deps.preferredModel, ); - if (newCharmPath) { - deps.navigate(newCharmPath); - } + deps.navigate(createPath("charmShow", { + charmId: charmId(newCharm)!, + replicaName: deps.focusedReplicaId, + })); deps.setLoading(false); deps.setOpen(false); } diff --git a/jumble/src/hooks/use-audio-recorder.ts b/jumble/src/hooks/use-audio-recorder.ts index fb852cfdd..8de7009d2 100644 --- a/jumble/src/hooks/use-audio-recorder.ts +++ b/jumble/src/hooks/use-audio-recorder.ts @@ -1,20 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -// Define NodeJS.Timeout for Deno compatibility -declare global { - namespace NodeJS { - interface Timeout { - _idleTimeout: number; - _idlePrev: object; - _idleNext: object; - _idleStart: number; - _onTimeout: () => void; - _timerArgs: unknown[]; - _repeat: number | null; - } - } -} - interface AudioRecorderOptions { transcribe?: boolean; url?: string; diff --git a/jumble/src/hooks/use-charm.ts b/jumble/src/hooks/use-charm.ts index 46f78b8c2..505249779 100644 --- a/jumble/src/hooks/use-charm.ts +++ b/jumble/src/hooks/use-charm.ts @@ -18,9 +18,12 @@ const loadCharmData = async ( let iframeRecipe = null; if (charm) { - await charmManager.syncRecipe(charm); - const ir = getIframeRecipe(charm); - iframeRecipe = ir?.iframe ?? null; + try { + const ir = getIframeRecipe(charm); + iframeRecipe = ir?.iframe ?? null; + } catch (e) { + console.info(e); + } } return { charm, iframeRecipe }; diff --git a/jumble/src/utils/charm-operations.ts b/jumble/src/utils/charm-operations.ts index 1016a276f..345cdc5c8 100644 --- a/jumble/src/utils/charm-operations.ts +++ b/jumble/src/utils/charm-operations.ts @@ -1,26 +1,23 @@ import { + castNewRecipe, Charm, CharmManager, - extend, + generateNewRecipeVersion, getIframeRecipe, iterate, - saveNewRecipeVersion, } from "@commontools/charm"; -import { Cell, EntityId } from "@commontools/runner"; - -import { charmId } from "@/utils/charms.ts"; +import { Cell } from "@commontools/runner"; import { fixRecipePrompt } from "@/utils/prompt-library/recipe-fix.ts"; -import { createPath } from "@/routes.ts"; export async function fixItCharm( charmManager: CharmManager, charm: Cell, error: Error, model = "anthropic:claude-3-7-sonnet-20250219-thinking", -): Promise { +): Promise> { const iframeRecipe = getIframeRecipe(charm); - if (!iframeRecipe?.iframe) { - throw new Error("No iframe recipe found in charm"); + if (!iframeRecipe.iframe) { + throw new Error("Fixit only works for iframe charms"); } const fixedCode = await fixRecipePrompt( @@ -30,105 +27,36 @@ export async function fixItCharm( error.message, model, ); - if (!fixedCode) { - throw new Error("Could not extract fixed code from LLM response"); - } - const newCharm = await saveNewRecipeVersion( + return generateNewRecipeVersion( charmManager, charm, fixedCode, iframeRecipe.iframe.spec, ); - const newCharmId = charmId(newCharm as EntityId); - - console.log("new charm id", newCharmId); - return newCharmId; } export async function extendCharm( charmManager: CharmManager, focusedCharmId: string, - focusedReplicaId: string, - input: string, - variants: boolean = false, - preferredModel?: string, -): Promise { - try { - console.group("Extending Charm"); - console.log("Performing extension"); - console.log("Focused Charm ID", focusedCharmId); - console.log("Focused Replica ID", focusedReplicaId); - console.log("Input", input); - console.log("Variants", variants); - console.log("Preferred Model", preferredModel); - const charm = await charmManager.get(focusedCharmId); - console.log("CHARM", charm); - const newCharmId = await extend( - charmManager, - charm ?? null, - input, - preferredModel, - ); - if (!newCharmId) { - throw new Error("No new charm ID found after extend()"); - } - console.log("NEW CHARM ID", newCharmId); - console.groupEnd(); - const id = charmId(newCharmId); - if (!id) { - throw new Error("Invalid charm ID"); - } - return createPath("charmShow", { - charmId: id, - replicaName: focusedReplicaId, - }); - } catch (error) { - console.groupEnd(); - console.error("Extend recipe error:", error); - } + spec: string, +): Promise> { + const charm = (await charmManager.get(focusedCharmId, false))!; + return castNewRecipe(charmManager, charm, spec); } export async function iterateCharm( charmManager: CharmManager, focusedCharmId: string, - focusedReplicaId: string, input: string, - variants: boolean, preferredModel?: string, -): Promise { - try { - console.group("Iterating Charm"); - console.log("Performing iteration"); - console.log("Focused Charm ID", focusedCharmId); - console.log("Focused Replica ID", focusedReplicaId); - console.log("Input", input); - console.log("Variants", variants); - console.log("Preferred Model", preferredModel); - const charm = await charmManager.get(focusedCharmId); - console.log("CHARM", charm); - const newCharmId = await iterate( - charmManager, - charm ?? null, - input, - false, - preferredModel, - ); - if (!newCharmId) { - throw new Error("No new charm ID found after iterate()"); - } - console.log("NEW CHARM ID", newCharmId); - console.groupEnd(); - const id = charmId(newCharmId); - if (!id) { - throw new Error("Invalid charm ID"); - } - return createPath("charmShow", { - charmId: id, - replicaName: focusedReplicaId, - }); - } catch (error) { - console.groupEnd(); - console.error("Edit recipe error:", error); - } +): Promise> { + const charm = (await charmManager.get(focusedCharmId, false))!; + return iterate( + charmManager, + charm, + input, + false, + preferredModel, + ); } diff --git a/jumble/src/utils/prompt-library/prompting.ts b/jumble/src/utils/prompt-library/prompting.ts index c6c120360..1e547c491 100644 --- a/jumble/src/utils/prompt-library/prompting.ts +++ b/jumble/src/utils/prompt-library/prompting.ts @@ -22,10 +22,12 @@ export function hydratePrompt(prompt: string, context: any): string { export function parseTagFromResponse( response: string, tag: string, -): string | null { +): string { const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(`<${escapedTag}>([\\s\\S]*?)`); const match = response.trim().match(regex); - - return match ? match[1].trim() : null; + if (match && match[1]) { + return match[1].trim(); + } + throw new Error(`Tag ${tag} not found in response`); } diff --git a/jumble/src/views/CharmDetailView.tsx b/jumble/src/views/CharmDetailView.tsx index 638489c75..63434f3d2 100644 --- a/jumble/src/views/CharmDetailView.tsx +++ b/jumble/src/views/CharmDetailView.tsx @@ -1,8 +1,8 @@ import { Charm, + generateNewRecipeVersion, getIframeRecipe, IFrameRecipe, - saveNewRecipeVersion, } from "@commontools/charm"; import React, { createContext, @@ -28,7 +28,7 @@ import { generateCharmSuggestions, } from "@/utils/prompt-library/charm-suggestions.ts"; import { Cell } from "@commontools/runner"; -import { createPath, createPathWithHash } from "@/routes.ts"; +import { createPath } from "@/routes.ts"; import JsonView from "@uiw/react-json-view"; type Tab = "iterate" | "code" | "data"; @@ -264,7 +264,18 @@ function useCodeEditor( const saveChanges = useCallback(() => { if (workingSrc && iframeRecipe && charm) { - saveNewRecipeVersion(charmManager, charm, workingSrc, iframeRecipe.spec); + generateNewRecipeVersion( + charmManager, + charm, + workingSrc, + iframeRecipe.spec, + ).then((newCharm) => { + console.log("Fixme, navigate to editted charm", newCharm); + // navigate(createPath("charmShow", { + // charmId: charmId(newCharm)!, + // replicaName: replicaName, + // })); + }); } }, [workingSrc, iframeRecipe, charm]); @@ -302,7 +313,7 @@ function useCharmOperation() { // Function that performs the selected operation (iterate or extend) const performOperation = useCallback( - async ( + ( charmId: string, replicaName: string, input: string, @@ -310,22 +321,17 @@ function useCharmOperation() { model: string, ) => { if (operationType === "iterate") { - return await iterateCharm( + return iterateCharm( charmManager, charmId, - replicaName, input, - replace, model, ); } else { - return await extendCharm( + return extendCharm( charmManager, charmId, - replicaName, input, - replace, - model, ); } }, @@ -337,46 +343,30 @@ function useCharmOperation() { if (!input || !charm || !paramCharmId || !replicaName) return; setLoading(true); - const handleVariants = () => { + const handleVariants = async () => { setVariants([]); setSelectedVariant(charm); - try { - // For each model, start generating a variant - variantModels.forEach(async (model) => { - try { - const path = await performOperation( - charmId(charm)!, - replicaName!, - input, - false, - model, - ); - if (path) { - const id = path.split("/").pop()!; - const newCharm = await charmManager.get(id); - if (newCharm) { - // Store the variant and keep track of which model was used - setVariants((prev) => [...prev, newCharm]); - setVariantModelsMap((prev) => ({ - ...prev, - [charmId(newCharm) || ""]: model, - })); - // Set the first completed variant as selected if none selected - setSelectedVariant((current) => - current === charm ? newCharm : current - ); - } - } - } catch (error) { - console.error(`Variant ${model} generation error:`, error); - } - }); - } catch (error) { - console.error("Variants error:", error); - } finally { + const gens = variantModels.map(async (model) => { + const newCharm = await performOperation( + charmId(charm)!, + replicaName!, + input, + false, + model, + ); + // Store the variant and keep track of which model was used + setVariants((prev) => [...prev, newCharm]); setLoading(false); - } + setVariantModelsMap((prev) => ({ + ...prev, + [charmId(newCharm) || ""]: model, + })); + // Set the first completed variant as selected if none selected + setSelectedVariant((current) => current === charm ? newCharm : current); + }); + + await Promise.allSettled(gens); }; if (showVariants) { @@ -385,16 +375,17 @@ function useCharmOperation() { handleVariants(); } else { try { - const newPath = await performOperation( + const newCharm = await performOperation( charmId(charm)!, - replicaName!, + replicaName, input, false, selectedModel, ); - if (newPath) { - navigate(newPath); - } + navigate(createPath("charmShow", { + charmId: charmId(newCharm)!, + replicaName, + })); } catch (error) { console.error(`${operationType} error:`, error); } finally { diff --git a/scripts/check-all.sh b/scripts/check-all.sh index bfc58de18..f8b3cc03f 100755 --- a/scripts/check-all.sh +++ b/scripts/check-all.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash -# DISABLED deno check \ builder \ charm \