From 824abe93eba34dd89cf26e01236db5e021c44047 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sun, 18 Oct 2026 12:12:43 +0300 Subject: [PATCH] Tiny Sunny: draw the chat with Base44ChatProvider and Tiny's parts Workspace runs one above the preview and the chat. draws each item with Tiny's Text and Step parts and a part per question kind (QuestionParts.tsx), so the chat no longer switches over question kinds itself. Needs @base44/platform/react from base44/javascript-sdk#335. Co-Authored-By: Claude Opus 5.5 --- .../white-label-minimal/components/Chat.tsx | 67 ++++++--------- .../components/QuestionParts.tsx | 85 +++++++++++++++++++ .../components/ToolQuestion.tsx | 62 -------------- .../components/Workspace.tsx | 34 ++++++-- 4 files changed, 140 insertions(+), 108 deletions(-) create mode 100644 examples/white-label-minimal/components/QuestionParts.tsx delete mode 100644 examples/white-label-minimal/components/ToolQuestion.tsx diff --git a/examples/white-label-minimal/components/Chat.tsx b/examples/white-label-minimal/components/Chat.tsx index b10fdf7..5d90af5 100644 --- a/examples/white-label-minimal/components/Chat.tsx +++ b/examples/white-label-minimal/components/Chat.tsx @@ -1,35 +1,24 @@ "use client"; -import { useEffect, useState } from "react"; -import { useBase44Chat, type Base44Chat, type Base44ChatBackend, type ChatItem, type ChatStep } from "@base44/platform/react"; +import { useState } from "react"; +import { Message, useChatActions, useChatState, type ChatStep, type ChatState, type StepProps, type TextProps } from "@base44/platform/react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { StickToBottom } from "use-stick-to-bottom"; -import { createApp, openLiveSession, sendMessage, submitToolCallInput, type App } from "../server/base44"; -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 }; - -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 }); +// The chat reads the chat that Workspace's runs. draws each item with +// Tiny's parts: the text and step parts below, and the question parts from QuestionParts. +export default function Chat({ hasApp }: { hasApp: boolean }) { + const { items, status } = useChatState(); + const { send, create } = useChatActions(); 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]); + const canSend = status === "ready" || status === "error"; // The first prompt creates the app; every later one goes to it. function submit() { if (!canSend || !prompt.trim()) return; - if (app) chat.send(prompt); - else chat.create(prompt); + if (hasApp) send(prompt); + else create(prompt); setPrompt(""); } @@ -37,19 +26,12 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: {
- {chat.items.map((item) => ( + {items.map((item) => (
- {item.text &&

{item.text}

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

{stepIcon(step)} {step.label}

- ), - )} +
))} - +
@@ -61,7 +43,7 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: { e.preventDefault(); submit(); }} - placeholder={app ? "Ask for a change…" : "Describe your app…"} + placeholder={hasApp ? "Ask for a change…" : "Describe your app…"} className="chat-input" rows={1} /> @@ -72,10 +54,15 @@ 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"; - if (item.pending) return "chat-user-text chat-pending"; - return "chat-user-text"; +export function Text({ text, role, pending }: TextProps) { + let className = "chat-user-text"; + if (role === "assistant") className = "chat-assistant-text"; + else if (pending) className = "chat-user-text chat-pending"; + return

{text}

; +} + +export function Step(step: StepProps) { + return

{stepIcon(step)} {step.label}

; } function stepIcon(step: ChatStep) { @@ -86,8 +73,10 @@ function stepIcon(step: ChatStep) { return "✓"; } -// Tiny's wording for what the hook reports. -function StatusLine({ chat, hasApp }: { chat: Base44Chat; hasApp: boolean }) { +// Tiny's wording for what the chat reports. +function StatusLine({ hasApp }: { hasApp: boolean }) { + const chat: ChatState = useChatState(); + const { clearError } = useChatActions(); let text = ""; if (chat.error) text = chat.error.message; else if (chat.status === "submitted") text = hasApp ? "Sending…" : "Creating app…"; @@ -98,7 +87,7 @@ function StatusLine({ chat, hasApp }: { chat: Base44Chat; hasApp: boolean }) { return (

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

); } diff --git a/examples/white-label-minimal/components/QuestionParts.tsx b/examples/white-label-minimal/components/QuestionParts.tsx new file mode 100644 index 0000000..f22f529 --- /dev/null +++ b/examples/white-label-minimal/components/QuestionParts.tsx @@ -0,0 +1,85 @@ +"use client"; +import { useState } from "react"; +import type { ApprovalQuestion, ChoiceQuestion, QuestionComponents, SecretsQuestion } from "@base44/platform/react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +// Tiny's part for each question kind the builder can wait on. The provider picks the part, and +// each one gets its question with the actions already bound to its tool call. + +// A failed answer opens the question again, so the buttons come back. +function useSend() { + const [sent, setSent] = useState(false); + async function send(action: () => Promise) { + setSent(true); + await action(); + setSent(false); + } + return { sent, send }; +} + +function Actions({ label, onSubmit, onDecline, sent }: { label: string; onSubmit: () => void; onDecline: () => void; sent: boolean }) { + return ( +
+ + +
+ ); +} + +function Choice({ questions, answer, decline }: ChoiceQuestion) { + const { sent, send } = useSend(); + const [picked, setPicked] = useState(() => questions.map(() => [])); + + 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))); + } + + return ( +
+ {questions.map((q, i) => ( +
+

{q.text}

+
+ {q.options.map((label) => ( + + ))} +
+
+ ))} + send(() => answer(picked))} onDecline={() => send(decline)} sent={sent} /> +
+ ); +} + +// Tiny passes these values to Base44 and keeps none. +function Secrets({ fields, submit, decline }: SecretsQuestion) { + const { sent, send } = useSend(); + const [values, setValues] = useState>({}); + return ( +
+ {fields.map(({ name }) => ( + setValues({ ...values, [name]: e.target.value })} /> + ))} + send(() => submit(values))} onDecline={() => send(decline)} sent={sent} /> +
+ ); +} + +function Approval({ action, reason, approve, decline }: ApprovalQuestion) { + const { sent, send } = useSend(); + return ( +
+

Allow {action}? {reason}

+ send(approve)} onDecline={() => send(decline)} sent={sent} /> +
+ ); +} + +export const questionParts: QuestionComponents = { Choice, Secrets, Approval }; diff --git a/examples/white-label-minimal/components/ToolQuestion.tsx b/examples/white-label-minimal/components/ToolQuestion.tsx deleted file mode 100644 index 3331211..0000000 --- a/examples/white-label-minimal/components/ToolQuestion.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; -import { useState } from "react"; -import type { 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. -export default function ToolQuestion({ question }: { question: ChatQuestion }) { - const [sent, setSent] = useState(false); - const [picked, setPicked] = useState(question.kind === "choice" ? question.questions.map(() => []) : []); - // Tiny passes these values to Base44 and keeps none. - const [secrets, setSecrets] = useState>({}); - - // A failed answer opens the question again, so the buttons come back. - async function send(action: () => Promise) { - setSent(true); - await action(); - setSent(false); - } - - 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 (question.kind === "choice") return send(() => question.answer(picked)); - if (question.kind === "secrets") return send(() => question.submit(secrets)); - return send(question.approve); - } - - return ( -
- {question.kind === "approval" &&

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

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

{q.text}

-
- {q.options.map((label) => ( - - ))} -
-
- ))} - {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 62f772f..81585a7 100644 --- a/examples/white-label-minimal/components/Workspace.tsx +++ b/examples/white-label-minimal/components/Workspace.tsx @@ -1,9 +1,16 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { listApps, type App } from "../server/base44"; +import { Base44ChatProvider, useChatState, type Base44ChatBackend } from "@base44/platform/react"; +import { createApp, listApps, openLiveSession, sendMessage, submitToolCallInput, type App } from "../server/base44"; import AppPreview from "./AppPreview"; -import Chat from "./Chat"; +import Chat, { Step, Text } from "./Chat"; import Header from "./Header"; +import { questionParts } from "./QuestionParts"; + +// The four Base44 calls, run on the server. The chat calls them, so the browser never holds a key. +const backend: Base44ChatBackend = { createApp, openLiveSession, sendMessage, submitToolCallInput }; +// Tiny's parts. Every question kind needs one; the text and step parts are optional. +const components = { message: { Text, Step }, question: questionParts }; export default function Workspace() { const [apps, setApps] = useState([]); @@ -28,11 +35,24 @@ export default function Workspace() { [appId, liveAppId], ); + // One provider above everything that reads the chat. return ( -
-
- - -
+ +
+
+ + + +
+
); } + +// Tells the workspace when a build starts and ends, so the preview goes live and the app list refreshes. +function BuildWatcher({ onBuildingChange }: { onBuildingChange: (building: boolean) => void }) { + const building = useChatState().status === "building"; + useEffect(() => { + onBuildingChange(building); + }, [building, onBuildingChange]); + return null; +}