diff --git a/client/src/Agents.tsx b/client/src/Agents.tsx index 06e2c56b49..90de2fca66 100644 --- a/client/src/Agents.tsx +++ b/client/src/Agents.tsx @@ -1,23 +1,11 @@ -import { useQuery } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { useNavigate } from "react-router-dom"; +import { useGetAgentsQuery } from "@/api"; import "./App.css"; -type Agent = { - id: string; - name: string; -}; - function Agents() { const navigate = useNavigate(); - const { data: agents, isLoading } = useQuery({ - queryKey: ["agents"], - queryFn: async () => { - const res = await fetch("/api/agents"); - const data = await res.json(); - return data.agents as Agent[]; - }, - }); + const { data: agents, isLoading } = useGetAgentsQuery() return (
diff --git a/client/src/Chat.tsx b/client/src/Chat.tsx index c699692ddc..b9959f6a40 100644 --- a/client/src/Chat.tsx +++ b/client/src/Chat.tsx @@ -1,17 +1,12 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { useMutation } from "@tanstack/react-query"; +import type { TextResponse } from "@/api"; +import { useSendMessageMutation } from "@/api"; import { ImageIcon } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import "./App.css"; -type TextResponse = { - text: string; - user: string; - attachments?: { url: string; contentType: string; title: string }[]; -}; - export default function Chat() { const { agentId } = useParams(); const [input, setInput] = useState(""); @@ -19,6 +14,7 @@ export default function Chat() { const [selectedFile, setSelectedFile] = useState(null); const fileInputRef = useRef(null); const messagesEndRef = useRef(null); + const { mutate: sendMessage, isPending } = useSendMessageMutation({ setMessages, setSelectedFile }); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); @@ -28,32 +24,9 @@ export default function Chat() { scrollToBottom(); }, [messages]); - const mutation = useMutation({ - mutationFn: async (text: string) => { - const formData = new FormData(); - formData.append("text", text); - formData.append("userId", "user"); - formData.append("roomId", `default-room-${agentId}`); - - if (selectedFile) { - formData.append("file", selectedFile); - } - - const res = await fetch(`/api/${agentId}/message`, { - method: "POST", - body: formData, - }); - return res.json() as Promise; - }, - onSuccess: (data) => { - setMessages((prev) => [...prev, ...data]); - setSelectedFile(null); - }, - }); - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!input.trim() && !selectedFile) return; + if ((!input.trim() && !selectedFile) || !agentId) return; // Add user message immediately to state const userMessage: TextResponse = { @@ -63,7 +36,7 @@ export default function Chat() { }; setMessages((prev) => [...prev, userMessage]); - mutation.mutate(input); + sendMessage({ text: input, agentId, selectedFile }); setInput(""); }; @@ -142,19 +115,19 @@ export default function Chat() { onChange={(e) => setInput(e.target.value)} placeholder="Type a message..." className="flex-1" - disabled={mutation.isPending} + disabled={isPending} /> - {selectedFile && ( diff --git a/client/src/api/index.ts b/client/src/api/index.ts new file mode 100644 index 0000000000..0c2adeab02 --- /dev/null +++ b/client/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./mutations"; +export * from "./queries"; diff --git a/client/src/api/mutations/index.ts b/client/src/api/mutations/index.ts new file mode 100644 index 0000000000..ca9f0653dc --- /dev/null +++ b/client/src/api/mutations/index.ts @@ -0,0 +1 @@ +export * from "./sendMessageMutation"; diff --git a/client/src/api/mutations/sendMessageMutation.ts b/client/src/api/mutations/sendMessageMutation.ts new file mode 100644 index 0000000000..500e19d2e1 --- /dev/null +++ b/client/src/api/mutations/sendMessageMutation.ts @@ -0,0 +1,60 @@ +import type { CustomMutationResult } from "../types"; + +import { useMutation } from "@tanstack/react-query"; +import { ROUTES } from "../routes"; +import { SetStateAction } from "react"; + +export type TextResponse = { + text: string; + user: string; + attachments?: { url: string; contentType: string; title: string }[]; +}; + +type SendMessageMutationProps = { + text: string; + agentId: string; + selectedFile: File | null; +}; + +type Props = Required<{ + setMessages: (value: SetStateAction) => void; + setSelectedFile: (value: SetStateAction) => void; +}>; + +export const useSendMessageMutation = ({ + setMessages, + setSelectedFile, +}: Props): CustomMutationResult => { + const mutation = useMutation({ + mutationFn: async ({ + text, + agentId, + selectedFile, + }: SendMessageMutationProps) => { + const formData = new FormData(); + formData.append("text", text); + formData.append("userId", "user"); + formData.append("roomId", `default-room-${agentId}`); + + if (selectedFile) { + formData.append("file", selectedFile); + } + + const res = await fetch(ROUTES.sendMessage(agentId), { + method: "POST", + body: formData, + }); + + return res.json() as Promise; + }, + onSuccess: (data) => { + setMessages((prev) => [...prev, ...data]); + setSelectedFile(null); + }, + onError: (error) => { + console.error("[useSendMessageMutation]:", error); + }, + }); + + return mutation; +}; diff --git a/client/src/api/queries/index.ts b/client/src/api/queries/index.ts new file mode 100644 index 0000000000..1b1c08c1e9 --- /dev/null +++ b/client/src/api/queries/index.ts @@ -0,0 +1 @@ +export * from "./useGetAgentsQuery"; diff --git a/client/src/api/queries/queries.ts b/client/src/api/queries/queries.ts new file mode 100644 index 0000000000..40253fe29d --- /dev/null +++ b/client/src/api/queries/queries.ts @@ -0,0 +1,3 @@ +export enum Queries { + AGENTS = "agents", +} diff --git a/client/src/api/queries/useGetAgentsQuery.ts b/client/src/api/queries/useGetAgentsQuery.ts new file mode 100644 index 0000000000..88f91ff7e7 --- /dev/null +++ b/client/src/api/queries/useGetAgentsQuery.ts @@ -0,0 +1,23 @@ +import { useQuery } from "@tanstack/react-query"; +import type { CustomQueryResult } from "../types"; +import { Queries } from "./queries"; +import { ROUTES } from "../routes"; + +export type Agent = { + id: string; + name: string; +}; + +export const useGetAgentsQuery = (): CustomQueryResult => { + return useQuery({ + queryKey: [Queries.AGENTS], + queryFn: async () => { + const res = await fetch(ROUTES.getAgents()); + const data = await res.json(); + return data.agents as Agent[]; + }, + retry: (failureCount) => failureCount < 3, + staleTime: 5 * 60 * 1000, // 5 minutes + refetchOnWindowFocus: false, + }); +}; diff --git a/client/src/api/routes.ts b/client/src/api/routes.ts new file mode 100644 index 0000000000..1005a61a72 --- /dev/null +++ b/client/src/api/routes.ts @@ -0,0 +1,4 @@ +export const ROUTES = { + sendMessage: (agentId: string): string => `/api/${agentId}/message`, + getAgents: (): string => `/api/agents`, +}; diff --git a/client/src/api/types.ts b/client/src/api/types.ts new file mode 100644 index 0000000000..286daf64b5 --- /dev/null +++ b/client/src/api/types.ts @@ -0,0 +1,13 @@ +import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query"; + +export type CustomMutationResult = UseMutationResult< + TData, + Error, + TArgs, + unknown +>; + +export type CustomQueryResult = Omit< + UseQueryResult, + "data" | "refetch" | "promise" +> & { data: TData; refetch: () => void; promise: unknown }; diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index e47a067b9d..9fc8918427 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -1,4 +1,4 @@ -import { Calendar, Home, Inbox, Search, Settings } from "lucide-react"; +import { Calendar, Inbox } from "lucide-react"; import { useParams } from "react-router-dom"; import { ThemeToggle } from "@/components/theme-toggle";