fix: refine assistant controls

This commit is contained in:
ajnart committed 2026-08-14 11:32:11 +02:00
1 parent ef9c37d5ec
commit ebd63bea74
6 files changed
+56 -35

No files matched your search

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { getAssistantConnectionState } from "./assistant-configuration-state";
import { formatAssistantContextWindow, getAssistantConnectionState } from "./assistant-configuration-state";
describe("getAssistantConnectionState", () => {
it("marks a saved required-key provider as ready", () => {
@@ -63,3 +63,10 @@ describe("getAssistantConnectionState", () => {
});
});
});
describe("formatAssistantContextWindow", () => {
it("uses Homarr's compact number format for large context windows", () => {
expect(formatAssistantContextWindow(1_048_576)).toBe("1M");
expect(formatAssistantContextWindow(131_072)).toBe("131k");
});
});
@@ -1,3 +1,5 @@
import { formatNumber } from "@homarr/common";
interface AssistantConnectionStateInput {
connectionConfigured: boolean;
destinationChanged: boolean;
@@ -21,3 +23,5 @@ export const getAssistantConnectionState = ({
connectionReady: !connectionPending,
};
};
export const formatAssistantContextWindow = (tokens: number) => formatNumber(tokens, 0);
@@ -57,7 +57,7 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica
import { useScopedI18n } from "@homarr/translation/client";
import classes from "./assistant-configuration.module.css";
import { getAssistantConnectionState } from "./assistant-configuration-state";
import { formatAssistantContextWindow, getAssistantConnectionState } from "./assistant-configuration-state";
type HeaderEntry = { id: number; name: string; value: string };
type CredentialFlow = "idle" | "replace" | "remove";
@@ -438,9 +438,11 @@ export const AssistantConfiguration = () => {
{t("overview.keyProtected")}
</Badge>
)}
<Badge variant="filled" color={configuration?.enabled ? "green" : "gray"}>
{configuration?.enabled ? t("overview.available") : t("overview.disabled")}
</Badge>
{configuration?.enabled === false && (
<Badge variant="filled" color="gray">
{t("overview.disabled")}
</Badge>
)}
</Group>
</Group>
</Paper>
@@ -473,7 +475,7 @@ export const AssistantConfiguration = () => {
allowDeselect={false}
leftSection={<ProviderIcon providerId={provider} />}
renderOption={({ option }) => (
<Group gap="sm" wrap="nowrap" align={option.value === "homarr" ? "flex-start" : "center"} w="100%">
<Group gap="sm" wrap="nowrap" align="center" w="100%">
<ProviderIcon providerId={option.value as AssistantProviderOption} />
<Box flex={1} miw={0}>
<Group gap="xs" wrap="nowrap" justify="space-between">
@@ -486,11 +488,6 @@ export const AssistantConfiguration = () => {
</Badge>
)}
</Group>
{option.value === "homarr" && (
<Text size="xs" c="dimmed" lineClamp={2} maw="46ch">
{t("provider.options.homarr.description")}
</Text>
)}
</Box>
</Group>
)}
@@ -518,13 +515,11 @@ export const AssistantConfiguration = () => {
title={t("credentials.title")}
description={t("credentials.description")}
status={
<Badge variant="light" color={hasStoredApiKey ? "green" : preset.requiresApiKey ? "yellow" : "gray"}>
{hasStoredApiKey
? t("apiKey.configuredBadge")
: preset.requiresApiKey
? t("credentials.required")
: t("credentials.optional")}
</Badge>
hasStoredApiKey ? undefined : (
<Badge variant="light" color={preset.requiresApiKey ? "yellow" : "gray"}>
{preset.requiresApiKey ? t("credentials.required") : t("credentials.optional")}
</Badge>
)
}
actions={
<Group justify="space-between">
@@ -781,14 +776,14 @@ export const AssistantConfiguration = () => {
title={t("model.title")}
description={t("model.description")}
status={
configuration?.modelId ? (
<Badge variant="light" color={configuration.enabled ? "green" : "gray"}>
{configuration.enabled ? t("overview.available") : t("overview.disabled")}
</Badge>
) : (
!configuration?.modelId ? (
<Badge variant="light" color="gray">
{t("overview.noModel")}
</Badge>
) : configuration.enabled ? undefined : (
<Badge variant="light" color="gray">
{t("overview.disabled")}
</Badge>
)
}
actions={
@@ -867,7 +862,9 @@ export const AssistantConfiguration = () => {
</Box>
<Group className={classes.selectedModelBadges} gap="xs" justify="flex-end">
{selectedModel.contextLength && (
<Badge variant="light">{t("model.context", { count: selectedModel.contextLength })}</Badge>
<Badge variant="light">
{t("model.context", { count: formatAssistantContextWindow(selectedModel.contextLength) })}
</Badge>
)}
<Badge
variant="light"
@@ -1157,6 +1157,13 @@
background: var(--mantine-color-default-hover);
}
.composerContext[data-compact] {
width: 1.75rem;
justify-content: center;
padding: 0;
border-radius: 50%;
}
.composerContext:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
@@ -1223,6 +1230,10 @@
background: var(--mantine-color-body);
}
.composer:has(.composerInput:focus-within) {
box-shadow: inset 0 0 0 2px var(--mantine-color-red-filled);
}
.composerDropzone[data-dragging] .composer {
box-shadow: inset 0 0 0 2px var(--mantine-primary-color-filled);
}
@@ -1527,7 +1527,7 @@ const ConversationContext = () => {
? Math.min(100, Math.round(((usage.contextUsed ?? 0) / Math.max(usage.contextLength ?? 1, 1)) * 100))
: 0;
const quickLabel = [
hasTokenUsage ? `${usage.totalTokens.toLocaleString()} ${t("usage.tokens")}` : t("usage.notReported"),
hasTokenUsage ? `${usage.totalTokens.toLocaleString()} ${t("usage.tokens")}` : undefined,
hasCost ? formatCost(usage.cost) : undefined,
]
.filter(Boolean)
@@ -1548,9 +1548,10 @@ const ConversationContext = () => {
<UnstyledButton
className={classes.composerContext}
type="button"
aria-label={`${t("usage.contextWindow")}: ${quickLabel}`}
aria-label={quickLabel ? `${t("usage.contextWindow")}: ${quickLabel}` : t("usage.contextWindow")}
aria-expanded={opened}
aria-haspopup="dialog"
data-compact={!quickLabel || undefined}
onClick={() => setOpened((value) => !value)}
>
<RingProgress
@@ -1559,9 +1560,11 @@ const ConversationContext = () => {
roundCaps
sections={hasContext ? [{ value: contextPercentage, color: getContextColor(contextPercentage) }] : []}
/>
<Text size="xs" fw={650} truncate>
{quickLabel}
</Text>
{quickLabel && (
<Text size="xs" fw={650} truncate>
{quickLabel}
</Text>
)}
</UnstyledButton>
</Popover.Target>
<Popover.Dropdown className={classes.conversationContextPopover}>