diff --git a/site/src/pages/AgentsPage/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/AgentCreateForm.stories.tsx index 91815f846d..c8c22b4eac 100644 --- a/site/src/pages/AgentsPage/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/AgentCreateForm.stories.tsx @@ -11,7 +11,7 @@ import { waitFor, within, } from "storybook/test"; -import { AgentCreateForm } from "./AgentsPage"; +import { AgentCreateForm } from "./AgentCreateForm"; const modelOptions = [ { diff --git a/site/src/pages/AgentsPage/AgentCreateForm.tsx b/site/src/pages/AgentsPage/AgentCreateForm.tsx new file mode 100644 index 0000000000..786ebac0a6 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentCreateForm.tsx @@ -0,0 +1,451 @@ +import { chatSystemPrompt, updateChatSystemPrompt } from "api/queries/chats"; +import { workspaces } from "api/queries/workspaces"; +import type * as TypesGen from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { ChevronDownIcon } from "components/AnimatedIcons/ChevronDown"; +import type { ModelSelectorOption } from "components/ai-elements"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "components/Combobox/Combobox"; +import { MonitorIcon } from "lucide-react"; +import { useDashboard } from "modules/dashboard/useDashboard"; +import { + type FC, + type FormEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { toast } from "sonner"; +import { AgentChatInput } from "./AgentChatInput"; +import { ConfigureAgentsDialog } from "./ConfigureAgentsDialog"; +import { + getModelCatalogStatusMessage, + getModelSelectorPlaceholder, + hasConfiguredModelsInCatalog, +} from "./modelOptions"; +import { useFileAttachments } from "./useFileAttachments"; + +/** @internal Exported for testing. */ +export const emptyInputStorageKey = "agents.empty-input"; +const selectedWorkspaceIdStorageKey = "agents.selected-workspace-id"; +const lastModelConfigIDStorageKey = "agents.last-model-config-id"; + +type ChatModelOption = ModelSelectorOption; + +export type CreateChatOptions = { + message: string; + fileIDs?: string[]; + workspaceId?: string; + model?: string; +}; + +/** + * Hook that manages draft persistence for the empty-state chat input. + * Persists the current input to localStorage so the user's draft + * survives page reloads. + * + * Once `submitDraft` is called, the stored draft is removed and further + * content changes are no longer persisted for the lifetime of the hook. + * Call `resetDraft` to re-enable persistence (e.g. on mutation failure). + * + * @internal Exported for testing. + */ +export function useEmptyStateDraft() { + const [initialInputValue] = useState(() => { + if (typeof window === "undefined") { + return ""; + } + return localStorage.getItem(emptyInputStorageKey) ?? ""; + }); + const inputValueRef = useRef(initialInputValue); + const sentRef = useRef(false); + + const handleContentChange = useCallback((content: string) => { + inputValueRef.current = content; + if (typeof window !== "undefined" && !sentRef.current) { + if (content) { + localStorage.setItem(emptyInputStorageKey, content); + } else { + localStorage.removeItem(emptyInputStorageKey); + } + } + }, []); + + const submitDraft = useCallback(() => { + // Mark as sent so that editor change events firing during + // the async gap cannot re-persist the draft. + sentRef.current = true; + localStorage.removeItem(emptyInputStorageKey); + }, []); + + const resetDraft = useCallback(() => { + sentRef.current = false; + }, []); + + const getCurrentContent = useCallback(() => inputValueRef.current, []); + + return { + initialInputValue, + getCurrentContent, + handleContentChange, + submitDraft, + resetDraft, + }; +} + +interface AgentCreateFormProps { + onCreateChat: (options: CreateChatOptions) => Promise; + isCreating: boolean; + createError: unknown; + modelCatalog: TypesGen.ChatModelsResponse | null | undefined; + modelOptions: readonly ChatModelOption[]; + isModelCatalogLoading: boolean; + modelConfigs: readonly TypesGen.ChatModelConfig[]; + isModelConfigsLoading: boolean; + modelCatalogError: unknown; + canSetSystemPrompt: boolean; + canManageChatModelConfigs: boolean; + isConfigureAgentsDialogOpen: boolean; + onConfigureAgentsDialogOpenChange: (open: boolean) => void; +} + +export const AgentCreateForm: FC = ({ + onCreateChat, + isCreating, + createError, + modelCatalog, + modelOptions, + modelConfigs, + isModelCatalogLoading, + isModelConfigsLoading, + modelCatalogError, + canSetSystemPrompt, + canManageChatModelConfigs, + isConfigureAgentsDialogOpen, + onConfigureAgentsDialogOpenChange, +}) => { + const { organizations } = useDashboard(); + const queryClient = useQueryClient(); + const { initialInputValue, handleContentChange, submitDraft, resetDraft } = + useEmptyStateDraft(); + const systemPromptQuery = useQuery(chatSystemPrompt()); + const { + mutate: saveSystemPrompt, + isPending: isSavingSystemPrompt, + isError: isSaveSystemPromptError, + } = useMutation(updateChatSystemPrompt(queryClient)); + const [initialLastModelConfigID] = useState(() => { + if (typeof window === "undefined") { + return ""; + } + return localStorage.getItem(lastModelConfigIDStorageKey) ?? ""; + }); + const modelIDByConfigID = useMemo(() => { + const optionIDByRef = new Map(); + for (const option of modelOptions) { + const provider = option.provider.trim().toLowerCase(); + const model = option.model.trim(); + if (!provider || !model) { + continue; + } + const key = `${provider}:${model}`; + if (!optionIDByRef.has(key)) { + optionIDByRef.set(key, option.id); + } + } + + const byConfigID = new Map(); + for (const config of modelConfigs) { + const provider = config.provider.trim().toLowerCase(); + const model = config.model.trim(); + if (!provider || !model) { + continue; + } + const modelID = optionIDByRef.get(`${provider}:${model}`); + if (!modelID || byConfigID.has(config.id)) { + continue; + } + byConfigID.set(config.id, modelID); + } + return byConfigID; + }, [modelConfigs, modelOptions]); + const lastUsedModelID = useMemo(() => { + if (!initialLastModelConfigID) { + return ""; + } + return modelIDByConfigID.get(initialLastModelConfigID) ?? ""; + }, [initialLastModelConfigID, modelIDByConfigID]); + const defaultModelID = useMemo(() => { + const defaultModelConfig = modelConfigs.find((config) => config.is_default); + if (!defaultModelConfig) { + return ""; + } + return modelIDByConfigID.get(defaultModelConfig.id) ?? ""; + }, [modelConfigs, modelIDByConfigID]); + const preferredModelID = + lastUsedModelID || defaultModelID || (modelOptions[0]?.id ?? ""); + const [userSelectedModel, setUserSelectedModel] = useState(""); + const [hasUserSelectedModel, setHasUserSelectedModel] = useState(false); + // Derive the effective model every render so we never reference + // a stale model id and can honor fallback precedence. + const selectedModel = + hasUserSelectedModel && + modelOptions.some((modelOption) => modelOption.id === userSelectedModel) + ? userSelectedModel + : preferredModelID; + const serverPrompt = systemPromptQuery.data?.system_prompt ?? ""; + const [localEdit, setLocalEdit] = useState(null); + const systemPromptDraft = localEdit ?? serverPrompt; + const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 0 })); + const [selectedWorkspaceId, setSelectedWorkspaceId] = useState( + () => { + if (typeof window === "undefined") return null; + return localStorage.getItem(selectedWorkspaceIdStorageKey) || null; + }, + ); + const workspaceOptions = workspacesQuery.data?.workspaces ?? []; + const autoCreateWorkspaceValue = "__auto_create_workspace__"; + const hasAdminControls = canSetSystemPrompt || canManageChatModelConfigs; + const hasModelOptions = modelOptions.length > 0; + const hasConfiguredModels = hasConfiguredModelsInCatalog(modelCatalog); + const modelSelectorPlaceholder = getModelSelectorPlaceholder( + modelOptions, + isModelCatalogLoading, + hasConfiguredModels, + ); + const modelCatalogStatusMessage = getModelCatalogStatusMessage( + modelCatalog, + modelOptions, + isModelCatalogLoading, + Boolean(modelCatalogError), + ); + const inputStatusText = hasModelOptions + ? null + : hasConfiguredModels + ? "Models are configured but unavailable. Ask an admin." + : "No models configured. Ask an admin."; + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + if (!initialLastModelConfigID) { + return; + } + if (isModelCatalogLoading || isModelConfigsLoading) { + return; + } + if (lastUsedModelID) { + return; + } + localStorage.removeItem(lastModelConfigIDStorageKey); + }, [ + initialLastModelConfigID, + isModelCatalogLoading, + isModelConfigsLoading, + lastUsedModelID, + ]); + + // Keep a mutable ref to selectedWorkspaceId and selectedModel so + // that the onSend callback always sees the latest values without + // the shared input component re-rendering on every change. + const selectedWorkspaceIdRef = useRef(selectedWorkspaceId); + selectedWorkspaceIdRef.current = selectedWorkspaceId; + const selectedModelRef = useRef(selectedModel); + selectedModelRef.current = selectedModel; + const isSystemPromptDirty = localEdit !== null && localEdit !== serverPrompt; + + const handleWorkspaceChange = (value: string) => { + if (value === autoCreateWorkspaceValue) { + setSelectedWorkspaceId(null); + if (typeof window !== "undefined") { + localStorage.removeItem(selectedWorkspaceIdStorageKey); + } + return; + } + setSelectedWorkspaceId(value); + if (typeof window !== "undefined") { + localStorage.setItem(selectedWorkspaceIdStorageKey, value); + } + }; + + const handleModelChange = useCallback((value: string) => { + setHasUserSelectedModel(true); + setUserSelectedModel(value); + }, []); + + const handleSaveSystemPrompt = useCallback( + (event: FormEvent) => { + event.preventDefault(); + if (!isSystemPromptDirty) { + return; + } + saveSystemPrompt( + { system_prompt: systemPromptDraft }, + { onSuccess: () => setLocalEdit(null) }, + ); + }, + [isSystemPromptDirty, systemPromptDraft, saveSystemPrompt], + ); + + const handleSend = useCallback( + async (message: string, fileIDs?: string[]) => { + submitDraft(); + await onCreateChat({ + message, + fileIDs, + workspaceId: selectedWorkspaceIdRef.current ?? undefined, + model: selectedModelRef.current || undefined, + }).catch(() => { + // Re-enable draft persistence so the user can edit + // and retry after a failed send attempt. + resetDraft(); + }); + }, + [submitDraft, resetDraft, onCreateChat], + ); + + const selectedWorkspace = selectedWorkspaceId + ? workspaceOptions.find((ws) => ws.id === selectedWorkspaceId) + : undefined; + const selectedWorkspaceLabel = selectedWorkspace + ? `${selectedWorkspace.owner_name}/${selectedWorkspace.name}` + : undefined; + + const { + attachments, + uploadStates, + previewUrls, + handleAttach, + handleRemoveAttachment, + resetAttachments, + } = useFileAttachments(organizations[0]?.id); + + const handleSendWithAttachments = useCallback( + async (message: string) => { + const fileIds: string[] = []; + let skippedErrors = 0; + for (const file of attachments) { + const state = uploadStates.get(file); + if (state?.status === "error") { + skippedErrors++; + continue; + } + if (state?.status === "uploaded" && state.fileId) { + fileIds.push(state.fileId); + } + } + if (skippedErrors > 0) { + toast.warning( + `${skippedErrors} attachment${skippedErrors > 1 ? "s" : ""} could not be sent (upload failed)`, + ); + } + try { + await handleSend(message, fileIds.length > 0 ? fileIds : undefined); + resetAttachments(); + } catch { + // Attachments preserved for retry on failure. + } + }, + [attachments, handleSend, resetAttachments, uploadStates], + ); + + return ( +
+
+ {createError ? : null} + {workspacesQuery.isError && ( + + )} + + + handleWorkspaceChange(value ?? autoCreateWorkspaceValue) + } + > + + + + + + + + Auto-create Workspace + + {workspaceOptions.map((workspace) => ( + + {workspace.owner_name}/{workspace.name} + + ))} + + No workspaces found + + + } + /> +
+ + {hasAdminControls && ( + + )} +
+ ); +}; diff --git a/site/src/pages/AgentsPage/AgentsPage.test.ts b/site/src/pages/AgentsPage/AgentsPage.test.ts index f9b35aaecd..1759a12f32 100644 --- a/site/src/pages/AgentsPage/AgentsPage.test.ts +++ b/site/src/pages/AgentsPage/AgentsPage.test.ts @@ -1,6 +1,6 @@ import { act, renderHook } from "@testing-library/react"; import { beforeEach, describe, expect, it } from "vitest"; -import { emptyInputStorageKey, useEmptyStateDraft } from "./AgentsPage"; +import { emptyInputStorageKey, useEmptyStateDraft } from "./AgentCreateForm"; describe("useEmptyStateDraft", () => { beforeEach(() => { diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index a8cdd0e499..8d95259068 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -7,35 +7,18 @@ import { chatKey, chatModelConfigs, chatModels, - chatSystemPrompt, chatsKey, createChat, infiniteChats, readInfiniteChatsCache, unarchiveChat, - updateChatSystemPrompt, updateInfiniteChatsCache, } from "api/queries/chats"; -import { workspaces } from "api/queries/workspaces"; import type * as TypesGen from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { ChevronDownIcon } from "components/AnimatedIcons/ChevronDown"; -import type { ModelSelectorOption } from "components/ai-elements"; -import { - Combobox, - ComboboxContent, - ComboboxEmpty, - ComboboxInput, - ComboboxItem, - ComboboxList, - ComboboxTrigger, -} from "components/Combobox/Combobox"; import { useAuthenticated } from "hooks"; -import { MonitorIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, - type FormEvent, useCallback, useEffect, useMemo, @@ -51,36 +34,20 @@ import { import { useNavigate, useParams } from "react-router"; import { toast } from "sonner"; import { createReconnectingWebSocket } from "utils/reconnectingWebSocket"; -import { AgentChatInput } from "./AgentChatInput"; +import { + type CreateChatOptions, + emptyInputStorageKey, +} from "./AgentCreateForm"; import { maybePlayChime } from "./AgentDetail/useAgentChime"; import type { AgentsOutletContext } from "./AgentsPageView"; import { AgentsPageView } from "./AgentsPageView"; -import { ConfigureAgentsDialog } from "./ConfigureAgentsDialog"; -import { - getModelCatalogStatusMessage, - getModelOptionsFromCatalog, - getModelSelectorPlaceholder, - hasConfiguredModelsInCatalog, -} from "./modelOptions"; +import { getModelOptionsFromCatalog } from "./modelOptions"; import { useAgentsPageKeybindings } from "./useAgentsPageKeybindings"; import { useAgentsPWA } from "./useAgentsPWA"; -import { useFileAttachments } from "./useFileAttachments"; -/** @internal Exported for testing. */ -export const emptyInputStorageKey = "agents.empty-input"; -const selectedWorkspaceIdStorageKey = "agents.selected-workspace-id"; const lastModelConfigIDStorageKey = "agents.last-model-config-id"; const nilUUID = "00000000-0000-0000-0000-000000000000"; -type ChatModelOption = ModelSelectorOption; - -export type CreateChatOptions = { - message: string; - fileIDs?: string[]; - workspaceId?: string; - model?: string; -}; - // Type guard for SSE events from the chat list watch endpoint. function isChatListSSEEvent( data: unknown, @@ -521,405 +488,4 @@ const AgentsPage: FC = () => { ); }; -/** - * Hook that manages draft persistence for the empty-state chat input. - * Persists the current input to localStorage so the user's draft - * survives page reloads. - * - * Once `submitDraft` is called, the stored draft is removed and further - * content changes are no longer persisted for the lifetime of the hook. - * Call `resetDraft` to re-enable persistence (e.g. on mutation failure). - * - * @internal Exported for testing. - */ -export function useEmptyStateDraft() { - const [initialInputValue] = useState(() => { - if (typeof window === "undefined") { - return ""; - } - return localStorage.getItem(emptyInputStorageKey) ?? ""; - }); - const inputValueRef = useRef(initialInputValue); - const sentRef = useRef(false); - - const handleContentChange = useCallback((content: string) => { - inputValueRef.current = content; - if (typeof window !== "undefined" && !sentRef.current) { - if (content) { - localStorage.setItem(emptyInputStorageKey, content); - } else { - localStorage.removeItem(emptyInputStorageKey); - } - } - }, []); - - const submitDraft = useCallback(() => { - // Mark as sent so that editor change events firing during - // the async gap cannot re-persist the draft. - sentRef.current = true; - localStorage.removeItem(emptyInputStorageKey); - }, []); - - const resetDraft = useCallback(() => { - sentRef.current = false; - }, []); - - const getCurrentContent = useCallback(() => inputValueRef.current, []); - - return { - initialInputValue, - getCurrentContent, - handleContentChange, - submitDraft, - resetDraft, - }; -} - -interface AgentCreateFormProps { - onCreateChat: (options: CreateChatOptions) => Promise; - isCreating: boolean; - createError: unknown; - modelCatalog: TypesGen.ChatModelsResponse | null | undefined; - modelOptions: readonly ChatModelOption[]; - isModelCatalogLoading: boolean; - modelConfigs: readonly TypesGen.ChatModelConfig[]; - isModelConfigsLoading: boolean; - modelCatalogError: unknown; - canSetSystemPrompt: boolean; - canManageChatModelConfigs: boolean; - isConfigureAgentsDialogOpen: boolean; - onConfigureAgentsDialogOpenChange: (open: boolean) => void; -} - -export const AgentCreateForm: FC = ({ - onCreateChat, - isCreating, - createError, - modelCatalog, - modelOptions, - modelConfigs, - isModelCatalogLoading, - isModelConfigsLoading, - modelCatalogError, - canSetSystemPrompt, - canManageChatModelConfigs, - isConfigureAgentsDialogOpen, - onConfigureAgentsDialogOpenChange, -}) => { - const { organizations } = useDashboard(); - const queryClient = useQueryClient(); - const { initialInputValue, handleContentChange, submitDraft, resetDraft } = - useEmptyStateDraft(); - const systemPromptQuery = useQuery(chatSystemPrompt()); - const { - mutate: saveSystemPrompt, - isPending: isSavingSystemPrompt, - isError: isSaveSystemPromptError, - } = useMutation(updateChatSystemPrompt(queryClient)); - const [initialLastModelConfigID] = useState(() => { - if (typeof window === "undefined") { - return ""; - } - return localStorage.getItem(lastModelConfigIDStorageKey) ?? ""; - }); - const modelIDByConfigID = useMemo(() => { - const optionIDByRef = new Map(); - for (const option of modelOptions) { - const provider = option.provider.trim().toLowerCase(); - const model = option.model.trim(); - if (!provider || !model) { - continue; - } - const key = `${provider}:${model}`; - if (!optionIDByRef.has(key)) { - optionIDByRef.set(key, option.id); - } - } - - const byConfigID = new Map(); - for (const config of modelConfigs) { - const provider = config.provider.trim().toLowerCase(); - const model = config.model.trim(); - if (!provider || !model) { - continue; - } - const modelID = optionIDByRef.get(`${provider}:${model}`); - if (!modelID || byConfigID.has(config.id)) { - continue; - } - byConfigID.set(config.id, modelID); - } - return byConfigID; - }, [modelConfigs, modelOptions]); - const lastUsedModelID = useMemo(() => { - if (!initialLastModelConfigID) { - return ""; - } - return modelIDByConfigID.get(initialLastModelConfigID) ?? ""; - }, [initialLastModelConfigID, modelIDByConfigID]); - const defaultModelID = useMemo(() => { - const defaultModelConfig = modelConfigs.find((config) => config.is_default); - if (!defaultModelConfig) { - return ""; - } - return modelIDByConfigID.get(defaultModelConfig.id) ?? ""; - }, [modelConfigs, modelIDByConfigID]); - const preferredModelID = - lastUsedModelID || defaultModelID || (modelOptions[0]?.id ?? ""); - const [userSelectedModel, setUserSelectedModel] = useState(""); - const [hasUserSelectedModel, setHasUserSelectedModel] = useState(false); - // Derive the effective model every render so we never reference - // a stale model id and can honor fallback precedence. - const selectedModel = - hasUserSelectedModel && - modelOptions.some((modelOption) => modelOption.id === userSelectedModel) - ? userSelectedModel - : preferredModelID; - const serverPrompt = systemPromptQuery.data?.system_prompt ?? ""; - const [localEdit, setLocalEdit] = useState(null); - const systemPromptDraft = localEdit ?? serverPrompt; - const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 0 })); - const [selectedWorkspaceId, setSelectedWorkspaceId] = useState( - () => { - if (typeof window === "undefined") return null; - return localStorage.getItem(selectedWorkspaceIdStorageKey) || null; - }, - ); - const workspaceOptions = workspacesQuery.data?.workspaces ?? []; - const autoCreateWorkspaceValue = "__auto_create_workspace__"; - const hasAdminControls = canSetSystemPrompt || canManageChatModelConfigs; - const hasModelOptions = modelOptions.length > 0; - const hasConfiguredModels = hasConfiguredModelsInCatalog(modelCatalog); - const modelSelectorPlaceholder = getModelSelectorPlaceholder( - modelOptions, - isModelCatalogLoading, - hasConfiguredModels, - ); - const modelCatalogStatusMessage = getModelCatalogStatusMessage( - modelCatalog, - modelOptions, - isModelCatalogLoading, - Boolean(modelCatalogError), - ); - const inputStatusText = hasModelOptions - ? null - : hasConfiguredModels - ? "Models are configured but unavailable. Ask an admin." - : "No models configured. Ask an admin."; - - useEffect(() => { - if (typeof window === "undefined") { - return; - } - if (!initialLastModelConfigID) { - return; - } - if (isModelCatalogLoading || isModelConfigsLoading) { - return; - } - if (lastUsedModelID) { - return; - } - localStorage.removeItem(lastModelConfigIDStorageKey); - }, [ - initialLastModelConfigID, - isModelCatalogLoading, - isModelConfigsLoading, - lastUsedModelID, - ]); - - // Keep a mutable ref to selectedWorkspaceId and selectedModel so - // that the onSend callback always sees the latest values without - // the shared input component re-rendering on every change. - const selectedWorkspaceIdRef = useRef(selectedWorkspaceId); - selectedWorkspaceIdRef.current = selectedWorkspaceId; - const selectedModelRef = useRef(selectedModel); - selectedModelRef.current = selectedModel; - const isSystemPromptDirty = localEdit !== null && localEdit !== serverPrompt; - - const handleWorkspaceChange = (value: string) => { - if (value === autoCreateWorkspaceValue) { - setSelectedWorkspaceId(null); - if (typeof window !== "undefined") { - localStorage.removeItem(selectedWorkspaceIdStorageKey); - } - return; - } - setSelectedWorkspaceId(value); - if (typeof window !== "undefined") { - localStorage.setItem(selectedWorkspaceIdStorageKey, value); - } - }; - - const handleModelChange = useCallback((value: string) => { - setHasUserSelectedModel(true); - setUserSelectedModel(value); - }, []); - - const handleSaveSystemPrompt = useCallback( - (event: FormEvent) => { - event.preventDefault(); - if (!isSystemPromptDirty) { - return; - } - saveSystemPrompt( - { system_prompt: systemPromptDraft }, - { onSuccess: () => setLocalEdit(null) }, - ); - }, - [isSystemPromptDirty, systemPromptDraft, saveSystemPrompt], - ); - - const handleSend = useCallback( - async (message: string, fileIDs?: string[]) => { - submitDraft(); - await onCreateChat({ - message, - fileIDs, - workspaceId: selectedWorkspaceIdRef.current ?? undefined, - model: selectedModelRef.current || undefined, - }).catch(() => { - // Re-enable draft persistence so the user can edit - // and retry after a failed send attempt. - resetDraft(); - }); - }, - [submitDraft, resetDraft, onCreateChat], - ); - - const selectedWorkspace = selectedWorkspaceId - ? workspaceOptions.find((ws) => ws.id === selectedWorkspaceId) - : undefined; - const selectedWorkspaceLabel = selectedWorkspace - ? `${selectedWorkspace.owner_name}/${selectedWorkspace.name}` - : undefined; - - const { - attachments, - uploadStates, - previewUrls, - handleAttach, - handleRemoveAttachment, - resetAttachments, - } = useFileAttachments(organizations[0]?.id); - - const handleSendWithAttachments = useCallback( - async (message: string) => { - const fileIds: string[] = []; - let skippedErrors = 0; - for (const file of attachments) { - const state = uploadStates.get(file); - if (state?.status === "error") { - skippedErrors++; - continue; - } - if (state?.status === "uploaded" && state.fileId) { - fileIds.push(state.fileId); - } - } - if (skippedErrors > 0) { - toast.warning( - `${skippedErrors} attachment${skippedErrors > 1 ? "s" : ""} could not be sent (upload failed)`, - ); - } - try { - await handleSend(message, fileIds.length > 0 ? fileIds : undefined); - resetAttachments(); - } catch { - // Attachments preserved for retry on failure. - } - }, - [attachments, handleSend, resetAttachments, uploadStates], - ); - - return ( -
-
- {createError ? : null} - {workspacesQuery.isError && ( - - )} - - - handleWorkspaceChange(value ?? autoCreateWorkspaceValue) - } - > - - - - - - - - Auto-create Workspace - - {workspaceOptions.map((workspace) => ( - - {workspace.owner_name}/{workspace.name} - - ))} - - No workspaces found - - - } - /> -
- - {hasAdminControls && ( - - )} -
- ); -}; - export default AgentsPage; diff --git a/site/src/pages/AgentsPage/AgentsPageView.tsx b/site/src/pages/AgentsPage/AgentsPageView.tsx index 3b9db9e763..6510c400b7 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.tsx @@ -8,7 +8,7 @@ import { type FC, useState } from "react"; import { NavLink, Outlet } from "react-router"; import { cn } from "utils/cn"; import { pageTitle } from "utils/page"; -import { AgentCreateForm, type CreateChatOptions } from "./AgentsPage"; +import { AgentCreateForm, type CreateChatOptions } from "./AgentCreateForm"; import { AgentsSidebar } from "./AgentsSidebar"; import { ChimeButton } from "./ChimeButton"; import { WebPushButton } from "./WebPushButton";