feat: promote assistant configuration

This commit is contained in:
ajnart committed 2026-08-14 11:32:10 +02:00
1 parent c0445cdca2
commit cba2df72e2
13 files changed
+219 -23

No files matched your search

@@ -10,6 +10,7 @@ import {
Card,
Divider,
Group,
Image,
LoadingOverlay,
PasswordInput,
Select,
@@ -18,7 +19,6 @@ import {
Switch,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import {
@@ -26,7 +26,6 @@ import {
IconDatabaseSearch,
IconKey,
IconPlus,
IconRobot,
IconShieldLock,
IconTool,
IconTrash,
@@ -41,7 +40,16 @@ import { useScopedI18n } from "@homarr/translation/client";
type HeaderEntry = { id: number; name: string; value: string };
export const AssistantSettings = () => {
const ProviderIcon = ({ providerId, size = 20 }: { providerId: AssistantProvider; size?: number }) => {
const iconUrl = assistantProviderPresets[providerId].iconUrl;
return iconUrl ? (
<Image src={iconUrl} alt="" aria-hidden w={size} h={size} fit="contain" />
) : (
<IconWorld size={size} aria-hidden />
);
};
export const AssistantConfiguration = () => {
const t = useScopedI18n("management.page.settings.section.assistant");
const utils = clientApi.useUtils();
const { data: configuration, isLoading } = clientApi.assistant.getAdminConfiguration.useQuery();
@@ -200,12 +208,6 @@ export const AssistantSettings = () => {
return (
<Stack>
<Group gap="sm">
<IconRobot size={26} />
<Title order={2}>{t("title")}</Title>
</Group>
<Text c="dimmed">{t("description")}</Text>
<Card pos="relative" withBorder>
<LoadingOverlay visible={pending} />
<Stack gap="lg">
@@ -234,6 +236,13 @@ export const AssistantSettings = () => {
data={providerOptions}
searchable
allowDeselect={false}
leftSection={<ProviderIcon providerId={provider} />}
renderOption={({ option }) => (
<Group gap="sm" wrap="nowrap">
<ProviderIcon providerId={option.value as AssistantProvider} />
<Text size="sm">{option.label}</Text>
</Group>
)}
/>
<TextInput
label={t("baseUrl.title")}
@@ -0,0 +1,39 @@
import { notFound } from "next/navigation";
import { Stack, Text, Title } from "@mantine/core";
import { auth } from "@homarr/auth/next";
import { getScopedI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { AssistantConfiguration } from "./_components/assistant-configuration";
export async function generateMetadata() {
const t = await getScopedI18n("management.page.settings.section.assistant");
return {
title: `${t("title")} • Homarr`,
};
}
export default async function AssistantPage() {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
notFound();
}
const t = await getScopedI18n("management.page.settings.section.assistant");
return (
<>
<DynamicBreadcrumb />
<Stack>
<div>
<Title order={1}>{t("title")}</Title>
<Text c="dimmed" maw="70ch">
{t("description")}
</Text>
</div>
<AssistantConfiguration />
</Stack>
</>
);
}
@@ -21,6 +21,7 @@ import {
IconMailForward,
IconPhotoFilled,
IconPointerFilled,
IconRobot,
IconSearch,
IconSettingsFilled,
IconUserFilled,
@@ -198,6 +199,12 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
},
],
},
{
label: t("items.assistant"),
href: "/manage/assistant",
icon: IconRobot,
hidden: !session?.user.permissions.includes("admin"),
},
{
label: t("items.settings"),
href: "/manage/settings",
@@ -259,7 +259,7 @@ export const AssistantProvider = ({ children }: PropsWithChildren) => {
const t = useScopedI18n("common.assistant");
const session = useSession();
const [opened, setOpened] = useState(false);
const { data } = clientApi.assistant.getAvailability.useQuery(undefined, {
const { data, isLoading: isAvailabilityLoading } = clientApi.assistant.getAvailability.useQuery(undefined, {
enabled: session.status === "authenticated",
staleTime: 60_000,
});
@@ -268,19 +268,32 @@ export const AssistantProvider = ({ children }: PropsWithChildren) => {
const open = useCallback(() => setOpened(true), []);
const close = useCallback(() => setOpened(false), []);
const toggle = useCallback(() => setOpened((current) => !current), []);
useHotkeys([[hotkeys.openAssistant, open]]);
const openIfEnabled = useCallback(() => {
if (enabled) {
open();
}
}, [enabled, open]);
useHotkeys([[hotkeys.openAssistant, openIfEnabled]]);
const spotlightItem = useMemo(
() => ({
id: "homarr-assistant",
name: t("spotlight"),
icon: "/logo/logo.png",
interaction: () => ({ type: "javaScript" as const, onSelect: open }),
description: enabled
? t("spotlightDescription")
: session.status !== "authenticated"
? t("unavailable.signIn")
: isAvailabilityLoading
? t("unavailable.checking")
: t("unavailable.notConfigured"),
unavailable: !enabled,
interaction: () => (enabled ? { type: "javaScript" as const, onSelect: open } : { type: "none" as const }),
}),
[open, t],
[enabled, isAvailabilityLoading, open, session.status, t],
);
useRegisterSpotlightContextResults("homarr-assistant", enabled ? [spotlightItem] : [], [enabled, spotlightItem]);
useRegisterSpotlightContextActions("homarr-assistant", enabled ? [spotlightItem] : [], [enabled, spotlightItem]);
useRegisterSpotlightContextResults("homarr-assistant", [spotlightItem], [spotlightItem]);
useRegisterSpotlightContextActions("homarr-assistant", [spotlightItem], [spotlightItem]);
const value = useMemo(() => ({ enabled, opened, open, close, toggle }), [close, enabled, open, opened, toggle]);