From c863bf1b07a8d96ff343784da9d872c7b01b64b1 Mon Sep 17 00:00:00 2001 From: Adrian Stritzinger Date: Mon, 7 Jul 2025 12:24:45 +0200 Subject: [PATCH 1/4] chore: remove chat ui moved to and hosted on hub.askui.com --- README.md | 21 +- pyproject.toml | 2 - src/chat/ui/.gitignore | 33 - src/chat/ui/app/globals.css | 82 - src/chat/ui/app/layout.tsx | 31 - src/chat/ui/app/page.tsx | 33 - src/chat/ui/components.json | 20 - .../ui/components/chat/chat-container.tsx | 23 - src/chat/ui/components/chat/chat-header.tsx | 118 - src/chat/ui/components/chat/chat-input.tsx | 520 - src/chat/ui/components/chat/empty-state.tsx | 62 - .../components/chat/message-content-block.tsx | 126 - src/chat/ui/components/chat/message-item.tsx | 189 - src/chat/ui/components/chat/message-list.tsx | 157 - .../sidebar/rename-thread-dialog.tsx | 91 - src/chat/ui/components/sidebar/sidebar.tsx | 139 - .../components/sidebar/thread-item-menu.tsx | 106 - .../ui/components/sidebar/thread-list.tsx | 159 - src/chat/ui/components/ui/accordion.tsx | 58 - src/chat/ui/components/ui/alert-dialog.tsx | 141 - src/chat/ui/components/ui/alert.tsx | 59 - src/chat/ui/components/ui/aspect-ratio.tsx | 7 - src/chat/ui/components/ui/avatar.tsx | 50 - src/chat/ui/components/ui/badge.tsx | 36 - src/chat/ui/components/ui/breadcrumb.tsx | 115 - src/chat/ui/components/ui/button.tsx | 52 - src/chat/ui/components/ui/card.tsx | 86 - src/chat/ui/components/ui/carousel.tsx | 262 - src/chat/ui/components/ui/chart.tsx | 365 - src/chat/ui/components/ui/checkbox.tsx | 30 - src/chat/ui/components/ui/collapsible.tsx | 11 - src/chat/ui/components/ui/context-menu.tsx | 200 - src/chat/ui/components/ui/dialog.tsx | 122 - src/chat/ui/components/ui/dropdown-menu.tsx | 200 - src/chat/ui/components/ui/form.tsx | 179 - src/chat/ui/components/ui/hover-card.tsx | 29 - src/chat/ui/components/ui/input-otp.tsx | 71 - src/chat/ui/components/ui/input.tsx | 25 - src/chat/ui/components/ui/label.tsx | 26 - src/chat/ui/components/ui/menubar.tsx | 236 - src/chat/ui/components/ui/navigation-menu.tsx | 128 - src/chat/ui/components/ui/pagination.tsx | 117 - src/chat/ui/components/ui/popover.tsx | 31 - src/chat/ui/components/ui/progress.tsx | 28 - src/chat/ui/components/ui/radio-group.tsx | 44 - src/chat/ui/components/ui/resizable.tsx | 45 - src/chat/ui/components/ui/scroll-area.tsx | 48 - src/chat/ui/components/ui/select.tsx | 160 - src/chat/ui/components/ui/separator.tsx | 31 - src/chat/ui/components/ui/sheet.tsx | 140 - src/chat/ui/components/ui/skeleton.tsx | 15 - src/chat/ui/components/ui/slider.tsx | 28 - src/chat/ui/components/ui/sonner.tsx | 31 - src/chat/ui/components/ui/switch.tsx | 29 - src/chat/ui/components/ui/table.tsx | 117 - src/chat/ui/components/ui/tabs.tsx | 55 - src/chat/ui/components/ui/textarea.tsx | 24 - src/chat/ui/components/ui/toast.tsx | 129 - src/chat/ui/components/ui/toaster.tsx | 35 - src/chat/ui/components/ui/toggle-group.tsx | 61 - src/chat/ui/components/ui/toggle.tsx | 45 - src/chat/ui/components/ui/tooltip.tsx | 30 - src/chat/ui/hooks/use-toast.ts | 191 - src/chat/ui/lib/api.ts | 175 - src/chat/ui/lib/constants.ts | 16 - src/chat/ui/lib/store.ts | 34 - src/chat/ui/lib/types.ts | 167 - src/chat/ui/lib/utils.ts | 6 - src/chat/ui/next.config.js | 10 - src/chat/ui/package-lock.json | 8357 ----------------- src/chat/ui/package.json | 77 - src/chat/ui/postcss.config.js | 6 - src/chat/ui/tailwind.config.ts | 90 - src/chat/ui/tsconfig.json | 27 - 74 files changed, 8 insertions(+), 14791 deletions(-) delete mode 100644 src/chat/ui/.gitignore delete mode 100644 src/chat/ui/app/globals.css delete mode 100644 src/chat/ui/app/layout.tsx delete mode 100644 src/chat/ui/app/page.tsx delete mode 100644 src/chat/ui/components.json delete mode 100644 src/chat/ui/components/chat/chat-container.tsx delete mode 100644 src/chat/ui/components/chat/chat-header.tsx delete mode 100644 src/chat/ui/components/chat/chat-input.tsx delete mode 100644 src/chat/ui/components/chat/empty-state.tsx delete mode 100644 src/chat/ui/components/chat/message-content-block.tsx delete mode 100644 src/chat/ui/components/chat/message-item.tsx delete mode 100644 src/chat/ui/components/chat/message-list.tsx delete mode 100644 src/chat/ui/components/sidebar/rename-thread-dialog.tsx delete mode 100644 src/chat/ui/components/sidebar/sidebar.tsx delete mode 100644 src/chat/ui/components/sidebar/thread-item-menu.tsx delete mode 100644 src/chat/ui/components/sidebar/thread-list.tsx delete mode 100644 src/chat/ui/components/ui/accordion.tsx delete mode 100644 src/chat/ui/components/ui/alert-dialog.tsx delete mode 100644 src/chat/ui/components/ui/alert.tsx delete mode 100644 src/chat/ui/components/ui/aspect-ratio.tsx delete mode 100644 src/chat/ui/components/ui/avatar.tsx delete mode 100644 src/chat/ui/components/ui/badge.tsx delete mode 100644 src/chat/ui/components/ui/breadcrumb.tsx delete mode 100644 src/chat/ui/components/ui/button.tsx delete mode 100644 src/chat/ui/components/ui/card.tsx delete mode 100644 src/chat/ui/components/ui/carousel.tsx delete mode 100644 src/chat/ui/components/ui/chart.tsx delete mode 100644 src/chat/ui/components/ui/checkbox.tsx delete mode 100644 src/chat/ui/components/ui/collapsible.tsx delete mode 100644 src/chat/ui/components/ui/context-menu.tsx delete mode 100644 src/chat/ui/components/ui/dialog.tsx delete mode 100644 src/chat/ui/components/ui/dropdown-menu.tsx delete mode 100644 src/chat/ui/components/ui/form.tsx delete mode 100644 src/chat/ui/components/ui/hover-card.tsx delete mode 100644 src/chat/ui/components/ui/input-otp.tsx delete mode 100644 src/chat/ui/components/ui/input.tsx delete mode 100644 src/chat/ui/components/ui/label.tsx delete mode 100644 src/chat/ui/components/ui/menubar.tsx delete mode 100644 src/chat/ui/components/ui/navigation-menu.tsx delete mode 100644 src/chat/ui/components/ui/pagination.tsx delete mode 100644 src/chat/ui/components/ui/popover.tsx delete mode 100644 src/chat/ui/components/ui/progress.tsx delete mode 100644 src/chat/ui/components/ui/radio-group.tsx delete mode 100644 src/chat/ui/components/ui/resizable.tsx delete mode 100644 src/chat/ui/components/ui/scroll-area.tsx delete mode 100644 src/chat/ui/components/ui/select.tsx delete mode 100644 src/chat/ui/components/ui/separator.tsx delete mode 100644 src/chat/ui/components/ui/sheet.tsx delete mode 100644 src/chat/ui/components/ui/skeleton.tsx delete mode 100644 src/chat/ui/components/ui/slider.tsx delete mode 100644 src/chat/ui/components/ui/sonner.tsx delete mode 100644 src/chat/ui/components/ui/switch.tsx delete mode 100644 src/chat/ui/components/ui/table.tsx delete mode 100644 src/chat/ui/components/ui/tabs.tsx delete mode 100644 src/chat/ui/components/ui/textarea.tsx delete mode 100644 src/chat/ui/components/ui/toast.tsx delete mode 100644 src/chat/ui/components/ui/toaster.tsx delete mode 100644 src/chat/ui/components/ui/toggle-group.tsx delete mode 100644 src/chat/ui/components/ui/toggle.tsx delete mode 100644 src/chat/ui/components/ui/tooltip.tsx delete mode 100644 src/chat/ui/hooks/use-toast.ts delete mode 100644 src/chat/ui/lib/api.ts delete mode 100644 src/chat/ui/lib/constants.ts delete mode 100644 src/chat/ui/lib/store.ts delete mode 100644 src/chat/ui/lib/types.ts delete mode 100644 src/chat/ui/lib/utils.ts delete mode 100644 src/chat/ui/next.config.js delete mode 100644 src/chat/ui/package-lock.json delete mode 100644 src/chat/ui/package.json delete mode 100644 src/chat/ui/postcss.config.js delete mode 100644 src/chat/ui/tailwind.config.ts delete mode 100644 src/chat/ui/tsconfig.json diff --git a/README.md b/README.md index d356cd7e..bc5027a7 100644 --- a/README.md +++ b/README.md @@ -775,25 +775,28 @@ If you would like to disable the recording of usage data, set the `ASKUI__VA__TE ### AskUI Chat AskUI Chat is a web application that allows interacting with an AskUI Vision Agent similar how it can be -done with `VisionAgent.act()` but in a more interactive manner that involves less code. Aside from -telling the AskUI Vision Agent what to do, the user can also demonstrate what to do (currently, only +done with `VisionAgent.act()` or `AndroidVisionAgent.act()` but in a more interactive manner that involves less code. Aside from +telling the agent what to do, the user can also demonstrate what to do (currently, only clicking is supported). **⚠️ Warning:** AskUI Chat is currently in an experimental stage and has several limitations (see below). +#### Architecture + +This repository only includes the AskUI Chat API (`src/askui/chat`). The AskUI Chat UI can be accessed through the [AskUI Hub](https://hub.askui.com/) and connects to the local Chat API after it has been started. + #### Configuration To use the chat, configure the following environment variables: - `ASKUI_TOKEN`: AskUI Vision Agent behind chat uses currently the AskUI API - `ASKUI_WORKSPACE_ID`: AskUI Vision Agent behind chat uses currently the AskUI API -- `ASKUI__CHAT_API__DATA_DIR` (optional, defaults to `$(pwd)/chat`): Currently, the AskUI chat stores its data in a directory locally. You can change the default directory by setting this environment variable. +- `ASKUI__CHAT_API__DATA_DIR` (optional, defaults to `$(pwd)/chat`): Currently, the AskUI chat stores all data in a directory locally. You can change the default directory by setting this environment variable. #### Installation ```bash pdm install # is going to install the dependencies of the api -pdm run chat:ui:install # is going to install the dependencies of the ui ``` You may need to give permissions on the fast run of the Chat UI to demonstrate actions (aka record clicks). @@ -802,7 +805,6 @@ You may need to give permissions on the fast run of the Chat UI to demonstrate a ```bash pdm run chat:api # is going to start the api at port 8000 -pdm run chat:ui # is going to start the ui at port 3000 ``` You can use the chat to record a workflow and redo it later. For that, just tell the agent to redo all previous steps. @@ -815,7 +817,7 @@ You can use the chat to record a workflow and redo it later. For that, just tell #### Limitations - A lot of errors are not handled properly and we allow the user to do a lot of actions that can lead to errors instead of properly guiding the user. -- The chat currently only allows rerunning actions through `VisionAgent.act()` which can be expensive, slow and is not necessary the most reliable way to do it. +- The chat currently only allows rerunning actions through `VisionAgent.act()` (or `AndroidVisionAgent.act()` or `WebVisionAgent.act()`) which can be expensive, slow and is not necessary the most reliable way to do it. - A lot quirks in UI and API. - Currently, api and ui need to be run in dev mode. - When demonstrating actions, the corresponding screenshot may not reflect the correct state of the screen before the action. In this case, cancel demonstrating, delete messages and try again. @@ -824,10 +826,3 @@ You can use the chat to record a workflow and redo it later. For that, just tell - The agent is going to fail if there are no messages in the conversation, there is no tool use result message following the tool use message somewhere in the conversation, a message is too long etc. Just adding or deleting the message in this case should fix the issue. - You should not switch the conversation while waiting for an agent's answers or demonstrating actions. - - - -#### Architecture - -- The chat api/backend is a [FastAPI](https://fastapi.tiangolo.com/) application that provides a REST API similar to [OpenAI's Assistants API](https://platform.openai.com/docs/assistants/overview). -- The chat ui/frontend is a [Next.js](https://nextjs.org/) application that provides a web interface to the chat api. diff --git a/pyproject.toml b/pyproject.toml index bf9a71d6..f3e8008b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,8 +58,6 @@ lint = "ruff check src tests" typecheck = "mypy" "typecheck:all" = "mypy src tests" "chat:api" = "uvicorn chat.api.app:app --reload --port 8000" -"chat:ui:install" = {shell = "cd src/chat/ui && npm ci"} -"chat:ui" = {shell = "cd src/chat/ui && npm run dev"} "mcp:dev" = "mcp dev src/askui/mcp/__init__.py" [dependency-groups] diff --git a/src/chat/ui/.gitignore b/src/chat/ui/.gitignore deleted file mode 100644 index 5ab2f9bd..00000000 --- a/src/chat/ui/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/src/chat/ui/app/globals.css b/src/chat/ui/app/globals.css deleted file mode 100644 index 20b1c1db..00000000 --- a/src/chat/ui/app/globals.css +++ /dev/null @@ -1,82 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/src/chat/ui/app/layout.tsx b/src/chat/ui/app/layout.tsx deleted file mode 100644 index 4ec63bdc..00000000 --- a/src/chat/ui/app/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import "./globals.css"; -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import { ThemeProvider } from "next-themes"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "AskUI Chat", -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - - - {children} - - - - ); -} diff --git a/src/chat/ui/app/page.tsx b/src/chat/ui/app/page.tsx deleted file mode 100644 index 2f8a007a..00000000 --- a/src/chat/ui/app/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Toaster } from "sonner"; -import { Sidebar } from "@/components/sidebar/sidebar"; -import { ChatContainer } from "@/components/chat/chat-container"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - retry: 1, - }, - }, -}); - -function ChatApp() { - return ( -
- - -
- ); -} - -export default function Home() { - return ( - - - - - ); -} diff --git a/src/chat/ui/components.json b/src/chat/ui/components.json deleted file mode 100644 index c5974621..00000000 --- a/src/chat/ui/components.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - } -} diff --git a/src/chat/ui/components/chat/chat-container.tsx b/src/chat/ui/components/chat/chat-container.tsx deleted file mode 100644 index 221d5460..00000000 --- a/src/chat/ui/components/chat/chat-container.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import { useChatStore } from "@/lib/store"; -import { EmptyState } from "./empty-state"; -import { ChatHeader } from "./chat-header"; -import { MessageList } from "./message-list"; -import { ChatInput } from "./chat-input"; - -export function ChatContainer() { - const { selectedThread } = useChatStore(); - - if (!selectedThread) { - return ; - } - - return ( -
- - - -
- ); -} diff --git a/src/chat/ui/components/chat/chat-header.tsx b/src/chat/ui/components/chat/chat-header.tsx deleted file mode 100644 index 16da6302..00000000 --- a/src/chat/ui/components/chat/chat-header.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; - -import { Bot, Zap } from "lucide-react"; -import { useQuery } from "@tanstack/react-query"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, -} from "@/components/ui/select"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useChatStore } from "@/lib/store"; -import { apiClient } from "@/lib/api"; -import { HUMAN_DEMONSTRATION_AGENT_ID } from "@/lib/constants"; - -export function ChatHeader() { - const { selectedAssistant, setSelectedAssistant, currentRun } = - useChatStore(); - - const { data: assistantsListResponse, isLoading } = useQuery({ - queryKey: ["assistants"], - queryFn: () => - apiClient.listAssistants().then((response) => { - return { - ...response, - data: response.data.filter( - (a) => a.id !== HUMAN_DEMONSTRATION_AGENT_ID - ), - }; - }), - }); - - const handleAssistantChange = (assistantId: string) => { - const assistant = assistantsListResponse?.data.find( - (a) => a.id === assistantId - ); - if (assistant) { - setSelectedAssistant(assistant); - } - }; - - if (isLoading) { - return ( -
-
- - -
- -
- ); - } - - return ( -
-
- -
- - {currentRun && ( - - - {currentRun.status === "in_progress" - ? "Thinking..." - : currentRun.status} - - )} -
- ); -} diff --git a/src/chat/ui/components/chat/chat-input.tsx b/src/chat/ui/components/chat/chat-input.tsx deleted file mode 100644 index 32ef0ce6..00000000 --- a/src/chat/ui/components/chat/chat-input.tsx +++ /dev/null @@ -1,520 +0,0 @@ -"use client"; - -import { useState, useRef, useCallback } from "react"; -import { - Send, - Plus, - X, - Paperclip, - Square, - MousePointerClick, -} from "lucide-react"; -import { motion, AnimatePresence } from "framer-motion"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { useChatStore } from "@/lib/store"; -import { apiClient } from "@/lib/api"; -import { Event } from "@/lib/types"; -import { HUMAN_DEMONSTRATION_AGENT_ID } from "@/lib/constants"; - -interface AttachedFile { - id: string; - file: File; - preview: string; - type: "image"; -} - -let buffer = ""; - -const SseSplitterStream = (): TransformStream => - new TransformStream({ - start() {}, - transform(chunk, controller) { - buffer += chunk; - const parts = buffer.split("\n\n"); - buffer = parts.pop()!; // Keep the last partial event in buffer - - for (const part of parts) { - controller.enqueue(part); - } - }, - flush(controller) {}, - }); - -function parseSseMessage(message: string): Event { - const lines = message.split("\n"); - let type = "message"; - const dataLines: string[] = []; - - for (const line of lines) { - if (line.startsWith("event:")) { - type = line.slice(6).trim(); - } else if (line.startsWith("data:")) { - dataLines.push(line.slice(5).trim()); - } - } - - if (dataLines.length === 0) { - throw new Error("No data field in SSE message"); - } - - const rawData = dataLines.join("\n"); - - try { - switch (type) { - case "thread.run.created": - case "thread.run.queued": - case "thread.run.in_progress": - case "thread.run.completed": - case "thread.run.cancelling": - case "thread.run.cancelled": - case "thread.run.failed": - case "thread.run.expired": - return { type, data: JSON.parse(rawData) }; - case "thread.message.created": - return { type, data: JSON.parse(rawData) }; - case "error": - return { type, data: JSON.parse(rawData) }; - case "done": - return { type, data: "[DONE]" }; - default: - throw new Error(`Unknown event type: ${type}`); - } - } catch (e) { - throw new Error( - `Failed to parse SSE data of event "${type}": ${ - e instanceof Error ? e.message : String(e) - }: ${rawData}` - ); - } -} - -export function ChatInput() { - const [message, setMessage] = useState(""); - const [attachedFiles, setAttachedFiles] = useState([]); - const [isDragOver, setIsDragOver] = useState(false); - const [runningAction, setRunningAction] = useState<"send" | "demo" | null>( - null - ); - const textareaRef = useRef(null); - const fileInputRef = useRef(null); - const queryClient = useQueryClient(); - - const { - selectedThread, - selectedAssistant, - currentRun, - setCurrentRun, - appendMessage, - clearMessages, - } = useChatStore(); - - const createMessageMutation = useMutation({ - mutationFn: async (data: { content: any; role: "user" }) => { - if (!selectedThread) throw new Error("No thread selected"); - return apiClient.createMessage(selectedThread.id, data); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["messages", selectedThread?.id], - }); - }, - onError: (error) => { - toast.error(`Failed to send message: ${error}`); - }, - }); - - const createRunMutation = useMutation({ - mutationFn: async (assistantId: string) => { - if (!selectedThread || !assistantId) { - throw new Error("Thread and assistant required"); - } - - clearMessages(); - const response = await fetch( - `${ - process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000" - }/v1/threads/${selectedThread.id}/runs`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - assistant_id: assistantId, - stream: true, - }), - } - ); - - if (!response.ok) { - throw new Error(`API Error: ${response.status} ${response.statusText}`); - } - - if (!response.body) { - throw new Error("No response body"); - } - - const reader = response.body - .pipeThrough(new TextDecoderStream()) - .pipeThrough(SseSplitterStream()) - .getReader(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const event: Event = parseSseMessage(value); - switch (event.type) { - case "thread.run.created": - case "thread.run.queued": - case "thread.run.in_progress": - case "thread.run.completed": - case "thread.run.cancelling": - case "thread.run.cancelled": - case "thread.run.failed": - setCurrentRun(event.data); - break; - case "thread.run.expired": - setCurrentRun(event.data); - throw new Error("Run expired"); - case "thread.message.created": - appendMessage(event.data); - break; - case "error": - throw new Error(event.data.error.message); - case "done": - setCurrentRun(null); - break; - } - } - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["messages", selectedThread?.id], - }); - setCurrentRun(null); - setRunningAction(null); - }, - onError: (error) => { - toast.error(`Run failed: ${error.message}`); - queryClient.invalidateQueries({ - queryKey: ["messages", selectedThread?.id], - }); - setCurrentRun(null); - setRunningAction(null); - }, - }); - - const handleFileSelect = (files: FileList | null) => { - if (!files) return; - - Array.from(files).forEach((file) => { - if (file.type.startsWith("image/")) { - const reader = new FileReader(); - reader.onload = (e) => { - const newFile: AttachedFile = { - id: Math.random().toString(36).substr(2, 9), - file, - preview: e.target?.result as string, - type: "image", - }; - setAttachedFiles((prev) => [...prev, newFile]); - }; - reader.readAsDataURL(file); - } else { - toast.error("Only image files are supported"); - } - }); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!selectedThread || !selectedAssistant) { - toast.error("Please select a thread and assistant"); - return; - } - - if (message.trim() || attachedFiles.length > 0) { - const content: any[] = []; - - if (message.trim()) { - content.push({ - type: "text", - text: message.trim(), - }); - } - - attachedFiles.forEach((file) => { - const base64Data = file.preview.split(",")[1]; - content.push({ - type: "image", - source: { - type: "base64", - media_type: file.file.type, - data: base64Data, - }, - }); - }); - - await createMessageMutation.mutateAsync({ - content: - content.length === 1 && content[0].type === "text" - ? content[0].text - : content, - role: "user", - }); - - setMessage(""); - setAttachedFiles([]); - } - - if (!selectedAssistant.id) { - toast.warning( - "Select an assistant and hit the send button again if you want to receive an answer" - ); - return; - } - - setRunningAction("send"); - await createRunMutation.mutateAsync(selectedAssistant.id); - }; - - const handleCancel = () => { - if (currentRun) { - // Cancel the run - apiClient - .cancelRun(currentRun.thread_id, currentRun.id) - .then(() => { - toast.success("Send request to cancel run"); - }) - .catch(() => { - toast.error("Failed to send request to cancel run"); - }); - } - }; - - const handleDemo = async () => { - setRunningAction("demo"); - await createRunMutation.mutateAsync(HUMAN_DEMONSTRATION_AGENT_ID); - }; - - const removeFile = (fileId: string) => { - setAttachedFiles((prev) => prev.filter((f) => f.id !== fileId)); - }; - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(true); - }, []); - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(false); - }, []); - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(false); - handleFileSelect(e.dataTransfer.files); - }, []); - - const isLoading = - createMessageMutation.isPending || createRunMutation.isPending; - - return ( - -
-
- {/* File Attachments */} - - {attachedFiles.length > 0 && ( - - {attachedFiles.map((file) => ( -
- {file.file.name} - -
- ))} -
- )} -
- - {/* Input Area */} -
-