feat(open-webui): header entry, side panel, keybind and spotlight chat
Replace the bottom-right floating chat button with app-wide launchers, as requested in review: - side panel: a right-side Drawer (OpenWebUiChat) reusing the chat widget, mounted app-wide and shown only to authed users who own an Open WebUI integration - header entry: an 'AI Chat' item in the user-avatar menu (like the Docker item) - keybind: mod+shift+a toggles the panel (hotkeys.toggleAiChat) - spotlight: a '?' 'Ask AI' mode that starts a conversation from the typed query and opens the panel pre-filled; also surfaced in the Cmd+K help/mode list when the integration is present - the panel and spotlight are bridged to the app via an openWebUiChatEvent window event (mirrors the existing media-request search event) The Chat widget gains an optional initialQuery that auto-sends once on open.
This commit is contained in:
15 files changed
+272
-148
No files matched your search
@@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const FloatingChatButton = dynamic(
|
||||
() => import("~/components/board/floating-chat-button").then((mod) => mod.FloatingChatButton),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export const FloatingChatButtonProvider = () => {
|
||||
return <FloatingChatButton />;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const OpenWebUiChat = dynamic(() => import("~/components/open-webui-chat").then((mod) => mod.OpenWebUiChat), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const OpenWebUiChatProvider = () => {
|
||||
return <OpenWebUiChat />;
|
||||
};
|
||||
@@ -32,7 +32,7 @@ import { SearchEngineOptimization } from "~/components/layout/search-engine-opti
|
||||
import { ServiceWorkerRegistration } from "~/components/layout/service-worker-registration";
|
||||
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
|
||||
import { DayJsLoader } from "./_client-providers/dayjs-loader";
|
||||
import { FloatingChatButtonProvider } from "./_client-providers/floating-chat";
|
||||
import { OpenWebUiChatProvider } from "./_client-providers/open-webui-chat";
|
||||
import { JotaiProvider } from "./_client-providers/jotai";
|
||||
import { CustomMantineProvider } from "./_client-providers/mantine";
|
||||
import { AuthProvider } from "./_client-providers/session";
|
||||
@@ -148,7 +148,7 @@ export default async function Layout(props: {
|
||||
<StackedProvider>
|
||||
<Notifications pauseResetOnHover="notification" />
|
||||
<ServiceWorkerRegistration />
|
||||
<FloatingChatButtonProvider />
|
||||
<OpenWebUiChatProvider />
|
||||
{props.children}
|
||||
</StackedProvider>
|
||||
</body>
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { ActionIcon, Box, Dialog, Modal } from "@mantine/core";
|
||||
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
|
||||
import { IconRobot } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Chat } from "@homarr/widgets/_open-webui/chat";
|
||||
|
||||
export const FloatingChatButton = () => {
|
||||
const t = useI18n();
|
||||
const { data: session } = useSession();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
|
||||
const { data: integrations = [] } = clientApi.integration.all.useQuery(undefined, {
|
||||
enabled: !!session?.user?.id,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Find the first OpenWebUI integration where the current user is the creator.
|
||||
const myOpenWebUiIntegration = useMemo(() => {
|
||||
if (!session?.user?.id) return null;
|
||||
return integrations.find(
|
||||
(integration) => integration.kind === "openWebUi" && integration.creatorId === session.user.id,
|
||||
);
|
||||
}, [integrations, session?.user?.id]);
|
||||
|
||||
if (!myOpenWebUiIntegration) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!opened && (
|
||||
<ActionIcon
|
||||
size={56}
|
||||
radius="xl"
|
||||
variant="filled"
|
||||
onClick={open}
|
||||
aria-label={t("widget.openWebUi.name")}
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
zIndex: 1000,
|
||||
boxShadow: "var(--mantine-shadow-lg)",
|
||||
}}
|
||||
>
|
||||
<IconRobot size={28} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
{isMobile ? (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={t("widget.openWebUi.name")}
|
||||
fullScreen
|
||||
styles={{
|
||||
body: {
|
||||
height: "calc(100dvh - 60px)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
|
||||
<Chat
|
||||
options={{
|
||||
systemPrompt: "",
|
||||
showHistory: true,
|
||||
}}
|
||||
integrationIds={[myOpenWebUiIntegration.id]}
|
||||
itemId={undefined}
|
||||
boardId={undefined}
|
||||
isEditMode={false}
|
||||
setOptions={() => {
|
||||
// No-op: options are fixed for the floating chat.
|
||||
}}
|
||||
width={800}
|
||||
height={600}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
) : (
|
||||
<Dialog
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
withCloseButton
|
||||
size="35vw"
|
||||
position={{ bottom: 100, right: 24 }}
|
||||
transitionProps={{ transition: "pop-top-right", duration: 200 }}
|
||||
styles={{
|
||||
root: {
|
||||
height: "50dvh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: 0,
|
||||
overflow: "hidden",
|
||||
},
|
||||
closeButton: {
|
||||
zIndex: 10,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
|
||||
<Chat
|
||||
options={{
|
||||
systemPrompt: "",
|
||||
showHistory: true,
|
||||
}}
|
||||
integrationIds={[myOpenWebUiIntegration.id]}
|
||||
itemId={undefined}
|
||||
boardId={undefined}
|
||||
isEditMode={false}
|
||||
setOptions={() => {
|
||||
// No-op: options are fixed for the floating chat.
|
||||
}}
|
||||
width={800}
|
||||
height={600}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Box, Drawer } from "@mantine/core";
|
||||
import { useHotkeys, useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
import { openWebUiChatEvent } from "@homarr/spotlight";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Chat } from "@homarr/widgets/_open-webui/chat";
|
||||
|
||||
/**
|
||||
* App-wide launcher for the Open WebUI chat. Renders a right-side panel that can
|
||||
* be opened from the header user menu, the {@link hotkeys.toggleAiChat} keybind,
|
||||
* or the spotlight "ask AI" mode (via the {@link openWebUiChatEvent} window
|
||||
* event). Renders nothing unless the signed-in user owns an Open WebUI
|
||||
* integration.
|
||||
*/
|
||||
export const OpenWebUiChat = () => {
|
||||
const t = useI18n();
|
||||
const { data: session } = useSession();
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [initialQuery, setInitialQuery] = useState<string | undefined>(undefined);
|
||||
// Bumped on every open so the chat remounts and a fresh initial query is sent.
|
||||
const [openNonce, setOpenNonce] = useState(0);
|
||||
|
||||
const { data: integrations = [] } = clientApi.integration.all.useQuery(undefined, {
|
||||
enabled: Boolean(session?.user?.id),
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const myOpenWebUiIntegration = useMemo(() => {
|
||||
if (!session?.user?.id) return null;
|
||||
return integrations.find(
|
||||
(integration) => integration.kind === "openWebUi" && integration.creatorId === session.user.id,
|
||||
);
|
||||
}, [integrations, session?.user?.id]);
|
||||
|
||||
const open = useCallback((query?: string) => {
|
||||
setInitialQuery(query?.trim() ? query.trim() : undefined);
|
||||
setOpenNonce((nonce) => nonce + 1);
|
||||
setOpened(true);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setOpened((previous) => {
|
||||
if (previous) return false;
|
||||
setInitialQuery(undefined);
|
||||
setOpenNonce((nonce) => nonce + 1);
|
||||
return true;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Open (with an optional query) when the spotlight "ask AI" mode dispatches.
|
||||
useEffect(() => {
|
||||
const handler = (event: Event) => {
|
||||
if (!myOpenWebUiIntegration) return;
|
||||
open((event as CustomEvent<{ query?: string }>).detail?.query);
|
||||
};
|
||||
window.addEventListener(openWebUiChatEvent, handler);
|
||||
return () => window.removeEventListener(openWebUiChatEvent, handler);
|
||||
}, [open, myOpenWebUiIntegration]);
|
||||
|
||||
useHotkeys([[hotkeys.toggleAiChat, () => myOpenWebUiIntegration && toggle()]]);
|
||||
|
||||
if (!myOpenWebUiIntegration) return null;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
position="right"
|
||||
size={isMobile ? "100%" : 480}
|
||||
title={t("widget.openWebUi.name")}
|
||||
keepMounted={false}
|
||||
styles={{
|
||||
body: {
|
||||
height: "calc(100dvh - 60px)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
|
||||
<Chat
|
||||
key={openNonce}
|
||||
options={{ systemPrompt: "", showHistory: true }}
|
||||
integrationIds={[myOpenWebUiIntegration.id]}
|
||||
itemId={undefined}
|
||||
boardId={undefined}
|
||||
isEditMode={false}
|
||||
setOptions={() => {
|
||||
// No-op: options are fixed for the launcher chat.
|
||||
}}
|
||||
width={480}
|
||||
height={600}
|
||||
initialQuery={initialQuery}
|
||||
/>
|
||||
</Box>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -11,14 +11,17 @@ import {
|
||||
IconHome,
|
||||
IconLogin,
|
||||
IconLogout,
|
||||
IconRobot,
|
||||
IconSettings,
|
||||
IconTool,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { signOut, useSession } from "@homarr/auth/client";
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { openOpenWebUiChat } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
@@ -46,6 +49,15 @@ export const UserAvatarMenu = ({ children, availableUpdatesPromise, isDockerEnab
|
||||
const { openModal } = useModalAction(LogoutModal);
|
||||
const { openModal: openDockerModal } = useModalAction(DockerQuickAccessModal);
|
||||
|
||||
const { data: integrations = [] } = clientApi.integration.all.useQuery(undefined, {
|
||||
enabled: Boolean(session.data?.user?.id),
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const hasOpenWebUiIntegration = integrations.some(
|
||||
(integration) => integration.kind === "openWebUi" && integration.creatorId === session.data?.user?.id,
|
||||
);
|
||||
|
||||
const handleSignout = useCallback(async () => {
|
||||
await signOut({
|
||||
redirect: false,
|
||||
@@ -96,6 +108,11 @@ export const UserAvatarMenu = ({ children, availableUpdatesPromise, isDockerEnab
|
||||
{t("docker")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{hasOpenWebUiIntegration && (
|
||||
<Menu.Item leftSection={<IconRobot size="1rem" />} onClick={() => openOpenWebUiChat()}>
|
||||
{t("aiChat")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
|
||||
@@ -3,4 +3,5 @@ export const hotkeys = {
|
||||
toggleColorScheme: "mod+j",
|
||||
saveNotebook: "mod+s",
|
||||
openSpotlight: "mod+k",
|
||||
toggleAiChat: "mod+shift+a",
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { mediaRequestSearchEvent, spotlightActions } from "./spotlight-store";
|
||||
import { mediaRequestSearchEvent, openWebUiChatEvent, spotlightActions } from "./spotlight-store";
|
||||
|
||||
export { Spotlight } from "./components/spotlight";
|
||||
export { openSpotlight, openMediaRequestSearch };
|
||||
export { openSpotlight, openMediaRequestSearch, openOpenWebUiChat };
|
||||
export {
|
||||
SpotlightProvider,
|
||||
useRegisterSpotlightContextResults,
|
||||
@@ -25,4 +25,14 @@ const openMediaRequestSearch = (options: OpenMediaRequestSearchOptions = {}) =>
|
||||
spotlightActions.open();
|
||||
};
|
||||
|
||||
export { mediaRequestSearchEvent };
|
||||
export { mediaRequestSearchEvent, openWebUiChatEvent };
|
||||
|
||||
// Opens the Open WebUI chat side panel, optionally seeding it with a query to
|
||||
// start a conversation from. Closes the spotlight so the panel takes over.
|
||||
const openOpenWebUiChat = (query?: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent<{ query?: string }>(openWebUiChatEvent, { detail: { query } }));
|
||||
}
|
||||
|
||||
spotlightActions.close();
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import { IconRobot } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createGroup } from "../../lib/group";
|
||||
import { interaction } from "../../lib/interaction";
|
||||
import { openWebUiChatEvent } from "../../spotlight-store";
|
||||
|
||||
interface AskAiOption {
|
||||
key: string;
|
||||
query: string;
|
||||
}
|
||||
|
||||
// Opening the panel is decoupled from the app via a window event so the
|
||||
// spotlight package doesn't depend on the Next.js app.
|
||||
const dispatchOpenChat = (query: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent<{ query?: string }>(openWebUiChatEvent, { detail: { query } }));
|
||||
}
|
||||
};
|
||||
|
||||
// "Ask AI" — starts an Open WebUI conversation from the spotlight query.
|
||||
export const askAiGroup = createGroup<AskAiOption>({
|
||||
keyPath: "key",
|
||||
title: (t) => t("search.mode.askAi.title"),
|
||||
Component: (option) => {
|
||||
const t = useScopedI18n("search.mode.askAi.group.ask");
|
||||
return (
|
||||
<Group px="md" py="xs" w="100%" wrap="nowrap" align="center">
|
||||
<IconRobot stroke={1.5} />
|
||||
<Text>{option.query ? t("startWithQuery", { query: option.query }) : t("hint")}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.javaScript((_option, query) => ({
|
||||
onSelect() {
|
||||
dispatchOpenChat(query);
|
||||
},
|
||||
closeSpotlightOnTrigger: true,
|
||||
})),
|
||||
useQueryOptions(query) {
|
||||
const { data: session } = useSession();
|
||||
const { data: integrations = [] } = clientApi.integration.all.useQuery(undefined, {
|
||||
enabled: Boolean(session?.user?.id),
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const hasOpenWebUiIntegration = integrations.some(
|
||||
(integration) => integration.kind === "openWebUi" && integration.creatorId === session?.user?.id,
|
||||
);
|
||||
|
||||
return {
|
||||
data: hasOpenWebUiIntegration ? [{ key: "ask-ai", query }] : [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { SearchMode } from "../../lib/mode";
|
||||
import { askAiGroup } from "./ask-ai-group";
|
||||
|
||||
export const askAiMode = {
|
||||
modeKey: "askAi",
|
||||
character: "?",
|
||||
groups: [askAiGroup],
|
||||
} satisfies SearchMode;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IconSettings,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
@@ -17,6 +18,7 @@ import { createGroup } from "../../lib/group";
|
||||
import { interaction } from "../../lib/interaction";
|
||||
import type { SearchMode } from "../../lib/mode";
|
||||
import { appIntegrationBoardMode } from "../app-integration-board";
|
||||
import { askAiMode } from "../ask-ai";
|
||||
import { commandMode } from "../command";
|
||||
import { externalMode } from "../external";
|
||||
import { mediaMode } from "../media";
|
||||
@@ -40,6 +42,16 @@ export const useHomeEmptyGroups = () => {
|
||||
visibleSearchModes.unshift(userGroupMode);
|
||||
}
|
||||
|
||||
// Only surface the "Ask AI" mode when the user owns an Open WebUI integration.
|
||||
const { data: integrations = [] } = clientApi.integration.all.useQuery(undefined, {
|
||||
enabled: Boolean(session?.user?.id),
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
if (integrations.some((integration) => integration.kind === "openWebUi" && integration.creatorId === session?.user?.id)) {
|
||||
visibleSearchModes.push(askAiMode);
|
||||
}
|
||||
|
||||
return [
|
||||
createGroup({
|
||||
keyPath: "key",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { appIntegrationBoardMode } from "./app-integration-board";
|
||||
import { askAiMode } from "./ask-ai";
|
||||
import { commandMode } from "./command";
|
||||
import { externalMode } from "./external";
|
||||
import { homeMode } from "./home";
|
||||
@@ -13,5 +14,6 @@ export const searchModes = [
|
||||
commandMode,
|
||||
pageMode,
|
||||
mediaMode,
|
||||
askAiMode,
|
||||
homeMode,
|
||||
] as const;
|
||||
@@ -14,6 +14,10 @@ export interface MediaRequestSearchScope {
|
||||
export const mediaRequestSearchScopeAtom = atom<MediaRequestSearchScope>({});
|
||||
export const mediaRequestSearchEvent = "homarr:spotlight:media-request-search";
|
||||
|
||||
// Dispatched to open the Open WebUI chat side panel (optionally with a query to
|
||||
// start a conversation from). The app-level provider listens for this.
|
||||
export const openWebUiChatEvent = "homarr:spotlight:open-web-ui-chat";
|
||||
|
||||
export const setSelectedAction = (index: number, store: SpotlightStore) => {
|
||||
store.updateState((state) => ({ ...state, selected: index }));
|
||||
};
|
||||
|
||||
@@ -1489,6 +1489,7 @@
|
||||
"switchToLightMode": "Switch to light mode",
|
||||
"management": "Management",
|
||||
"docker": "Docker",
|
||||
"aiChat": "AI Chat",
|
||||
"preferences": "Your preferences",
|
||||
"logout": "Logout",
|
||||
"login": "Login",
|
||||
@@ -5787,6 +5788,16 @@
|
||||
"fetch": "An error occurred while fetching data"
|
||||
},
|
||||
"mode": {
|
||||
"askAi": {
|
||||
"help": "Start an AI chat",
|
||||
"title": "AI Chat",
|
||||
"group": {
|
||||
"ask": {
|
||||
"startWithQuery": "Ask AI: {query}",
|
||||
"hint": "Start an AI chat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appIntegrationBoard": {
|
||||
"help": "Search for apps, integrations or boards",
|
||||
"group": {
|
||||
|
||||
@@ -49,7 +49,15 @@ import { useCameraCapture } from "./use-camera-capture";
|
||||
import { useChatStream } from "./use-chat-stream";
|
||||
import { useVoiceRecording } from "./use-voice-recording";
|
||||
|
||||
export function Chat({ options, integrationIds, isEditMode }: WidgetComponentProps<"openWebUi">) {
|
||||
// `initialQuery` is only supplied when the chat is opened from outside the board
|
||||
// (spotlight / side panel "start chat with query"); the board widget renderer
|
||||
// never passes it.
|
||||
export function Chat({
|
||||
options,
|
||||
integrationIds,
|
||||
isEditMode,
|
||||
initialQuery,
|
||||
}: WidgetComponentProps<"openWebUi"> & { initialQuery?: string }) {
|
||||
const t = useI18n();
|
||||
const integrationId = integrationIds.at(0);
|
||||
|
||||
@@ -151,6 +159,19 @@ export function Chat({ options, integrationIds, isEditMode }: WidgetComponentPro
|
||||
viewportRef.current?.scrollTo({ top: viewportRef.current.scrollHeight, behavior: "smooth" });
|
||||
}, [stream.messages, stream.streamingText, composerHeight]);
|
||||
|
||||
// When opened with an initial query (spotlight / side panel "start chat with
|
||||
// query"), send it once the model has loaded. The caller remounts the chat per
|
||||
// open (via key) so a fresh query is always delivered.
|
||||
const autoSentRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (autoSentRef.current) return;
|
||||
const trimmed = initialQuery?.trim();
|
||||
if (!trimmed || !model || !integrationId || stream.isStreaming) return;
|
||||
autoSentRef.current = true;
|
||||
stream.send({ role: "user", content: trimmed });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialQuery, model, integrationId]);
|
||||
|
||||
// Nothing to send yet: no typed text and no inline image attachments.
|
||||
const composerEmpty = !input.trim() && attachments.images.length === 0;
|
||||
|
||||
|
||||
Reference in new issue
Block a user