diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 0cee0d078..9e03c2577 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -24,7 +24,7 @@ import { } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import Image from "next/image"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { agentToolsAtom, @@ -444,6 +444,52 @@ const ClipboardChip: FC<{ text: string; onDismiss: () => void }> = ({ text, onDi ); }; +/** + * Inline "this workspace can't chat yet" tray, mirrored on top of the composer + * (same visual treatment as the "Connect your tools" tray below it). + * + * Replaces the old full-screen onboarding redirect: a workspace with no usable + * chat model (fresh install, or an admin deleted the last model) renders the + * composer in place with this notice instead of navigating away. Configurable + * members get a CTA to connect one; everyone else is told to ask an admin. The + * send button stays disabled independently, and the backend rejects any send + * that slips through, so this is purely guidance. + * + * Presentational: visibility is decided by the parent so the composer can flatten + * its top shadow for a seamless join. + */ +const ChatUnavailableNotice: FC<{ workspaceId: number; canConfigure: boolean }> = ({ + workspaceId, + canConfigure, +}) => { + const router = useRouter(); + + return ( +
+
+ + + {canConfigure + ? "Connect a language model to start chatting." + : "No model available. Ask a workspace admin to connect a language model."} + +
+
+ {canConfigure ? ( + + ) : null} +
+ ); +}; + const Composer: FC = () => { const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom); const setSubmittedMentions = useSetAtom(submittedMentionsAtom); @@ -483,6 +529,9 @@ const Composer: FC = () => { const isThreadRunning = useAuiState(({ thread }) => thread.isRunning); const [connectToolsTrayVisible, setConnectToolsTrayVisible] = useState(false); + const { data: chatSetupStatus } = useAtomValue(llmSetupStatusAtomFamily(workspaceId ?? 0)); + const isChatUnavailable = !!chatSetupStatus && chatSetupStatus.status !== "ready"; + const currentPlaceholder = COMPOSER_PLACEHOLDER; const { data: currentUser } = useAtomValue(currentUserAtom); @@ -930,10 +979,17 @@ const Composer: FC = () => { ) : null}
+ {isChatUnavailable ? ( + + ) : null}
@@ -1151,10 +1207,10 @@ const ComposerAction: FC = ({ hydrateDisabled(); }, [hydrateDisabled]); - // Defense-in-depth only: the onboarding gate makes an unconfigured - // workspace unreachable, but a stale cache (e.g. another admin removed the - // last model) can briefly leave this composer mounted. Block sends silently - // until the status refetch re-engages the gate. + // A workspace with no usable chat model renders the composer with an inline + // notice (see ChatUnavailableNotice) rather than being redirected away, so + // the send button must stay disabled here. The backend also rejects any + // send that lacks a resolvable model, making this defense-in-depth. const isWorkspaceChatReady = setupStatus?.status === "ready"; const isSendDisabled = isComposerEmpty || !isWorkspaceChatReady || isBlockedByOtherUser;