diff --git a/ui/admin/app/components/ui/sonner.tsx b/ui/admin/app/components/ui/sonner.tsx new file mode 100644 index 00000000..02309ae8 --- /dev/null +++ b/ui/admin/app/components/ui/sonner.tsx @@ -0,0 +1,28 @@ +import { useTheme } from "next-themes"; +import { Toaster as Sonner } from "sonner"; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/ui/admin/app/lib/service/api/agentService.ts b/ui/admin/app/lib/service/api/agentService.ts index 0d7ba6ea..b7a5ec85 100644 --- a/ui/admin/app/lib/service/api/agentService.ts +++ b/ui/admin/app/lib/service/api/agentService.ts @@ -5,6 +5,7 @@ import { request } from "~/lib/service/api/primitives"; async function getAgents() { const res = await request<{ items: Agent[] }>({ url: ApiRoutes.agents.base().url, + errorMessage: "Failed to fetch agents", }); return res.data.items ?? ([] as Agent[]); @@ -14,6 +15,7 @@ getAgents.key = () => ({ url: ApiRoutes.agents.base().path }) as const; const getAgentById = async (agentId: string) => { const res = await request({ url: ApiRoutes.agents.getById(agentId).url, + errorMessage: "Failed to fetch agent", }); if (!res.data) return null; @@ -31,6 +33,7 @@ async function createAgent({ agent }: { agent: CreateAgent }) { url: ApiRoutes.agents.base().url, method: "POST", data: agent, + errorMessage: "Failed to create agent", }); return res.data; @@ -41,6 +44,7 @@ async function updateAgent({ id, agent }: { id: string; agent: UpdateAgent }) { url: ApiRoutes.agents.getById(id).url, method: "PUT", data: agent, + errorMessage: "Failed to update agent", }); return res.data; @@ -50,6 +54,7 @@ async function deleteAgent(id: string) { await request({ url: ApiRoutes.agents.getById(id).url, method: "DELETE", + errorMessage: "Failed to delete agent", }); } diff --git a/ui/admin/app/lib/service/api/invokeService.ts b/ui/admin/app/lib/service/api/invokeService.ts index cf9e16fb..46f09c43 100644 --- a/ui/admin/app/lib/service/api/invokeService.ts +++ b/ui/admin/app/lib/service/api/invokeService.ts @@ -16,6 +16,7 @@ async function invokeWithStream({ headers: { Accept: "text/event-stream" }, responseType: "stream", data: prompt, + errorMessage: "Failed to invoke agent", }); const reader = response.data diff --git a/ui/admin/app/lib/service/api/knowledgeService.ts b/ui/admin/app/lib/service/api/knowledgeService.ts index d5e19f68..7ba1a2c7 100644 --- a/ui/admin/app/lib/service/api/knowledgeService.ts +++ b/ui/admin/app/lib/service/api/knowledgeService.ts @@ -9,6 +9,7 @@ import { request } from "~/lib/service/api/primitives"; async function getKnowledgeForAgent(agentId: string, includeDeleted = false) { const res = await request<{ items: KnowledgeFile[] }>({ url: ApiRoutes.agents.getKnowledge(agentId).url, + errorMessage: "Failed to fetch knowledge for agent", }); if (includeDeleted) return res.data.items; @@ -28,6 +29,7 @@ async function addKnowledgeToAgent(agentId: string, file: File) { method: "POST", data: await file.arrayBuffer(), headers: { "Content-Type": "application/x-www-form-urlencoded" }, + errorMessage: "Failed to add knowledge to agent", }); } @@ -35,6 +37,7 @@ async function deleteKnowledgeFromAgent(agentId: string, fileName: string) { await request({ url: ApiRoutes.agents.deleteKnowledge(agentId, fileName).url, method: "DELETE", + errorMessage: "Failed to delete knowledge from agent", }); } @@ -42,6 +45,7 @@ async function triggerKnowledgeIngestion(agentId: string) { await request({ url: ApiRoutes.agents.triggerKnowledgeIngestion(agentId).url, method: "POST", + errorMessage: "Failed to trigger knowledge ingestion", }); } @@ -53,6 +57,7 @@ async function createRemoteKnowledgeSource( url: ApiRoutes.agents.createRemoteKnowledgeSource(agentId).url, method: "POST", data: JSON.stringify(input), + errorMessage: "Failed to create remote knowledge source", }); return res.data; } @@ -69,6 +74,7 @@ async function updateRemoteKnowledgeSource( ).url, method: "PUT", data: JSON.stringify(input), + errorMessage: "Failed to update remote knowledge source", }); } @@ -82,6 +88,7 @@ async function resyncRemoteKnowledgeSource( remoteKnowledgeSourceId ).url, method: "PATCH", + errorMessage: "Failed to resync remote knowledge source", }); } @@ -90,6 +97,7 @@ async function getRemoteKnowledgeSource(agentId: string) { items: RemoteKnowledgeSource[]; }>({ url: ApiRoutes.agents.getRemoteKnowledgeSource(agentId).url, + errorMessage: "Failed to fetch remote knowledge source", }); return res.data.items; } diff --git a/ui/admin/app/lib/service/api/primitives.ts b/ui/admin/app/lib/service/api/primitives.ts index db95fe08..a7b7a53e 100644 --- a/ui/admin/app/lib/service/api/primitives.ts +++ b/ui/admin/app/lib/service/api/primitives.ts @@ -1,5 +1,6 @@ // TODO: Add default configurations with auth tokens, etc. When ready -import axios from "axios"; +import axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from "axios"; +import { toast } from "sonner"; export const ResponseHeaders = { RunId: "x-otto-run-id", @@ -8,9 +9,36 @@ export const ResponseHeaders = { const internalFetch = axios.request; -export const request: typeof internalFetch = (config) => { - return internalFetch({ - adapter: "fetch", - ...config, - }); -}; +interface ExtendedAxiosRequestConfig + extends AxiosRequestConfig { + errorMessage?: string; +} + +export async function request, D = unknown>({ + errorMessage = "Request failed", + ...config +}: ExtendedAxiosRequestConfig): Promise { + try { + return await internalFetch({ + adapter: "fetch", + ...config, + }); + } catch (error) { + handleRequestError(error, errorMessage); + throw error; + } +} + +function handleRequestError(error: unknown, errorMessage: string): void { + if (isAxiosError(error) && error.response) { + const { status, config } = error.response; + const method = config.method?.toUpperCase() || "UNKNOWN"; + toast.error(`${status} ${method}`, { + description: errorMessage, + }); + } else { + toast.error("Request Error", { + description: errorMessage, + }); + } +} diff --git a/ui/admin/app/lib/service/api/runsService.ts b/ui/admin/app/lib/service/api/runsService.ts index d9524011..d0a10b4b 100644 --- a/ui/admin/app/lib/service/api/runsService.ts +++ b/ui/admin/app/lib/service/api/runsService.ts @@ -5,6 +5,7 @@ import { request } from "~/lib/service/api/primitives"; const getRuns = async () => { const res = await request<{ items: Run[] }>({ url: ApiRoutes.runs.base().url, + errorMessage: "Failed to fetch runs", }); return res.data.items ?? ([] as Run[]); @@ -14,6 +15,7 @@ getRuns.key = () => ({ url: ApiRoutes.runs.base().path }); const getRunDebugById = async (runId: string) => { const res = await request({ url: ApiRoutes.runs.getDebugById(runId).url, + errorMessage: "Failed to fetch run debug", }); return res.data; @@ -27,6 +29,7 @@ getRunDebugById.key = (runId?: Nullish) => { const getRunsByThread = async (threadId: string) => { const res = await request<{ items: Run[] }>({ url: ApiRoutes.runs.getByThread(threadId).url, + errorMessage: "Failed to fetch runs by thread", }); return res.data.items; diff --git a/ui/admin/app/lib/service/api/threadsService.ts b/ui/admin/app/lib/service/api/threadsService.ts index 45028adc..fd2a5b1c 100644 --- a/ui/admin/app/lib/service/api/threadsService.ts +++ b/ui/admin/app/lib/service/api/threadsService.ts @@ -8,6 +8,7 @@ import { request } from "~/lib/service/api/primitives"; const getThreads = async () => { const res = await request<{ items: Thread[] }>({ url: ApiRoutes.threads.base().url, + errorMessage: "Failed to fetch threads", }); return res.data.items ?? ([] as Thread[]); @@ -17,6 +18,7 @@ getThreads.key = () => ({ url: ApiRoutes.threads.base().path }) as const; const getThreadById = async (threadId: string) => { const res = await request({ url: ApiRoutes.threads.getById(threadId).url, + errorMessage: "Failed to fetch thread", }); return res.data; @@ -30,6 +32,7 @@ getThreadById.key = (threadId?: Nullish) => { const getThreadsByAgent = async (agentId: string) => { const res = await request<{ items: Thread[] }>({ url: ApiRoutes.threads.getByAgent(agentId).url, + errorMessage: "Failed to fetch threads by agent", }); return res.data.items ?? ([] as Thread[]); @@ -44,6 +47,7 @@ const getThreadEvents = async (threadId: string) => { const res = await request<{ items: ChatEvent[] }>({ url: ApiRoutes.threads.events(threadId).url, headers: { Accept: "application/json" }, + errorMessage: "Failed to fetch thread events", }); return res.data.items ?? ([] as ChatEvent[]); @@ -58,12 +62,14 @@ const deleteThread = async (threadId: string) => { await request({ url: ApiRoutes.threads.getById(threadId).url, method: "DELETE", + errorMessage: "Failed to delete thread", }); }; const getKnowledge = async (threadId: string) => { const res = await request<{ items: KnowledgeFile[] }>({ url: ApiRoutes.threads.getKnowledge(threadId).url, + errorMessage: "Failed to fetch knowledge for thread", }); return res.data.items ?? ([] as KnowledgeFile[]); @@ -77,6 +83,7 @@ getKnowledge.key = (threadId?: Nullish) => { const getFiles = async (threadId: string) => { const res = await request<{ items: WorkspaceFile[] }>({ url: ApiRoutes.threads.getFiles(threadId).url, + errorMessage: "Failed to fetch files", }); return res.data.items ?? ([] as WorkspaceFile[]); diff --git a/ui/admin/app/lib/service/api/toolreferenceService.ts b/ui/admin/app/lib/service/api/toolreferenceService.ts index 3b5b9772..aa35441b 100644 --- a/ui/admin/app/lib/service/api/toolreferenceService.ts +++ b/ui/admin/app/lib/service/api/toolreferenceService.ts @@ -10,6 +10,7 @@ import { request } from "~/lib/service/api/primitives"; async function getToolReferences(type?: ToolReferenceType) { const res = await request<{ items: ToolReference[] }>({ url: ApiRoutes.toolReferences.base({ type }).url, + errorMessage: "Failed to fetch tool references", }); return res.data.items ?? ([] as ToolReference[]); @@ -27,6 +28,7 @@ export type ToolCategoryMap = Record; async function getToolReferencesCategoryMap(type?: ToolReferenceType) { const res = await request<{ items: ToolReference[] }>({ url: ApiRoutes.toolReferences.base({ type }).url, + errorMessage: "Failed to fetch tool references category map", }); const toolReferences = res.data.items; @@ -58,6 +60,7 @@ getToolReferencesCategoryMap.key = (type?: ToolReferenceType) => const getToolReferenceById = async (toolReferenceId: string) => { const res = await request({ url: ApiRoutes.toolReferences.getById(toolReferenceId).url, + errorMessage: "Failed to fetch tool reference", }); return res.data; @@ -80,6 +83,7 @@ async function createToolReference({ url: ApiRoutes.toolReferences.base().url, method: "POST", data: toolReference, + errorMessage: "Failed to create tool reference", }); return res.data; @@ -96,6 +100,7 @@ async function updateToolReference({ url: ApiRoutes.toolReferences.getById(id).url, method: "PUT", data: toolReference, + errorMessage: "Failed to update tool reference", }); return res.data; @@ -105,6 +110,7 @@ async function deleteToolReference(id: string) { await request({ url: ApiRoutes.toolReferences.getById(id).url, method: "DELETE", + errorMessage: "Failed to delete tool reference", }); } diff --git a/ui/admin/app/lib/service/api/workflowService.ts b/ui/admin/app/lib/service/api/workflowService.ts index b167c690..0822b1a6 100644 --- a/ui/admin/app/lib/service/api/workflowService.ts +++ b/ui/admin/app/lib/service/api/workflowService.ts @@ -9,6 +9,7 @@ import { request } from "~/lib/service/api/primitives"; async function getWorkflows() { const res = await request<{ items: Workflow[] }>({ url: ApiRoutes.workflows.base().url, + errorMessage: "Failed to fetch workflows", }); return res.data.items ?? ([] as Workflow[]); @@ -18,6 +19,7 @@ getWorkflows.key = () => ({ url: ApiRoutes.workflows.base().path }) as const; const getWorkflowById = async (workflowId: string) => { const res = await request({ url: ApiRoutes.workflows.getById(workflowId).url, + errorMessage: "Failed to fetch workflow", }); return res.data; @@ -33,6 +35,7 @@ async function createWorkflow({ workflow }: { workflow: CreateWorkflow }) { url: ApiRoutes.workflows.base().url, method: "POST", data: workflow, + errorMessage: "Failed to create workflow", }); return res.data; @@ -49,6 +52,7 @@ async function updateWorkflow({ url: ApiRoutes.workflows.getById(id).url, method: "PUT", data: workflow, + errorMessage: "Failed to update workflow", }); return res.data; @@ -58,6 +62,7 @@ async function deleteWorkflow(id: string) { await request({ url: ApiRoutes.workflows.getById(id).url, method: "DELETE", + errorMessage: "Failed to delete workflow", }); } diff --git a/ui/admin/app/root.tsx b/ui/admin/app/root.tsx index 60d9477b..f346fabe 100644 --- a/ui/admin/app/root.tsx +++ b/ui/admin/app/root.tsx @@ -9,6 +9,7 @@ import { import { LayoutProvider } from "~/components/layout/LayoutProvider"; import { ThemeProvider } from "~/components/theme"; +import { Toaster } from "~/components/ui/sonner"; import "./tailwind.css"; @@ -39,6 +40,7 @@ export function Layout({ children }: { children: React.ReactNode }) { {children} + diff --git a/ui/admin/package-lock.json b/ui/admin/package-lock.json index f971fd55..5c8c8a5a 100644 --- a/ui/admin/package-lock.json +++ b/ui/admin/package-lock.json @@ -33,6 +33,7 @@ "date-fns": "^4.1.0", "isbot": "^4", "lucide-react": "^0.441.0", + "next-themes": "^0.3.0", "query-string": "^9.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -43,6 +44,7 @@ "rehype-external-links": "^3.0.0", "remark-gfm": "^4.0.0", "remix-routes": "^1.7.7", + "sonner": "1.4.3", "swr": "^2.2.5", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", @@ -15863,6 +15865,16 @@ "node": ">= 0.6" } }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, "node_modules/node-downloader-helper": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.9.tgz", @@ -19616,6 +19628,16 @@ "node": ">=8" } }, + "node_modules/sonner": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.3.tgz", + "integrity": "sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", diff --git a/ui/admin/package.json b/ui/admin/package.json index f4df4248..ac04bf81 100644 --- a/ui/admin/package.json +++ b/ui/admin/package.json @@ -38,6 +38,7 @@ "date-fns": "^4.1.0", "isbot": "^4", "lucide-react": "^0.441.0", + "next-themes": "^0.3.0", "query-string": "^9.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -48,6 +49,7 @@ "rehype-external-links": "^3.0.0", "remark-gfm": "^4.0.0", "remix-routes": "^1.7.7", + "sonner": "1.4.3", "swr": "^2.2.5", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7",