feat(onboarding): polish setup experience
This commit is contained in:
1 parent
f4891447d3
commit
4ca02a9133
34 files changed
+1005
-1172
No files matched your search
@@ -21,6 +21,7 @@ import { IconX } from "@tabler/icons-react";
|
||||
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { BoardColorInput } from "@homarr/ui";
|
||||
|
||||
import { SectionCard } from "~/components/manage/section-card";
|
||||
import { generateColors } from "../../(content)/_theme";
|
||||
@@ -44,21 +45,11 @@ export const ColorSettingsContent = ({ form }: Props) => {
|
||||
<Grid>
|
||||
<Grid.Col span={{ sm: 12, md: 6 }}>
|
||||
<Stack gap="xs">
|
||||
<ColorInput
|
||||
label={t("board.field.primaryColor.label")}
|
||||
format="hex"
|
||||
swatches={Object.values(theme.colors).map((color) => color[6])}
|
||||
{...form.getInputProps("primaryColor")}
|
||||
/>
|
||||
<BoardColorInput label={t("board.field.primaryColor.label")} {...form.getInputProps("primaryColor")} />
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ sm: 12, md: 6 }}>
|
||||
<ColorInput
|
||||
label={t("board.field.secondaryColor.label")}
|
||||
format="hex"
|
||||
swatches={Object.values(theme.colors).map((color) => color[6])}
|
||||
{...form.getInputProps("secondaryColor")}
|
||||
/>
|
||||
<BoardColorInput label={t("board.field.secondaryColor.label")} {...form.getInputProps("secondaryColor")} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Anchor onClick={toggle}>{showPreview ? t("common.preview.hide") : t("common.preview.show")}</Anchor>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { resolveHomarrUrlConfig } from "@homarr/workshop/schema";
|
||||
|
||||
import { DatabaseRestoreFlow } from "~/components/backup";
|
||||
import { env } from "~/env";
|
||||
import { AssistantConfiguration } from "../manage/assistant/_components/assistant-configuration";
|
||||
|
||||
export default async function InitPage() {
|
||||
const [state, session, requestHeaders, firstUser] = await Promise.all([
|
||||
@@ -68,6 +69,7 @@ export default async function InitPage() {
|
||||
availableBoards,
|
||||
}}
|
||||
sqliteRestore={databaseDriver === "sqlite" ? <DatabaseRestoreFlow variant="standalone" /> : undefined}
|
||||
assistantConfiguration={canConfigurePrivileged ? <AssistantConfiguration /> : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import "@gfazioli/mantine-onboarding-tour/styles.css";
|
||||
import "@homarr/notifications/styles.css";
|
||||
import "@homarr/spotlight/styles.css";
|
||||
import "@homarr/ui/styles.css";
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
import "mantine-datatable/styles.css";
|
||||
import "~/styles/color-scheme.scss";
|
||||
import "~/styles/scroll-area.scss";
|
||||
|
||||
+9
-4
@@ -142,8 +142,8 @@ export const AssistantConfiguration = () => {
|
||||
const t = useScopedI18n("management.page.settings.section.assistant");
|
||||
const utils = clientApi.useUtils();
|
||||
const { data: configuration, isLoading } = clientApi.assistant.getAdminConfiguration.useQuery();
|
||||
const [provider, setProvider] = useState<AssistantProvider>("openrouter");
|
||||
const [baseUrl, setBaseUrl] = useState<string>(assistantProviderPresets.openrouter.baseUrl);
|
||||
const [provider, setProvider] = useState<AssistantProvider>("homarr");
|
||||
const [baseUrl, setBaseUrl] = useState<string>(assistantProviderPresets.homarr.baseUrl);
|
||||
const [modelDiscoveryPath, setModelDiscoveryPath] = useState<string>("/models");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [credentialFlow, setCredentialFlow] = useState<CredentialFlow>("idle");
|
||||
@@ -155,7 +155,7 @@ export const AssistantConfiguration = () => {
|
||||
const [webSearchEnabled, setWebSearchEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const configuredProvider = configuration?.provider ?? "openrouter";
|
||||
const configuredProvider = configuration?.provider ?? "homarr";
|
||||
setProvider(configuredProvider);
|
||||
setBaseUrl(
|
||||
configuredProvider === "homarr"
|
||||
@@ -201,7 +201,7 @@ export const AssistantConfiguration = () => {
|
||||
);
|
||||
const providerOptions = useMemo(
|
||||
() =>
|
||||
(["hosted", "local", "custom"] satisfies AssistantProviderCategory[]).map((category) => {
|
||||
(["free", "hosted", "local", "custom"] satisfies AssistantProviderCategory[]).map((category) => {
|
||||
const items = assistantProviderIds
|
||||
.filter((providerId) => assistantProviderPresets[providerId].category === category)
|
||||
.map((providerId) => ({
|
||||
@@ -438,6 +438,11 @@ export const AssistantConfiguration = () => {
|
||||
{option.label}
|
||||
</Text>
|
||||
</Group>
|
||||
{option.value === "homarr" && (
|
||||
<Text size="xs" c="dimmed" lineClamp={2}>
|
||||
{t("provider.options.homarr.description")}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -1,94 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Combobox, Group, InputBase, Text, useCombobox } from "@mantine/core";
|
||||
import { IconCheck, IconDeviceDesktop, IconMoon, IconSun } from "@tabler/icons-react";
|
||||
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
import { colorSchemes } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
const schemeIcons: Record<ColorScheme, React.ElementType> = {
|
||||
auto: IconDeviceDesktop,
|
||||
light: IconSun,
|
||||
dark: IconMoon,
|
||||
};
|
||||
|
||||
interface ColorSchemeComboboxProps {
|
||||
value: ColorScheme;
|
||||
onChange: (value: ColorScheme) => void;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
export const ColorSchemeCombobox = ({ value, onChange, width }: ColorSchemeComboboxProps) => {
|
||||
const tOptions = useScopedI18n("common.colorScheme.options");
|
||||
|
||||
const combobox = useCombobox({
|
||||
onDropdownClose: () => combobox.resetSelectedOption(),
|
||||
});
|
||||
|
||||
const handleOnOptionSubmit = React.useCallback(
|
||||
(val: string) => {
|
||||
if (!val) return;
|
||||
onChange(val as ColorScheme);
|
||||
combobox.closeDropdown();
|
||||
},
|
||||
[onChange, combobox],
|
||||
);
|
||||
|
||||
const handleOnClick = React.useCallback(() => {
|
||||
combobox.toggleDropdown();
|
||||
}, [combobox]);
|
||||
|
||||
return (
|
||||
<Combobox store={combobox} onOptionSubmit={handleOnOptionSubmit}>
|
||||
<Combobox.Target>
|
||||
<InputBase
|
||||
component="button"
|
||||
type="button"
|
||||
pointer
|
||||
rightSection={<Combobox.Chevron />}
|
||||
rightSectionPointerEvents="none"
|
||||
onClick={handleOnClick}
|
||||
variant="filled"
|
||||
w={width}
|
||||
>
|
||||
<OptionItem scheme={value} currentScheme={value} tOptions={tOptions} />
|
||||
</InputBase>
|
||||
</Combobox.Target>
|
||||
<Combobox.Dropdown>
|
||||
<Combobox.Options>
|
||||
{colorSchemes.map((scheme) => (
|
||||
<Combobox.Option value={scheme} key={scheme}>
|
||||
<OptionItem scheme={scheme} currentScheme={value} tOptions={tOptions} showCheck />
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox.Dropdown>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
const OptionItem = ({
|
||||
scheme,
|
||||
currentScheme,
|
||||
tOptions,
|
||||
showCheck,
|
||||
}: {
|
||||
scheme: ColorScheme;
|
||||
currentScheme: ColorScheme;
|
||||
tOptions: (key: ColorScheme) => string;
|
||||
showCheck?: boolean;
|
||||
}) => {
|
||||
const Icon = schemeIcons[scheme];
|
||||
|
||||
return (
|
||||
<Group wrap="nowrap" justify="space-between">
|
||||
<Group wrap="nowrap">
|
||||
<Icon size={16} stroke={1.5} />
|
||||
<Text>{tOptions(scheme)}</Text>
|
||||
</Group>
|
||||
{showCheck && scheme === currentScheme && <IconCheck color="currentColor" size={16} />}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
export { ColorSchemeCombobox } from "@homarr/ui";
|
||||
@@ -1,105 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Combobox, Group, InputBase, Loader, ScrollArea, Text, useCombobox } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
|
||||
import { LanguageIcon } from "@homarr/ui";
|
||||
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
|
||||
interface LanguageComboboxProps {
|
||||
label?: string;
|
||||
value: SupportedLanguage;
|
||||
onChange: (value: SupportedLanguage) => void;
|
||||
isPending?: boolean;
|
||||
width?: string;
|
||||
withinPortal?: boolean;
|
||||
}
|
||||
|
||||
export const LanguageCombobox = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
isPending,
|
||||
width,
|
||||
withinPortal = false,
|
||||
}: LanguageComboboxProps) => {
|
||||
const combobox = useCombobox({
|
||||
onDropdownClose: () => combobox.resetSelectedOption(),
|
||||
});
|
||||
|
||||
const handleOnOptionSubmit = React.useCallback(
|
||||
(value: string) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
onChange(value as SupportedLanguage);
|
||||
combobox.closeDropdown();
|
||||
},
|
||||
[onChange, combobox],
|
||||
);
|
||||
|
||||
const handleOnClick = React.useCallback(() => {
|
||||
combobox.toggleDropdown();
|
||||
}, [combobox]);
|
||||
|
||||
return (
|
||||
<Combobox store={combobox} withinPortal={withinPortal} onOptionSubmit={handleOnOptionSubmit}>
|
||||
<Combobox.Target>
|
||||
<InputBase
|
||||
component="button"
|
||||
type="button"
|
||||
pointer
|
||||
label={label}
|
||||
leftSection={isPending ? <Loader size={16} /> : null}
|
||||
rightSection={<Combobox.Chevron />}
|
||||
rightSectionPointerEvents="none"
|
||||
onClick={handleOnClick}
|
||||
variant="filled"
|
||||
w={width}
|
||||
>
|
||||
<OptionItem currentLocale={value} localeKey={value} />
|
||||
</InputBase>
|
||||
</Combobox.Target>
|
||||
<Combobox.Dropdown>
|
||||
<ScrollArea h={300}>
|
||||
<Combobox.Options>
|
||||
{supportedLanguages.map((languageKey) => (
|
||||
<Combobox.Option value={languageKey} key={languageKey}>
|
||||
<OptionItem currentLocale={value} localeKey={languageKey} showCheck />
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</ScrollArea>
|
||||
</Combobox.Dropdown>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
const OptionItem = ({
|
||||
currentLocale,
|
||||
localeKey,
|
||||
showCheck,
|
||||
}: {
|
||||
currentLocale: SupportedLanguage;
|
||||
localeKey: SupportedLanguage;
|
||||
showCheck?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Group wrap="nowrap" justify="space-between">
|
||||
<Group wrap="nowrap">
|
||||
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Text>{localeConfigurations[localeKey].name}</Text>
|
||||
<Text size="xs" c="dimmed" inherit>
|
||||
({localeConfigurations[localeKey].translatedName})
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
{showCheck && localeKey === currentLocale && <IconCheck color="currentColor" size={16} />}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
export { LanguageCombobox } from "@homarr/ui";
|
||||
Reference in new issue
Block a user