e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
import { useCallback, useRef, useState } from "react";
|
|
import type { ThreadMessageLike, AppendMessage } from "@assistant-ui/react";
|
|
|
|
const INITIAL_MESSAGES: ThreadMessageLike[] = [];
|
|
|
|
const CANNED_RESPONSE = `Great question! Here's what I think:
|
|
|
|
This is a **mock response** from the external store runtime. In a real extension, you'd connect this to an AI backend like:
|
|
|
|
- [assistant-ui Cloud](https://www.assistant-ui.com)
|
|
- OpenAI via AI SDK
|
|
- Any custom API
|
|
|
|
The UI you're seeing is built with \`@assistant-ui/react\` -- the same components work in Next.js, Vite, React Native, and now browser extensions.`;
|
|
|
|
export function useMockStore() {
|
|
const [messages, setMessages] =
|
|
useState<readonly ThreadMessageLike[]>(INITIAL_MESSAGES);
|
|
const [isRunning, setIsRunning] = useState(false);
|
|
// Tracks the in-flight stream so a second `onNew` cancels the first and the
|
|
// late-arriving loop no longer mutates state once a newer one starts.
|
|
const activeRunRef = useRef<{ cancelled: boolean } | null>(null);
|
|
|
|
const onNew = useCallback(async (message: AppendMessage) => {
|
|
if (message.content.length !== 1 || message.content[0]?.type !== "text")
|
|
return;
|
|
|
|
if (activeRunRef.current) activeRunRef.current.cancelled = true;
|
|
const run = { cancelled: false };
|
|
activeRunRef.current = run;
|
|
|
|
const userMessage: ThreadMessageLike = {
|
|
role: "user",
|
|
content: [{ type: "text", text: message.content[0].text }],
|
|
};
|
|
|
|
setMessages((prev) => [...prev, userMessage]);
|
|
setIsRunning(true);
|
|
|
|
const words = CANNED_RESPONSE.split(" ");
|
|
let partial = "";
|
|
for (let i = 0; i < words.length; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
if (run.cancelled) return;
|
|
partial = partial ? `${partial} ${words[i]}` : (words[i] ?? "");
|
|
setMessages((prev) => {
|
|
const withoutLast =
|
|
prev.at(-1)?.role === "assistant" ? prev.slice(0, -1) : prev;
|
|
return [
|
|
...withoutLast,
|
|
{ role: "assistant", content: [{ type: "text", text: partial }] },
|
|
];
|
|
});
|
|
}
|
|
|
|
if (activeRunRef.current === run) {
|
|
activeRunRef.current = null;
|
|
setIsRunning(false);
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
messages,
|
|
setMessages,
|
|
isRunning,
|
|
onNew,
|
|
convertMessage: (m: ThreadMessageLike) => m,
|
|
};
|
|
}
|