e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
268 lines
7.9 KiB
TypeScript
268 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
type ReactNode,
|
|
} from "react";
|
|
import {
|
|
AssistantRuntimeProvider,
|
|
ThreadPrimitive,
|
|
ComposerPrimitive,
|
|
MessagePrimitive,
|
|
ErrorPrimitive,
|
|
AuiIf,
|
|
useAuiState,
|
|
Tools,
|
|
Suggestions,
|
|
} from "@assistant-ui/react";
|
|
import {
|
|
useChatRuntime,
|
|
AssistantChatTransport,
|
|
} from "@assistant-ui/react-ai-sdk";
|
|
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
import { SendHorizontal, SquareIcon } from "lucide-react";
|
|
import {
|
|
createPlaygroundChatToolkit,
|
|
type PartialBuilderConfig,
|
|
} from "@/lib/playground-chat-toolkit";
|
|
import { useAui } from "@assistant-ui/store";
|
|
import type { BuilderConfig } from "./types";
|
|
import { applyDiff } from "@/lib/playground-url-state";
|
|
|
|
const PLAYGROUND_SUGGESTIONS = [
|
|
{
|
|
title: "Make it look like",
|
|
label: "ChatGPT",
|
|
prompt: "Make it look like ChatGPT",
|
|
},
|
|
{
|
|
title: "Switch to",
|
|
label: "dark mode",
|
|
prompt: "Switch to dark mode with blue accents",
|
|
},
|
|
{
|
|
title: "Enable all",
|
|
label: "features",
|
|
prompt: "Enable all features like attachments, avatars, and feedback",
|
|
},
|
|
];
|
|
|
|
// --- Context (shares runtime between mobile sheet + desktop sidebar) ---
|
|
|
|
type PlaygroundChatContextValue = {
|
|
runtime: ReturnType<typeof useChatRuntime>;
|
|
aui: ReturnType<typeof useAui>;
|
|
};
|
|
|
|
const PlaygroundChatContext = createContext<PlaygroundChatContextValue | null>(
|
|
null,
|
|
);
|
|
|
|
function usePlaygroundChat() {
|
|
const ctx = useContext(PlaygroundChatContext);
|
|
if (!ctx)
|
|
throw new Error(
|
|
"usePlaygroundChat must be used within PlaygroundChatProvider",
|
|
);
|
|
return ctx;
|
|
}
|
|
|
|
// --- Provider (plain context, no AssistantRuntimeProvider) ---
|
|
|
|
interface PlaygroundChatProviderProps {
|
|
config: BuilderConfig;
|
|
setConfig: (config: BuilderConfig) => void;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function PlaygroundChatProvider({
|
|
config,
|
|
setConfig,
|
|
children,
|
|
}: PlaygroundChatProviderProps) {
|
|
const configRef = useRef(config);
|
|
configRef.current = config;
|
|
|
|
const onConfigUpdate = useCallback(
|
|
(update: PartialBuilderConfig) => {
|
|
const { customCSS, ...rest } = update;
|
|
const merged = applyDiff(
|
|
rest as Record<string, unknown>,
|
|
configRef.current,
|
|
);
|
|
if (customCSS !== undefined) {
|
|
merged.customCSS = customCSS
|
|
? [configRef.current.customCSS, customCSS].filter(Boolean).join("\n")
|
|
: "";
|
|
}
|
|
setConfig(merged);
|
|
},
|
|
[setConfig],
|
|
);
|
|
|
|
const toolkit = useMemo(
|
|
() => createPlaygroundChatToolkit(onConfigUpdate),
|
|
[onConfigUpdate],
|
|
);
|
|
|
|
const transport = useMemo(
|
|
() =>
|
|
new AssistantChatTransport({
|
|
api: "/api/playground-chat",
|
|
prepareSendMessagesRequest: async (options) => ({
|
|
body: {
|
|
...options.body,
|
|
messages: options.messages,
|
|
builderConfig: configRef.current,
|
|
},
|
|
}),
|
|
}),
|
|
[],
|
|
);
|
|
|
|
const runtime = useChatRuntime({
|
|
transport,
|
|
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
});
|
|
|
|
const aui = useAui(
|
|
{
|
|
tools: Tools({ toolkit }),
|
|
suggestions: Suggestions(PLAYGROUND_SUGGESTIONS),
|
|
},
|
|
{ parent: null },
|
|
);
|
|
|
|
const value = useMemo(() => ({ runtime, aui }), [runtime, aui]);
|
|
|
|
return (
|
|
<PlaygroundChatContext.Provider value={value}>
|
|
{children}
|
|
</PlaygroundChatContext.Provider>
|
|
);
|
|
}
|
|
|
|
// --- Thread UI (each mount creates its own AssistantRuntimeProvider scope) ---
|
|
|
|
export function PlaygroundChatThread({
|
|
onRunningChange,
|
|
}: {
|
|
onRunningChange?: (isRunning: boolean) => void;
|
|
}) {
|
|
const { runtime, aui } = usePlaygroundChat();
|
|
|
|
return (
|
|
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
{onRunningChange && <RunningObserver onRunningChange={onRunningChange} />}
|
|
<ThreadPrimitive.Root className="flex flex-1 flex-col overflow-hidden">
|
|
<ThreadPrimitive.Viewport className="flex flex-1 scrollbar-none flex-col gap-3 overflow-y-auto px-3 pt-3">
|
|
<ThreadPrimitive.Empty>
|
|
<div className="flex flex-1 flex-col items-center justify-center gap-4 py-8 text-center">
|
|
<div>
|
|
<p className="text-sm font-medium">
|
|
Describe how you want your chat to look
|
|
</p>
|
|
<p className="text-muted-foreground text-xs">
|
|
e.g. "make it look like ChatGPT" or "use dark
|
|
mode with rounded corners"
|
|
</p>
|
|
</div>
|
|
<div className="flex w-full max-w-56 flex-col gap-2">
|
|
{PLAYGROUND_SUGGESTIONS.map((s) => (
|
|
<ThreadPrimitive.Suggestion
|
|
key={s.prompt}
|
|
prompt={s.prompt}
|
|
send
|
|
className="hover:bg-muted rounded-lg border px-3 py-2 text-left text-xs transition-colors"
|
|
>
|
|
<span className="font-medium">{s.title}</span>{" "}
|
|
<span className="text-muted-foreground">{s.label}</span>
|
|
</ThreadPrimitive.Suggestion>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</ThreadPrimitive.Empty>
|
|
|
|
<ThreadPrimitive.Messages
|
|
components={{ UserMessage, AssistantMessage }}
|
|
/>
|
|
|
|
<ThreadPrimitive.ViewportFooter className="bg-background sticky bottom-0 mt-auto">
|
|
<Composer />
|
|
</ThreadPrimitive.ViewportFooter>
|
|
</ThreadPrimitive.Viewport>
|
|
</ThreadPrimitive.Root>
|
|
</AssistantRuntimeProvider>
|
|
);
|
|
}
|
|
|
|
/** Bridges runtime state to the parent; resets on unmount. */
|
|
function RunningObserver({
|
|
onRunningChange,
|
|
}: {
|
|
onRunningChange: (isRunning: boolean) => void;
|
|
}) {
|
|
const isRunning = useAuiState((s) => s.thread.isRunning);
|
|
useEffect(() => {
|
|
onRunningChange(isRunning);
|
|
return () => onRunningChange(false);
|
|
}, [isRunning, onRunningChange]);
|
|
return null;
|
|
}
|
|
|
|
function Composer() {
|
|
return (
|
|
<ComposerPrimitive.Root className="py-2">
|
|
<div className="border-border bg-background focus-within:border-foreground/60 rounded-lg border transition-colors">
|
|
<ComposerPrimitive.Input asChild>
|
|
<textarea
|
|
placeholder="Describe a change..."
|
|
className="placeholder:text-muted-foreground field-sizing-content max-h-32 w-full resize-none bg-transparent px-3 pt-2.5 pb-2 text-sm leading-5 focus:outline-none"
|
|
rows={1}
|
|
/>
|
|
</ComposerPrimitive.Input>
|
|
<div className="flex items-center justify-end px-1.5 pb-1.5">
|
|
<AuiIf condition={(s) => !s.thread.isRunning}>
|
|
<ComposerPrimitive.Send className="text-muted-foreground hover:text-foreground rounded-md p-1 transition-colors disabled:opacity-30">
|
|
<SendHorizontal className="size-4" />
|
|
</ComposerPrimitive.Send>
|
|
</AuiIf>
|
|
<AuiIf condition={(s) => s.thread.isRunning}>
|
|
<ComposerPrimitive.Cancel className="text-muted-foreground hover:text-foreground rounded-md p-1 transition-colors">
|
|
<SquareIcon className="size-3.5 fill-current" />
|
|
</ComposerPrimitive.Cancel>
|
|
</AuiIf>
|
|
</div>
|
|
</div>
|
|
</ComposerPrimitive.Root>
|
|
);
|
|
}
|
|
|
|
function UserMessage() {
|
|
return (
|
|
<MessagePrimitive.Root className="flex justify-end">
|
|
<div className="bg-foreground/5 max-w-[85%] rounded-lg px-3 py-2 text-sm">
|
|
<MessagePrimitive.Content />
|
|
</div>
|
|
</MessagePrimitive.Root>
|
|
);
|
|
}
|
|
|
|
function AssistantMessage() {
|
|
return (
|
|
<MessagePrimitive.Root className="px-3 py-2 text-sm">
|
|
<MessagePrimitive.Content />
|
|
<MessagePrimitive.Error>
|
|
<ErrorPrimitive.Root className="border-destructive bg-destructive/10 text-destructive dark:bg-destructive/5 mt-2 rounded-md border p-2 text-xs dark:text-red-200">
|
|
<ErrorPrimitive.Message className="line-clamp-2" />
|
|
</ErrorPrimitive.Root>
|
|
</MessagePrimitive.Error>
|
|
</MessagePrimitive.Root>
|
|
);
|
|
}
|