Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/white-label-minimal/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,6 @@ export default function Chat({ app, onAppCreated, onBuildingChange }: {
// 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(", ") ?? ""}`;
const files = "display" in tool && tool.display && "file_paths" in tool.display ? tool.display.file_paths : undefined;
return `${icon} ${tool.name} ${files?.join(", ") ?? ""}`;
}
20 changes: 16 additions & 4 deletions examples/white-label-minimal/components/ToolQuestion.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import type { ToolCall, ToolQuestionArguments, ToolSecretArguments } from "@base44/platform";
import type { Question, SecretField, ToolCall } from "@base44/platform";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { submitToolCallInput } from "../server/base44";
Expand All @@ -12,9 +12,12 @@ export default function ToolQuestion({ appId, messageId, tool }: { appId: string
const [picked, setPicked] = useState<Record<number, string[]>>({});
const [secrets, setSecrets] = useState<Record<string, string>>({});
const kind = tool.waiting_on?.kind;
const questions = kind === "choice" ? (tool.arguments as ToolQuestionArguments)?.questions ?? [] : [];
// ToolCall includes OtherToolCall, which has no arguments, so narrow with `in` after the name.
let questions: Question[] = [];
if (kind === "choice" && tool.name === "ask_clarifying_questions" && "arguments" in tool) questions = tool.arguments?.questions ?? [];
// Tiny passes these values to Base44 and keeps none.
const fields = kind === "input" ? (tool.arguments as ToolSecretArguments)?.secrets_schema ?? [] : [];
let fields: SecretField[] = [];
if (kind === "input" && tool.name === "set_secrets" && "arguments" in tool) fields = tool.arguments?.secrets_schema ?? [];

function answer(approve: boolean, extraUserInput?: object) {
setSent(true);
Expand All @@ -35,7 +38,7 @@ export default function ToolQuestion({ appId, messageId, tool }: { appId: string

return (
<div className="question">
{kind === "approval" && <p>Allow {tool.name}? {tool.approval?.reason ?? tool.approval?.details?.summary}</p>}
{kind === "approval" && <p>Allow {tool.name}? {approvalText(tool)}</p>}
{questions.map((q, i) => (
<div key={i} className="question-item">
<p className="question-title">{q.question}</p>
Expand All @@ -58,3 +61,12 @@ export default function ToolQuestion({ appId, messageId, tool }: { appId: string
</div>
);
}

// Why a guard paused the call. Some guards, such as bash_approval, give no reason.
function approvalText(tool: ToolCall) {
const approval = tool.approval;
if (!approval || !("reason" in approval)) return "";
if (approval.reason) return approval.reason;
if (approval.details && "summary" in approval.details) return approval.details.summary ?? "";
return "";
}
56 changes: 26 additions & 30 deletions examples/white-label-minimal/components/useLiveChat.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { Base44PlatformClient, type ChatMessage } from "@base44/platform";
import { createPlatformClient, type Message } 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,
Expand All @@ -10,7 +10,7 @@ export function useLiveChat({ app, onAppCreated }: {
onAppCreated: (app: App) => void;
}) {
const appId = app?.id ?? null;
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [building, setBuilding] = useState(false);
const [loading, setLoading] = useState(!!appId);
const [creating, setCreating] = useState(false);
Expand All @@ -20,37 +20,33 @@ export function useLiveChat({ app, onAppCreated }: {
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);
// close() rejects a pending connect; that is not an error to show.
const fail = (e: { code?: string; message?: string }) => {
if (!stopped) setError(e.code || e.message || "Live updates failed");
};
// The SDK calls getSession on connect, and again when the session expires.
const builder = createPlatformClient().builder.init({ getSession: () => openLiveSession(appId), onError: fail });
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,
});
builder.connect().catch(fail);

return () => {
stopped = true;
socket?.close();
builder.close();
};
}, [appId]);

Expand Down
2 changes: 1 addition & 1 deletion examples/white-label-minimal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"typecheck": "next typegen && tsc --noEmit"
},
"dependencies": {
"@base44/platform": "^0.1.0",
"@base44/platform": "^0.2.0",
"class-variance-authority": "^0.7.1",
"cn": "^0.4.0",
"lucide-react": "^0.475.0",
Expand Down
6 changes: 4 additions & 2 deletions examples/white-label-minimal/server/base44.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use server";
import type { SocketSession } from "@base44/platform";

export type App = { id: string; name: string };
export type ToolCallAnswer = { toolCallId: string; messageId: string; approve: boolean; extraUserInput?: object };
Expand Down Expand Up @@ -31,9 +32,10 @@ export async function listApps(): Promise<App[]> {
.map(({ id, name }) => ({ id, name }));
}

export async function openLiveSession(appId: string) {
// The browser's socket needs only these three fields; session_id stays on the server.
export async function openLiveSession(appId: string): Promise<SocketSession> {
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 };
return { session_token: session.session_token, socket_url: session.socket_url, socket_path: session.socket_path };
}

export async function sendMessage(appId: string, content: string) {
Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.