chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import {
|
||||
useExternalStoreRuntime,
|
||||
type ThreadMessageLike,
|
||||
type AppendMessage,
|
||||
AssistantRuntimeProvider,
|
||||
} from "@assistant-ui/react";
|
||||
import { chatStream } from "@/server/chat";
|
||||
|
||||
type MyMessage = {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
};
|
||||
|
||||
const generateId = () => Math.random().toString(36).substring(2, 9);
|
||||
|
||||
const convertMessage = (message: MyMessage): ThreadMessageLike => {
|
||||
return {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
content: [{ type: "text", text: message.content }],
|
||||
};
|
||||
};
|
||||
|
||||
export function MyRuntimeProvider({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [messages, setMessages] = useState<MyMessage[]>([]);
|
||||
|
||||
const onNew = async (message: AppendMessage) => {
|
||||
if (message.content[0]?.type !== "text")
|
||||
throw new Error("Only text messages are supported");
|
||||
|
||||
const input = message.content[0].text;
|
||||
const userMessage: MyMessage = {
|
||||
id: generateId(),
|
||||
role: "user",
|
||||
content: input,
|
||||
};
|
||||
|
||||
// Add user message
|
||||
const updatedMessages = [...messages, userMessage];
|
||||
setMessages(updatedMessages);
|
||||
|
||||
// Create placeholder for assistant message
|
||||
setIsRunning(true);
|
||||
const assistantId = generateId();
|
||||
const assistantMessage: MyMessage = {
|
||||
id: assistantId,
|
||||
role: "assistant",
|
||||
content: "",
|
||||
};
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
|
||||
try {
|
||||
// Stream response using async generator
|
||||
const stream = await chatStream({
|
||||
data: {
|
||||
messages: updatedMessages.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
// Handle streaming chunks
|
||||
for await (const chunk of stream) {
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantId ? { ...m, content: m.content + chunk } : m,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Chat error:", error);
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantId
|
||||
? { ...m, content: "Sorry, an error occurred. Please try again." }
|
||||
: m,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runtime = useExternalStoreRuntime({
|
||||
isRunning,
|
||||
messages,
|
||||
convertMessage,
|
||||
onNew,
|
||||
});
|
||||
|
||||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
{children}
|
||||
</AssistantRuntimeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createRouter } from "@tanstack/react-router";
|
||||
|
||||
// Import the generated route tree
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
// Create a new router instance
|
||||
export const getRouter = () => {
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
defaultPreloadStaleTime: 0,
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
|
||||
|
||||
import appCss from "../styles.css?url";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: "utf-8",
|
||||
},
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{
|
||||
title: "assistant-ui + TanStack Start",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
shellComponent: RootDocument,
|
||||
});
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="bg-background text-foreground">
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Thread } from "@/components/assistant-ui/thread";
|
||||
import { useAui, AuiProvider, Suggestions } from "@assistant-ui/react";
|
||||
import { MyRuntimeProvider } from "@/components/MyRuntimeProvider";
|
||||
|
||||
export const Route = createFileRoute("/")({ component: App });
|
||||
|
||||
function ThreadWithSuggestions() {
|
||||
const aui = useAui({
|
||||
suggestions: Suggestions([
|
||||
{
|
||||
title: "Hello!",
|
||||
label: "start a conversation",
|
||||
prompt: "Hello! What can you help me with?",
|
||||
},
|
||||
{
|
||||
title: "What can you do?",
|
||||
label: "tell me your capabilities",
|
||||
prompt: "What kinds of things can you help me with?",
|
||||
},
|
||||
]),
|
||||
});
|
||||
return (
|
||||
<AuiProvider value={aui}>
|
||||
<Thread />
|
||||
</AuiProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<MyRuntimeProvider>
|
||||
<main className="h-dvh">
|
||||
<ThreadWithSuggestions />
|
||||
</main>
|
||||
</MyRuntimeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import OpenAI from "openai";
|
||||
|
||||
type ChatMessage = {
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
};
|
||||
|
||||
type ChatInput = {
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
|
||||
export const chatStream = createServerFn({ method: "POST" })
|
||||
.inputValidator((data: ChatInput) => data)
|
||||
.handler(async function* ({ data }) {
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: "gpt-5.4-nano",
|
||||
messages: data.messages,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content;
|
||||
if (content) {
|
||||
yield content;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@source "../../../packages/ui/src";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground m-0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
code {
|
||||
font-family:
|
||||
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation utilities for assistant-ui */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-from-bottom-1 {
|
||||
from {
|
||||
transform: translateY(0.25rem);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-from-bottom-2 {
|
||||
from {
|
||||
transform: translateY(0.5rem);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-from-bottom-4 {
|
||||
from {
|
||||
transform: translateY(1rem);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-in-95 {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-out-95 {
|
||||
to {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-from-top-2 {
|
||||
from {
|
||||
transform: translateY(-0.5rem);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-from-left-2 {
|
||||
from {
|
||||
transform: translateX(-0.5rem);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-from-right-2 {
|
||||
from {
|
||||
transform: translateX(0.5rem);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation-duration: 150ms;
|
||||
animation-timing-function: ease-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
|
||||
.fade-in-0 {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
|
||||
.zoom-in-95 {
|
||||
animation-name: zoom-in-95;
|
||||
}
|
||||
|
||||
.slide-in-from-bottom-1 {
|
||||
animation-name: fade-in, slide-in-from-bottom-1;
|
||||
}
|
||||
|
||||
.slide-in-from-bottom-2 {
|
||||
animation-name: fade-in, slide-in-from-bottom-2;
|
||||
}
|
||||
|
||||
.slide-in-from-bottom-4 {
|
||||
animation-name: fade-in, slide-in-from-bottom-4;
|
||||
}
|
||||
|
||||
.slide-in-from-top-2 {
|
||||
animation-name: fade-in, slide-in-from-top-2;
|
||||
}
|
||||
|
||||
.slide-in-from-left-2 {
|
||||
animation-name: fade-in, slide-in-from-left-2;
|
||||
}
|
||||
|
||||
.slide-in-from-right-2 {
|
||||
animation-name: fade-in, slide-in-from-right-2;
|
||||
}
|
||||
|
||||
[data-state="closed"].animate-out {
|
||||
animation-duration: 150ms;
|
||||
animation-timing-function: ease-in;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
[data-state="closed"].fade-out-0 {
|
||||
animation-name: fade-out;
|
||||
}
|
||||
|
||||
[data-state="closed"].zoom-out-95 {
|
||||
animation-name: zoom-out-95;
|
||||
}
|
||||
|
||||
.delay-100 {
|
||||
animation-delay: 100ms;
|
||||
}
|
||||
|
||||
.fill-mode-both {
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.duration-150 {
|
||||
animation-duration: 150ms;
|
||||
}
|
||||
|
||||
.duration-300 {
|
||||
animation-duration: 300ms;
|
||||
}
|
||||
|
||||
.ease-out {
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
Reference in New Issue
Block a user