From a6b642ea762df6079f547a270228e77bb08f7689 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 17 Oct 2026 13:03:05 +0300 Subject: [PATCH 1/9] Tiny Sunny: run the chat on @base44/platform/react's useBase44Chat The chat's own Base44 layer (useLiveChat.ts: the socket, merging and sorting messages, the status, the question formats) is replaced by the hook, and ToolQuestion takes the hook's bound question instead of a raw tool call. - Chat draws items, steps and questions from the hook, shows a sent prompt at once (faded while pending), and keeps Send disabled unless the status is ready or error. - Chat no longer remounts per app, so the first prompt survives create. - openLiveSession passes Base44's socket-session body through, and sendMessage and submitToolCallInput return after 3 seconds: Base44 holds both open for the whole turn, and Next.js runs one server action at a time. - @base44/platform comes from base44/javascript-sdk#315's preview package until that PR is released. Co-Authored-By: Claude Opus 5.5 --- examples/white-label-minimal/app/tiny.css | 3 + .../white-label-minimal/components/Chat.tsx | 73 ++++++++--- .../components/ToolQuestion.tsx | 74 ++++++----- .../components/Workspace.tsx | 2 +- .../components/useLiveChat.ts | 88 ------------- examples/white-label-minimal/package.json | 2 +- examples/white-label-minimal/server/base44.ts | 18 ++- package-lock.json | 124 +++--------------- 8 files changed, 127 insertions(+), 257 deletions(-) delete mode 100644 examples/white-label-minimal/components/useLiveChat.ts diff --git a/examples/white-label-minimal/app/tiny.css b/examples/white-label-minimal/app/tiny.css index 8db50de..ee56aec 100644 --- a/examples/white-label-minimal/app/tiny.css +++ b/examples/white-label-minimal/app/tiny.css @@ -52,6 +52,9 @@ .chat-user-text { @apply ml-auto max-w-[85%] rounded-lg bg-secondary px-3 py-2; } +.chat-pending { + @apply opacity-60; +} .chat-assistant-text { @apply whitespace-pre-wrap; } diff --git a/examples/white-label-minimal/components/Chat.tsx b/examples/white-label-minimal/components/Chat.tsx index b852d17..b10fdf7 100644 --- a/examples/white-label-minimal/components/Chat.tsx +++ b/examples/white-label-minimal/components/Chat.tsx @@ -1,28 +1,35 @@ "use client"; import { useEffect, useState } from "react"; -import type { ToolCall } from "@base44/platform"; +import { useBase44Chat, type Base44Chat, type Base44ChatBackend, type ChatItem, type ChatStep } from "@base44/platform/react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { StickToBottom } from "use-stick-to-bottom"; -import type { App } from "../server/base44"; +import { createApp, openLiveSession, sendMessage, submitToolCallInput, type App } from "../server/base44"; import ToolQuestion from "./ToolQuestion"; -import { useLiveChat } from "./useLiveChat"; + +// The four Base44 calls, run on the server. The hook calls them, so the browser never holds a key. +const backend: Base44ChatBackend = { createApp, openLiveSession, sendMessage, submitToolCallInput }; export default function Chat({ app, onAppCreated, onBuildingChange }: { app: App | null; onAppCreated: (app: App) => void; onBuildingChange: (building: boolean) => void; }) { - const { messages, status, building, canSend, send } = useLiveChat({ app, onAppCreated }); + const chat = useBase44Chat({ appId: app?.id ?? null, backend, onAppCreated }); const [prompt, setPrompt] = useState(""); + const building = chat.status === "building"; + // Send only when the builder can take a prompt now. After a failed build, a prompt can ask for the fix. + const canSend = chat.status === "ready" || chat.status === "error"; useEffect(() => { onBuildingChange(building); }, [building, onBuildingChange]); + // The first prompt creates the app; every later one goes to it. function submit() { - if (!prompt.trim() || !canSend) return; - send(prompt); + if (!canSend || !prompt.trim()) return; + if (app) chat.send(prompt); + else chat.create(prompt); setPrompt(""); } @@ -30,21 +37,19 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: {
- {messages.map((m) => ( -
- {m.content && ( -

{m.content}

- )} - {m.tool_calls?.map((tool) => - tool.status === "waiting_for_user_input" && app ? ( - + {chat.items.map((item) => ( +
+ {item.text &&

{item.text}

} + {item.steps.map((step) => + step.question ? ( + ) : ( -

{toolLine(tool)}

+

{stepIcon(step)} {step.label}

), )}
))} - {status &&

{status}

} +
@@ -60,14 +65,40 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: { className="chat-input" rows={1} /> - +
); } -// One tool step as a short line, e.g. "✓ write_file src/pages/Home.jsx". -function toolLine(tool: ToolCall) { - const icon = tool.status === "running" ? "…" : tool.status === "error" ? "✗" : "✓"; - return `${icon} ${tool.name} ${tool.display?.file_paths?.join(", ") ?? ""}`; +// A prompt Base44 hasn't confirmed yet shows faded. +function textClass(item: ChatItem) { + if (item.role === "assistant") return "chat-assistant-text"; + if (item.pending) return "chat-user-text chat-pending"; + return "chat-user-text"; +} + +function stepIcon(step: ChatStep) { + if (step.status === "running") return "…"; + if (step.status === "waiting") return "?"; + if (step.status === "error") return "✗"; + if (step.status === "stopped") return "–"; + return "✓"; +} + +// Tiny's wording for what the hook reports. +function StatusLine({ chat, hasApp }: { chat: Base44Chat; hasApp: boolean }) { + let text = ""; + if (chat.error) text = chat.error.message; + else if (chat.status === "submitted") text = hasApp ? "Sending…" : "Creating app…"; + else if (chat.status === "waiting") text = "Waiting for your answer"; + else if (chat.status === "building") text = "Building…"; + else if (hasApp && chat.connection === "connecting") text = "Loading the conversation…"; + if (!text) return null; + return ( +

+ {text} + {chat.error && } +

+ ); } diff --git a/examples/white-label-minimal/components/ToolQuestion.tsx b/examples/white-label-minimal/components/ToolQuestion.tsx index d215f0d..3331211 100644 --- a/examples/white-label-minimal/components/ToolQuestion.tsx +++ b/examples/white-label-minimal/components/ToolQuestion.tsx @@ -1,59 +1,61 @@ "use client"; import { useState } from "react"; -import type { ToolCall, ToolQuestionArguments, ToolSecretArguments } from "@base44/platform"; +import type { ChatQuestion } from "@base44/platform/react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { submitToolCallInput } from "../server/base44"; -// A tool step the builder waits on: a choice, values to enter (such as API keys), or an -// approval. Declining is always an answer. -export default function ToolQuestion({ appId, messageId, tool }: { appId: string; messageId: string; tool: ToolCall }) { +// A question the builder waits on: a choice, secrets to enter (such as API keys), or an +// approval. Its actions are already bound to its tool call, and declining is always an answer. +export default function ToolQuestion({ question }: { question: ChatQuestion }) { const [sent, setSent] = useState(false); - const [picked, setPicked] = useState>({}); - const [secrets, setSecrets] = useState>({}); - const kind = tool.waiting_on?.kind; - const questions = kind === "choice" ? (tool.arguments as ToolQuestionArguments)?.questions ?? [] : []; + const [picked, setPicked] = useState(question.kind === "choice" ? question.questions.map(() => []) : []); // Tiny passes these values to Base44 and keeps none. - const fields = kind === "input" ? (tool.arguments as ToolSecretArguments)?.secrets_schema ?? [] : []; + const [secrets, setSecrets] = useState>({}); - function answer(approve: boolean, extraUserInput?: object) { + // A failed answer opens the question again, so the buttons come back. + async function send(action: () => Promise) { setSent(true); - submitToolCallInput(appId, { toolCallId: tool.id!, messageId, approve, extraUserInput }).catch(() => setSent(false)); + await action(); + setSent(false); } - function pick(index: number, label: string, multi?: boolean) { - const current = picked[index] ?? []; - const next = !multi ? [label] : current.includes(label) ? current.filter((l) => l !== label) : [...current, label]; - setPicked({ ...picked, [index]: next }); + function pick(index: number, label: string, multi: boolean) { + const current = picked[index]; + let next = [label]; + if (multi && current.includes(label)) next = current.filter((l) => l !== label); + else if (multi) next = [...current, label]; + setPicked(picked.map((labels, i) => (i === index ? next : labels))); } function submit() { - if (kind === "choice") answer(true, { answers: questions.map((_, i) => ({ question_index: i, selected_labels: picked[i] ?? [] })) }); - else if (kind === "input") answer(true, { secrets }); - else answer(true); + if (question.kind === "choice") return send(() => question.answer(picked)); + if (question.kind === "secrets") return send(() => question.submit(secrets)); + return send(question.approve); } return (
- {kind === "approval" &&

Allow {tool.name}? {tool.approval?.reason ?? tool.approval?.details?.summary}

} - {questions.map((q, i) => ( -
-

{q.question}

-
- {(q.options ?? []).map((o) => (typeof o === "string" ? o : o.label ?? "")).map((label) => ( - - ))} + {question.kind === "approval" &&

Allow {question.action}? {question.reason}

} + {question.kind === "choice" && + question.questions.map((q, i) => ( +
+

{q.text}

+
+ {q.options.map((label) => ( + + ))} +
-
- ))} - {fields.map(({ secretName = "" }) => ( - setSecrets({ ...secrets, [secretName]: e.target.value })} /> - ))} + ))} + {question.kind === "secrets" && + question.fields.map(({ name }) => ( + setSecrets({ ...secrets, [name]: e.target.value })} /> + ))}
- - + +
); diff --git a/examples/white-label-minimal/components/Workspace.tsx b/examples/white-label-minimal/components/Workspace.tsx index 734ab99..62f772f 100644 --- a/examples/white-label-minimal/components/Workspace.tsx +++ b/examples/white-label-minimal/components/Workspace.tsx @@ -32,7 +32,7 @@ export default function Workspace() {
- +
); } diff --git a/examples/white-label-minimal/components/useLiveChat.ts b/examples/white-label-minimal/components/useLiveChat.ts deleted file mode 100644 index 1cd7022..0000000 --- a/examples/white-label-minimal/components/useLiveChat.ts +++ /dev/null @@ -1,88 +0,0 @@ -"use client"; -import { useEffect, useState } from "react"; -import { Base44PlatformClient, type ChatMessage } from "@base44/platform"; -import { createApp, openLiveSession, sendMessage, type App } from "../server/base44"; - -// The chat's state: the live connection to Base44, message ordering, create-or-send, -// and the status line. -export function useLiveChat({ app, onAppCreated }: { - app: App | null; - onAppCreated: (app: App) => void; -}) { - const appId = app?.id ?? null; - const [messages, setMessages] = useState([]); - const [building, setBuilding] = useState(false); - const [loading, setLoading] = useState(!!appId); - const [creating, setCreating] = useState(false); - const [error, setError] = useState(""); - - // Live updates: a snapshot on every connect, then events. - useEffect(() => { - if (!appId) return; - let stopped = false; - let socket: { close(): void } | undefined; - const fail = (e: { code?: string; message?: string }) => setError(e.code || e.message || "Live updates failed"); - - openLiveSession(appId) - .then(({ serverUrl, sessionToken }) => { - if (stopped) return; - const builder = new Base44PlatformClient({ serverUrl, getSessionToken: () => sessionToken }).builder.init({ onError: fail }); - socket = builder; - builder.subscribe(appId, { - onSnapshot: (snapshot) => { - setMessages(snapshot.messages); - setLoading(false); - setBuilding(snapshot.status?.state === "processing"); - }, - onEvent: (event) => { - if (event.type === "message.updated") { - const message = event.data.message; - // Replace the message with the same id in place, or add it at the end. - setMessages((all) => (all.some((m) => m.id === message.id) ? all.map((m) => (m.id === message.id ? message : m)) : [...all, message])); - } - if (event.type === "app.status_changed") setBuilding(event.data.status?.state === "processing"); - }, - onError: fail, - }); - return builder.connect(); - }) - .catch(fail); - - return () => { - stopped = true; - socket?.close(); - }; - }, [appId]); - - // Events can arrive out of order, so order messages by when they were created. - const sorted = [...messages].sort((a, b) => (a.metadata?.created_date ?? "").localeCompare(b.metadata?.created_date ?? "")); - // Lock the message box while a question is open or a build runs. - const waiting = sorted.some((m) => m.tool_calls?.some((tool) => tool.status === "waiting_for_user_input")); - - let status = error; - if (!status && creating) status = "Creating app…"; - else if (!status && loading) status = "Loading the conversation…"; - else if (!status && waiting) status = "Waiting for your answer"; - else if (!status && building) status = "Building…"; - - // The first prompt creates the app; every later one goes to it. - async function send(prompt: string) { - if (appId) { - setBuilding(true); - sendMessage(appId, prompt).catch((e) => { - setBuilding(false); - setError(e.message); - }); - return; - } - setCreating(true); - try { - onAppCreated(await createApp(prompt)); - } catch (e) { - setError((e as Error).message); - } - setCreating(false); - } - - return { messages: sorted, status, building, canSend: !creating && !waiting && !building, send }; -} diff --git a/examples/white-label-minimal/package.json b/examples/white-label-minimal/package.json index d3226cd..f3fa3d9 100644 --- a/examples/white-label-minimal/package.json +++ b/examples/white-label-minimal/package.json @@ -9,7 +9,7 @@ "typecheck": "next typegen && tsc --noEmit" }, "dependencies": { - "@base44/platform": "^0.1.0", + "@base44/platform": "npm:@base44-preview/platform@0.2.0-pr.315.49e926c", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "lucide-react": "^0.475.0", diff --git a/examples/white-label-minimal/server/base44.ts b/examples/white-label-minimal/server/base44.ts index f867d42..e96f804 100644 --- a/examples/white-label-minimal/server/base44.ts +++ b/examples/white-label-minimal/server/base44.ts @@ -1,4 +1,5 @@ "use server"; +import type { SocketSession } from "@base44/platform/react"; export type App = { id: string; name: string }; export type ToolCallAnswer = { toolCallId: string; messageId: string; approve: boolean; extraUserInput?: object }; @@ -31,17 +32,22 @@ export async function listApps(): Promise { .map(({ id, name }) => ({ id, name })); } -export async function openLiveSession(appId: string) { - const session = await call("/api/service/socket-sessions", { body: { app_ids: [appId] }, headers: serviceKey }); - return { serverUrl: new URL(session.socket_url).origin, sessionToken: session.session_token as string }; +// The body holds only a session token, never the key, so it goes to the browser unchanged. +export async function openLiveSession(appId: string): Promise { + return call("/api/service/socket-sessions", { body: { app_ids: [appId] }, headers: serviceKey }); } +// Base44 holds this request open for the whole turn, and Next.js runs one server action at a time, +// so waiting for it would hold every later call. Wait only long enough to catch a refusal. export async function sendMessage(appId: string, content: string) { - await call(`/api/apps/${appId}/chat/message`, { body: { content } }); + const sent = request(`/api/apps/${appId}/chat/message`, { body: { content } }); + const response = await Promise.race([sent, new Promise((resolve) => setTimeout(resolve, 3000, null))]); + if (response && !response.ok) throw new Error(`Base44 /chat/message failed: ${response.status}`); } +// Held open for the resumed turn, like sendMessage. export async function submitToolCallInput(appId: string, answer: ToolCallAnswer) { - await call(`/api/apps/${appId}/chat/submit-tool-call-input`, { + const sent = request(`/api/apps/${appId}/chat/submit-tool-call-input`, { body: { tool_call_id: answer.toolCallId, message_id: answer.messageId, @@ -50,6 +56,8 @@ export async function submitToolCallInput(appId: string, answer: ToolCallAnswer) }, headers: { ...accessToken, "X-Request-ID": `submit-${answer.toolCallId}` }, }); + const response = await Promise.race([sent, new Promise((resolve) => setTimeout(resolve, 3000, null))]); + if (response && !response.ok) throw new Error(`Base44 /chat/submit-tool-call-input failed: ${response.status}`); } export async function getPreviewUrl(appId: string, target: "latest_preview" | "live_preview") { diff --git a/package-lock.json b/package-lock.json index 33b36b2..7cd9252 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,41 +50,11 @@ "typescript": "^5" } }, - "examples/tiny-sunny": { - "name": "@base44/tiny-sunny", - "version": "0.1.0", - "extraneous": true, - "dependencies": { - "@assistant-ui/react": "^0.15.21", - "@assistant-ui/react-markdown": "^0.14.16", - "@base44/platform": "^0.1.0", - "@chatscope/chat-ui-kit-react": "^2.1.1", - "@chatscope/chat-ui-kit-styles": "^1.4.0", - "class-variance-authority": "^0.7.1", - "cn": "^0.4.0", - "lucide-react": "^0.475.0", - "next": "16.3.0", - "radix-ui": "^1.6.7", - "react": "19.2.8", - "react-dom": "19.2.8", - "shadcn": "^4.21.0", - "tw-animate-css": "^1.4.0", - "tw-shimmer": "^0.4.13" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.3.3", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "tailwindcss": "^4.3.3", - "typescript": "^5" - } - }, "examples/white-label-minimal": { "name": "@base44/white-label-minimal", "version": "0.1.0", "dependencies": { - "@base44/platform": "^0.1.0", + "@base44/platform": "npm:@base44-preview/platform@0.2.0-pr.315.49e926c", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "lucide-react": "^0.475.0", @@ -108,6 +78,24 @@ "node": "^22 || ^24 || >=26" } }, + "examples/white-label-minimal/node_modules/@base44/platform": { + "name": "@base44-preview/platform", + "version": "0.2.0-pr.315.49e926c", + "resolved": "https://registry.npmjs.org/@base44-preview/platform/-/platform-0.2.0-pr.315.49e926c.tgz", + "integrity": "sha512-8ckxpJxOaR3biQyIHLQ/iA+yMPTtSMiT6so4ZdiFIaPv9vE1YxelqjMWi9EHKzYVV+p+IOKmkkXT5+h2Q6wY9A==", + "license": "MIT", + "dependencies": { + "socket.io-client": "^4.8.3" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -564,14 +552,6 @@ "node": ">=6.9.0" } }, - "node_modules/@base44/platform": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@base44/platform/-/platform-0.1.0.tgz", - "integrity": "sha512-6InHb3mM2RPLqU42atFprwkkjR1Osu0CFQpAmloD1sAv7EfDg0h4rGMuUtSB5EZJLJiz4f6DaQ44dP/lR7mjtQ==", - "dependencies": { - "socket.io-client": "^4.8.3" - } - }, "node_modules/@base44/white-label-minimal": { "resolved": "examples/white-label-minimal", "link": true @@ -2411,23 +2391,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/@playwright/test": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", - "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "playwright": "1.62.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@prisma/client": { "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", @@ -11217,55 +11180,6 @@ "node": ">=4" } }, - "node_modules/playwright": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", - "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "playwright-core": "1.62.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", - "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", From eea7e3260ebd89491181d3b9d53b00efbc0f0041 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sun, 18 Oct 2026 16:20:38 +0300 Subject: [PATCH 2/9] Tiny Sunny: the released @base44/platform 0.3.0, with free-text answers, approval details and answers - @base44/platform is the released ^0.3.0 instead of #315's preview package. - A text_input question gets a text field, and the answer sends it. - An approval shows what the action would do: the entity, the change, the access, the function, as far as Base44's check says. - A step that was answered keeps its questions and answers under its line. Co-Authored-By: Claude Opus 5.5 --- examples/white-label-minimal/app/tiny.css | 6 +++ .../white-label-minimal/components/Chat.tsx | 8 +++- .../components/ToolQuestion.tsx | 42 ++++++++++++++----- examples/white-label-minimal/package.json | 2 +- package-lock.json | 40 +++++++++--------- 5 files changed, 66 insertions(+), 32 deletions(-) diff --git a/examples/white-label-minimal/app/tiny.css b/examples/white-label-minimal/app/tiny.css index ee56aec..4f20faf 100644 --- a/examples/white-label-minimal/app/tiny.css +++ b/examples/white-label-minimal/app/tiny.css @@ -83,6 +83,12 @@ .question-options { @apply flex flex-wrap gap-2; } +.question-details { + @apply text-xs text-muted-foreground; +} +.chat-answer { + @apply ml-4 text-xs text-muted-foreground; +} .question-actions { @apply flex gap-2; } diff --git a/examples/white-label-minimal/components/Chat.tsx b/examples/white-label-minimal/components/Chat.tsx index b10fdf7..cbd571e 100644 --- a/examples/white-label-minimal/components/Chat.tsx +++ b/examples/white-label-minimal/components/Chat.tsx @@ -44,7 +44,13 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: { step.question ? ( ) : ( -

{stepIcon(step)} {step.label}

+
+

{stepIcon(step)} {step.label}

+ {/* What the user answered stays after the question closes. */} + {step.answered?.map((answer, i) => ( +

{answer.question}: {[...answer.labels, answer.text].filter(Boolean).join(", ")}

+ ))} +
), )}
diff --git a/examples/white-label-minimal/components/ToolQuestion.tsx b/examples/white-label-minimal/components/ToolQuestion.tsx index 3331211..f0b331e 100644 --- a/examples/white-label-minimal/components/ToolQuestion.tsx +++ b/examples/white-label-minimal/components/ToolQuestion.tsx @@ -1,14 +1,16 @@ "use client"; import { useState } from "react"; -import type { ChatQuestion } from "@base44/platform/react"; +import type { ApprovalDetails, ChatQuestion } from "@base44/platform/react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -// A question the builder waits on: a choice, secrets to enter (such as API keys), or an -// approval. Its actions are already bound to its tool call, and declining is always an answer. +// A question the AI waits on: a choice (options, or free text), secrets to enter (such as API +// keys), or an approval. Its actions are already bound to its tool call, and declining is always +// an answer. export default function ToolQuestion({ question }: { question: ChatQuestion }) { const [sent, setSent] = useState(false); const [picked, setPicked] = useState(question.kind === "choice" ? question.questions.map(() => []) : []); + const [texts, setTexts] = useState([]); // Tiny passes these values to Base44 and keeps none. const [secrets, setSecrets] = useState>({}); @@ -28,7 +30,7 @@ export default function ToolQuestion({ question }: { question: ChatQuestion }) { } function submit() { - if (question.kind === "choice") return send(() => question.answer(picked)); + if (question.kind === "choice") return send(() => question.answer(picked, texts)); if (question.kind === "secrets") return send(() => question.submit(secrets)); return send(question.approve); } @@ -36,17 +38,22 @@ export default function ToolQuestion({ question }: { question: ChatQuestion }) { return (
{question.kind === "approval" &&

Allow {question.action}? {question.reason}

} + {question.kind === "approval" &&

{describe(question.details)}

} {question.kind === "choice" && question.questions.map((q, i) => (

{q.text}

-
- {q.options.map((label) => ( - - ))} -
+ {q.type === "text_input" ? ( + setTexts(Object.assign([...texts], { [i]: e.target.value }))} /> + ) : ( +
+ {q.options.map((label) => ( + + ))} +
+ )}
))} {question.kind === "secrets" && @@ -60,3 +67,16 @@ export default function ToolQuestion({ question }: { question: ChatQuestion }) {
); } + +// What the approved action would do, as far as Base44's check says. +function describe(details: ApprovalDetails) { + const parts: string[] = []; + if (details.summary) parts.push(details.summary); + if (details.entityName) parts.push(`Entity: ${details.entityName}`); + if (details.changeType) parts.push(`Change: ${details.changeType}`); + if (details.changedOps?.length) parts.push(`Access: ${details.changedOps.join(", ")}`); + if (details.functionName) parts.push(`Function: ${details.functionName}`); + if (details.method) parts.push(`Method: ${details.method}`); + if (details.inspectionFailed) parts.push("Base44 couldn't check this function's code"); + return parts.join(" · "); +} diff --git a/examples/white-label-minimal/package.json b/examples/white-label-minimal/package.json index f3fa3d9..2762899 100644 --- a/examples/white-label-minimal/package.json +++ b/examples/white-label-minimal/package.json @@ -9,7 +9,7 @@ "typecheck": "next typegen && tsc --noEmit" }, "dependencies": { - "@base44/platform": "npm:@base44-preview/platform@0.2.0-pr.315.49e926c", + "@base44/platform": "^0.3.0", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "lucide-react": "^0.475.0", diff --git a/package-lock.json b/package-lock.json index 7cd9252..8e51883 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "name": "@base44/white-label-minimal", "version": "0.1.0", "dependencies": { - "@base44/platform": "npm:@base44-preview/platform@0.2.0-pr.315.49e926c", + "@base44/platform": "^0.3.0", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "lucide-react": "^0.475.0", @@ -78,24 +78,6 @@ "node": "^22 || ^24 || >=26" } }, - "examples/white-label-minimal/node_modules/@base44/platform": { - "name": "@base44-preview/platform", - "version": "0.2.0-pr.315.49e926c", - "resolved": "https://registry.npmjs.org/@base44-preview/platform/-/platform-0.2.0-pr.315.49e926c.tgz", - "integrity": "sha512-8ckxpJxOaR3biQyIHLQ/iA+yMPTtSMiT6so4ZdiFIaPv9vE1YxelqjMWi9EHKzYVV+p+IOKmkkXT5+h2Q6wY9A==", - "license": "MIT", - "dependencies": { - "socket.io-client": "^4.8.3" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -552,6 +534,26 @@ "node": ">=6.9.0" } }, + "node_modules/@base44/platform": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@base44/platform/-/platform-0.3.0.tgz", + "integrity": "sha512-BlBWUIEdYLH0po4LFMzKPo1mIOKUv8bo9z3LmyABSEv/2AXGP/fIN4KxZl5DBYGlgnVf8h3aQeY36XT4eHnWVQ==", + "license": "MIT", + "dependencies": { + "socket.io-client": "^4.8.3" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@base44/white-label-minimal": { "resolved": "examples/white-label-minimal", "link": true From 42c79a2deb46c2c2fdcb6a94012c6bb2142a19de Mon Sep 17 00:00:00 2001 From: amitbardos Date: Mon, 19 Oct 2026 17:56:15 +0300 Subject: [PATCH 3/9] Tiny Sunny: @base44/platform 0.4.0; "the AI", not "the builder" 0.4.0 renames the client's builder module to chat; Tiny Sunny only uses useBase44Chat, whose API is unchanged, so this is the version bump. Co-Authored-By: Claude Opus 5.5 --- examples/white-label-minimal/components/Chat.tsx | 2 +- examples/white-label-minimal/package.json | 2 +- package-lock.json | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/white-label-minimal/components/Chat.tsx b/examples/white-label-minimal/components/Chat.tsx index cbd571e..9c20b80 100644 --- a/examples/white-label-minimal/components/Chat.tsx +++ b/examples/white-label-minimal/components/Chat.tsx @@ -18,7 +18,7 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: { const chat = useBase44Chat({ appId: app?.id ?? null, backend, onAppCreated }); const [prompt, setPrompt] = useState(""); const building = chat.status === "building"; - // Send only when the builder can take a prompt now. After a failed build, a prompt can ask for the fix. + // Send only when the AI can take a prompt now. After a failed build, a prompt can ask for the fix. const canSend = chat.status === "ready" || chat.status === "error"; useEffect(() => { diff --git a/examples/white-label-minimal/package.json b/examples/white-label-minimal/package.json index 2762899..26860f7 100644 --- a/examples/white-label-minimal/package.json +++ b/examples/white-label-minimal/package.json @@ -9,7 +9,7 @@ "typecheck": "next typegen && tsc --noEmit" }, "dependencies": { - "@base44/platform": "^0.3.0", + "@base44/platform": "^0.4.0", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "lucide-react": "^0.475.0", diff --git a/package-lock.json b/package-lock.json index 8e51883..57b244c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "name": "@base44/white-label-minimal", "version": "0.1.0", "dependencies": { - "@base44/platform": "^0.3.0", + "@base44/platform": "^0.4.0", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "lucide-react": "^0.475.0", @@ -535,9 +535,9 @@ } }, "node_modules/@base44/platform": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@base44/platform/-/platform-0.3.0.tgz", - "integrity": "sha512-BlBWUIEdYLH0po4LFMzKPo1mIOKUv8bo9z3LmyABSEv/2AXGP/fIN4KxZl5DBYGlgnVf8h3aQeY36XT4eHnWVQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@base44/platform/-/platform-0.4.0.tgz", + "integrity": "sha512-vzlql7aM7nWFbS18uF1aXQcgyRhRDgZ3B2Varr9ogqAtwaX+j1wIMdwK2JGJj/yXPdGR8HKurGq6AOfA8BZGrA==", "license": "MIT", "dependencies": { "socket.io-client": "^4.8.3" From cc513246ed09a909b448600ae3561702f590fe97 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Wed, 21 Oct 2026 10:23:21 +0300 Subject: [PATCH 4/9] Tiny Sunny: one waitForRefusal for the held-open chat requests; log late failures sendMessage and submitToolCallInput share the 3-second cutoff instead of repeating it. A failure that lands after the cutoff has no caller left, so it is logged on the server rather than dropped. Co-Authored-By: Claude Opus 5.5 --- examples/white-label-minimal/server/base44.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/examples/white-label-minimal/server/base44.ts b/examples/white-label-minimal/server/base44.ts index 436aba9..12c8261 100644 --- a/examples/white-label-minimal/server/base44.ts +++ b/examples/white-label-minimal/server/base44.ts @@ -35,15 +35,11 @@ export async function openLiveSession(appId: string): Promise { return call("/api/v1/socket-sessions", { body: { app_ids: [appId] } }); } -// Base44 holds this request open for the whole turn, and Next.js runs one server action at a time, -// so waiting for it would hold every later call. Wait only long enough to catch a refusal. export async function sendMessage(appId: string, content: string) { const sent = request(`/api/apps/${appId}/chat/message`, { body: { content } }); - const response = await Promise.race([sent, new Promise((resolve) => setTimeout(resolve, 3000, null))]); - if (response && !response.ok) throw new Error(`Base44 /chat/message failed: ${response.status}`); + await waitForRefusal("/chat/message", sent); } -// Held open for the resumed turn, like sendMessage. export async function submitToolCallInput(appId: string, answer: ToolCallAnswer) { const sent = request(`/api/apps/${appId}/chat/submit-tool-call-input`, { body: { @@ -54,8 +50,7 @@ export async function submitToolCallInput(appId: string, answer: ToolCallAnswer) }, headers: { ...accessToken, "X-Request-ID": `submit-${answer.toolCallId}` }, }); - const response = await Promise.race([sent, new Promise((resolve) => setTimeout(resolve, 3000, null))]); - if (response && !response.ok) throw new Error(`Base44 /chat/submit-tool-call-input failed: ${response.status}`); + await waitForRefusal("/chat/submit-tool-call-input", sent); } // A new app has no latest preview until its first build gives it a slug: Base44 answers 400 @@ -72,6 +67,24 @@ export async function getPreviewUrl(appId: string, target: "latest_preview" | "l return data.embed_url as string; } +// Base44 holds a chat request open for the whole turn, and Next.js runs one server action at a time, +// so waiting for it would hold every later call. Wait only long enough to catch a refusal. +async function waitForRefusal(name: string, sent: Promise) { + const timeout = new Promise((resolve) => setTimeout(resolve, 3000, null)); + const response = await Promise.race([sent, timeout]); + if (response === null) { + // The caller has moved on, so a later failure can only be logged. + sent.then( + (late) => { + if (!late.ok) console.error(`Base44 ${name} failed: ${late.status}`); + }, + (error) => console.error(`Base44 ${name} failed:`, error), + ); + return; + } + if (!response.ok) throw new Error(`Base44 ${name} failed: ${response.status}`); +} + function request(path: string, { body, headers = accessToken }: Options = {}) { return fetch(`${host}${path}`, { method: body ? "POST" : "GET", From 88b52ae75a4ceb96c1d45272741b55d9355d7a86 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Wed, 21 Oct 2026 10:37:58 +0300 Subject: [PATCH 5/9] Tiny Sunny: an approval shows its action and reason only; current netlify.toml comment describe() repeated the summary the hook already puts in reason, so the approval line drops the details. The secrets-scan comment no longer names versions that both sides have moved past. Co-Authored-By: Claude Opus 5.5 --- examples/white-label-minimal/app/tiny.css | 3 --- .../components/ToolQuestion.tsx | 16 +--------------- examples/white-label-minimal/netlify.toml | 6 +++--- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/examples/white-label-minimal/app/tiny.css b/examples/white-label-minimal/app/tiny.css index 4f20faf..1ee6602 100644 --- a/examples/white-label-minimal/app/tiny.css +++ b/examples/white-label-minimal/app/tiny.css @@ -83,9 +83,6 @@ .question-options { @apply flex flex-wrap gap-2; } -.question-details { - @apply text-xs text-muted-foreground; -} .chat-answer { @apply ml-4 text-xs text-muted-foreground; } diff --git a/examples/white-label-minimal/components/ToolQuestion.tsx b/examples/white-label-minimal/components/ToolQuestion.tsx index f0b331e..4ba988b 100644 --- a/examples/white-label-minimal/components/ToolQuestion.tsx +++ b/examples/white-label-minimal/components/ToolQuestion.tsx @@ -1,6 +1,6 @@ "use client"; import { useState } from "react"; -import type { ApprovalDetails, ChatQuestion } from "@base44/platform/react"; +import type { ChatQuestion } from "@base44/platform/react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -38,7 +38,6 @@ export default function ToolQuestion({ question }: { question: ChatQuestion }) { return (
{question.kind === "approval" &&

Allow {question.action}? {question.reason}

} - {question.kind === "approval" &&

{describe(question.details)}

} {question.kind === "choice" && question.questions.map((q, i) => (
@@ -67,16 +66,3 @@ export default function ToolQuestion({ question }: { question: ChatQuestion }) {
); } - -// What the approved action would do, as far as Base44's check says. -function describe(details: ApprovalDetails) { - const parts: string[] = []; - if (details.summary) parts.push(details.summary); - if (details.entityName) parts.push(`Entity: ${details.entityName}`); - if (details.changeType) parts.push(`Change: ${details.changeType}`); - if (details.changedOps?.length) parts.push(`Access: ${details.changedOps.join(", ")}`); - if (details.functionName) parts.push(`Function: ${details.functionName}`); - if (details.method) parts.push(`Method: ${details.method}`); - if (details.inspectionFailed) parts.push("Base44 couldn't check this function's code"); - return parts.join(" · "); -} diff --git a/examples/white-label-minimal/netlify.toml b/examples/white-label-minimal/netlify.toml index bb91561..084491b 100644 --- a/examples/white-label-minimal/netlify.toml +++ b/examples/white-label-minimal/netlify.toml @@ -7,7 +7,7 @@ [build.environment] NODE_VERSION = "24" - # This example pins @base44/platform 0.1.0, outside the root's 0.3.0 range, so - # npm installs it under the example's own node_modules, which the secrets - # scanner would otherwise read. Installed packages are never served. + # When this example's @base44/platform range leaves the root's, npm installs it + # under the example's own node_modules, which the secrets scanner would + # otherwise read. Installed packages are never served. SECRETS_SCAN_OMIT_PATHS = "examples/white-label-minimal/node_modules/**" From 027de21a746948e1fc612940871520359d3d8947 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Wed, 21 Oct 2026 10:42:16 +0300 Subject: [PATCH 6/9] Tiny Sunny: one component per question kind ToolQuestion picks Choice, Secrets or Approval by kind, so each keeps only its own state. They share one Actions row for Submit/Allow and Decline. Co-Authored-By: Claude Opus 5.5 --- .../components/ToolQuestion.tsx | 116 +++++++++++------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/examples/white-label-minimal/components/ToolQuestion.tsx b/examples/white-label-minimal/components/ToolQuestion.tsx index 4ba988b..b1f962b 100644 --- a/examples/white-label-minimal/components/ToolQuestion.tsx +++ b/examples/white-label-minimal/components/ToolQuestion.tsx @@ -1,6 +1,6 @@ "use client"; import { useState } from "react"; -import type { ChatQuestion } from "@base44/platform/react"; +import type { ApprovalQuestion, ChatQuestion, ChoiceQuestion, SecretsQuestion } from "@base44/platform/react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -8,18 +8,14 @@ import { Input } from "@/components/ui/input"; // keys), or an approval. Its actions are already bound to its tool call, and declining is always // an answer. export default function ToolQuestion({ question }: { question: ChatQuestion }) { - const [sent, setSent] = useState(false); - const [picked, setPicked] = useState(question.kind === "choice" ? question.questions.map(() => []) : []); - const [texts, setTexts] = useState([]); - // Tiny passes these values to Base44 and keeps none. - const [secrets, setSecrets] = useState>({}); + if (question.kind === "choice") return ; + if (question.kind === "secrets") return ; + return ; +} - // A failed answer opens the question again, so the buttons come back. - async function send(action: () => Promise) { - setSent(true); - await action(); - setSent(false); - } +function Choice({ question }: { question: ChoiceQuestion }) { + const [picked, setPicked] = useState(question.questions.map(() => [])); + const [texts, setTexts] = useState([]); function pick(index: number, label: string, multi: boolean) { const current = picked[index]; @@ -29,40 +25,76 @@ export default function ToolQuestion({ question }: { question: ChatQuestion }) { setPicked(picked.map((labels, i) => (i === index ? next : labels))); } - function submit() { - if (question.kind === "choice") return send(() => question.answer(picked, texts)); - if (question.kind === "secrets") return send(() => question.submit(secrets)); - return send(question.approve); + function write(index: number, text: string) { + const next = [...texts]; + next[index] = text; + setTexts(next); } return (
- {question.kind === "approval" &&

Allow {question.action}? {question.reason}

} - {question.kind === "choice" && - question.questions.map((q, i) => ( -
-

{q.text}

- {q.type === "text_input" ? ( - setTexts(Object.assign([...texts], { [i]: e.target.value }))} /> - ) : ( -
- {q.options.map((label) => ( - - ))} -
- )} -
- ))} - {question.kind === "secrets" && - question.fields.map(({ name }) => ( - setSecrets({ ...secrets, [name]: e.target.value })} /> - ))} -
- - -
+ {question.questions.map((q, i) => ( +
+

{q.text}

+ {q.type === "text_input" ? ( + write(i, e.target.value)} /> + ) : ( +
+ {q.options.map((label) => ( + + ))} +
+ )} +
+ ))} + question.answer(picked, texts)} onDecline={question.decline} /> +
+ ); +} + +function Secrets({ question }: { question: SecretsQuestion }) { + // Tiny passes these values to Base44 and keeps none. + const [values, setValues] = useState>({}); + + return ( +
+ {question.fields.map(({ name }) => ( + setValues({ ...values, [name]: e.target.value })} /> + ))} + question.submit(values)} onDecline={question.decline} /> +
+ ); +} + +function Approval({ question }: { question: ApprovalQuestion }) { + return ( +
+

Allow {question.action}? {question.reason}

+ +
+ ); +} + +function Actions({ submitLabel, onSubmit, onDecline }: { + submitLabel: string; + onSubmit: () => Promise; + onDecline: () => Promise; +}) { + const [sent, setSent] = useState(false); + + // A failed answer opens the question again, so the buttons come back. + async function send(action: () => Promise) { + setSent(true); + await action(); + setSent(false); + } + + return ( +
+ +
); } From 3bd00cc06e59af03c9982f8053450db4cc4a2611 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Wed, 21 Oct 2026 10:47:31 +0300 Subject: [PATCH 7/9] Tiny Sunny: the AI's messages render as Markdown The AI answers in Markdown, so its text showed raw ** and backticks. It now goes through react-markdown, the version Sunny already installs, with a few chat styles for lists, code and links. Links open in a new tab. The user's own prompts stay plain text. Co-Authored-By: Claude Opus 5.5 --- examples/white-label-minimal/app/tiny.css | 25 ++++++++++++++++++- .../white-label-minimal/components/Chat.tsx | 12 ++++++--- examples/white-label-minimal/package.json | 1 + package-lock.json | 1 + 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/examples/white-label-minimal/app/tiny.css b/examples/white-label-minimal/app/tiny.css index 1ee6602..27ab0b7 100644 --- a/examples/white-label-minimal/app/tiny.css +++ b/examples/white-label-minimal/app/tiny.css @@ -56,7 +56,30 @@ @apply opacity-60; } .chat-assistant-text { - @apply whitespace-pre-wrap; + @apply flex flex-col gap-2; +} +.chat-assistant-text ul { + @apply list-disc pl-5; +} +.chat-assistant-text ol { + @apply list-decimal pl-5; +} +.chat-assistant-text h1, +.chat-assistant-text h2, +.chat-assistant-text h3 { + @apply font-semibold; +} +.chat-assistant-text code { + @apply rounded bg-secondary px-1 font-mono text-xs; +} +.chat-assistant-text pre { + @apply overflow-x-auto rounded bg-secondary p-2; +} +.chat-assistant-text pre code { + @apply bg-transparent p-0; +} +.chat-assistant-text a { + @apply underline; } .chat-tool-line { @apply text-xs text-muted-foreground; diff --git a/examples/white-label-minimal/components/Chat.tsx b/examples/white-label-minimal/components/Chat.tsx index 9c20b80..51aaf26 100644 --- a/examples/white-label-minimal/components/Chat.tsx +++ b/examples/white-label-minimal/components/Chat.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useBase44Chat, type Base44Chat, type Base44ChatBackend, type ChatItem, type ChatStep } from "@base44/platform/react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; +import Markdown from "react-markdown"; import { StickToBottom } from "use-stick-to-bottom"; import { createApp, openLiveSession, sendMessage, submitToolCallInput, type App } from "../server/base44"; import ToolQuestion from "./ToolQuestion"; @@ -39,7 +40,13 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: { {chat.items.map((item) => (
- {item.text &&

{item.text}

} + {item.text && item.role === "user" &&

{item.text}

} + {/* The AI answers in Markdown. */} + {item.text && item.role === "assistant" && ( + + )} {item.steps.map((step) => step.question ? ( @@ -78,8 +85,7 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: { } // A prompt Base44 hasn't confirmed yet shows faded. -function textClass(item: ChatItem) { - if (item.role === "assistant") return "chat-assistant-text"; +function userTextClass(item: ChatItem) { if (item.pending) return "chat-user-text chat-pending"; return "chat-user-text"; } diff --git a/examples/white-label-minimal/package.json b/examples/white-label-minimal/package.json index 26860f7..c0084e2 100644 --- a/examples/white-label-minimal/package.json +++ b/examples/white-label-minimal/package.json @@ -17,6 +17,7 @@ "radix-ui": "^1.6.7", "react": "19.2.8", "react-dom": "19.2.8", + "react-markdown": "^9.0.1", "shadcn": "^4.21.0", "tw-animate-css": "^1.4.0", "use-stick-to-bottom": "^1.1.6" diff --git a/package-lock.json b/package-lock.json index 2327015..9ae7645 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "radix-ui": "^1.6.7", "react": "19.2.8", "react-dom": "19.2.8", + "react-markdown": "^9.0.1", "shadcn": "^4.21.0", "tw-animate-css": "^1.4.0", "use-stick-to-bottom": "^1.1.6" From 47c120879cfc3220801814a61533f768eac8dbc3 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Wed, 21 Oct 2026 11:40:23 +0300 Subject: [PATCH 8/9] Tiny Sunny: the hook is useChat @base44/platform drops the Base44 prefix from its React entry (base44/javascript-sdk#345), so Chat.tsx uses useChat, UseChatResult and ChatBackend. The new names ship in 0.5.0; the dependency moves to ^0.5.0 once it is published. Co-Authored-By: Claude Opus 5.5 --- examples/white-label-minimal/components/Chat.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/white-label-minimal/components/Chat.tsx b/examples/white-label-minimal/components/Chat.tsx index 51aaf26..ca8aa46 100644 --- a/examples/white-label-minimal/components/Chat.tsx +++ b/examples/white-label-minimal/components/Chat.tsx @@ -1,6 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import { useBase44Chat, type Base44Chat, type Base44ChatBackend, type ChatItem, type ChatStep } from "@base44/platform/react"; +import { useChat, type UseChatResult, type ChatBackend, type ChatItem, type ChatStep } from "@base44/platform/react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import Markdown from "react-markdown"; @@ -9,14 +9,14 @@ import { createApp, openLiveSession, sendMessage, submitToolCallInput, type App import ToolQuestion from "./ToolQuestion"; // The four Base44 calls, run on the server. The hook calls them, so the browser never holds a key. -const backend: Base44ChatBackend = { createApp, openLiveSession, sendMessage, submitToolCallInput }; +const backend: ChatBackend = { createApp, openLiveSession, sendMessage, submitToolCallInput }; export default function Chat({ app, onAppCreated, onBuildingChange }: { app: App | null; onAppCreated: (app: App) => void; onBuildingChange: (building: boolean) => void; }) { - const chat = useBase44Chat({ appId: app?.id ?? null, backend, onAppCreated }); + const chat = useChat({ appId: app?.id ?? null, backend, onAppCreated }); const [prompt, setPrompt] = useState(""); const building = chat.status === "building"; // Send only when the AI can take a prompt now. After a failed build, a prompt can ask for the fix. @@ -99,7 +99,7 @@ function stepIcon(step: ChatStep) { } // Tiny's wording for what the hook reports. -function StatusLine({ chat, hasApp }: { chat: Base44Chat; hasApp: boolean }) { +function StatusLine({ chat, hasApp }: { chat: UseChatResult; hasApp: boolean }) { let text = ""; if (chat.error) text = chat.error.message; else if (chat.status === "submitted") text = hasApp ? "Sending…" : "Creating app…"; From 8717657f119a24cc2130a204f5cac73c6dd3458b Mon Sep 17 00:00:00 2001 From: amitbardos Date: Wed, 21 Oct 2026 13:05:28 +0300 Subject: [PATCH 9/9] Tiny Sunny: @base44/platform ^0.5.0 0.5.0 is published with the new React names, so the dependency and the lockfile move to it. Co-Authored-By: Claude Opus 5.5 --- examples/white-label-minimal/package.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/white-label-minimal/package.json b/examples/white-label-minimal/package.json index c0084e2..372ad2a 100644 --- a/examples/white-label-minimal/package.json +++ b/examples/white-label-minimal/package.json @@ -9,7 +9,7 @@ "typecheck": "next typegen && tsc --noEmit" }, "dependencies": { - "@base44/platform": "^0.4.0", + "@base44/platform": "^0.5.0", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "lucide-react": "^0.475.0", diff --git a/package-lock.json b/package-lock.json index 9ae7645..f90754b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "examples/white-label-minimal" ], "dependencies": { - "@base44/platform": "^0.4.0", + "@base44/platform": "^0.5.0", "@hello-pangea/dnd": "^18.0.1", "@prisma/client": "^6.19.3", "@radix-ui/react-checkbox": "^1.1.4", @@ -55,7 +55,7 @@ "name": "@base44/white-label-minimal", "version": "0.1.0", "dependencies": { - "@base44/platform": "^0.4.0", + "@base44/platform": "^0.5.0", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", "lucide-react": "^0.475.0", @@ -537,9 +537,9 @@ } }, "node_modules/@base44/platform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@base44/platform/-/platform-0.4.0.tgz", - "integrity": "sha512-vzlql7aM7nWFbS18uF1aXQcgyRhRDgZ3B2Varr9ogqAtwaX+j1wIMdwK2JGJj/yXPdGR8HKurGq6AOfA8BZGrA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@base44/platform/-/platform-0.5.0.tgz", + "integrity": "sha512-Mn6t1YjQFpHPtXLbl1h+c6aJQVg6n1CQQkWpQ0Ygzx7dQ+XNKZYzMjjhy0CYIDmfugRiV0xJ9foF/vVRgmBZGQ==", "license": "MIT", "dependencies": { "socket.io-client": "^4.8.3" diff --git a/package.json b/package.json index 6e3cdf3..25ceb4c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "minimal:typecheck": "npm run typecheck --workspace @base44/white-label-minimal" }, "dependencies": { - "@base44/platform": "^0.4.0", + "@base44/platform": "^0.5.0", "@hello-pangea/dnd": "^18.0.1", "@prisma/client": "^6.19.3", "@radix-ui/react-checkbox": "^1.1.4",