fix: address assistant review findings
This commit is contained in:
1 parent
87f264a286
commit
b988e431e8
8 files changed
+71
-180
No files matched your search
@@ -159,6 +159,7 @@ import { getAssistantIconSearchQuery } from "./assistant-tool-label";
|
||||
import { getAssistantProviderQuotaLevel, isAssistantProviderUnavailable } from "./assistant-provider-quota";
|
||||
import { getAssistantToolTraceTarget } from "./assistant-tool-trace";
|
||||
import { getSafeAssistantHttpUrl } from "./assistant-url";
|
||||
import { isEscapeOwnedByNestedOverlay } from "../board/advanced-focus/escape";
|
||||
|
||||
// Shiki's web grammar bundle is intentionally loaded only when a settled response contains a code
|
||||
// block. Most assistant conversations never need it, so it should not inflate the global dashboard
|
||||
@@ -407,7 +408,27 @@ type MarkdownSpanProps = ComponentPropsWithoutRef<"span"> & {
|
||||
"data-directive-type"?: string;
|
||||
};
|
||||
|
||||
const AssistantDirectiveEntitiesContext = createContext<AssistantDirectiveEntity[]>([]);
|
||||
interface AssistantDirectiveEntitiesState {
|
||||
entities: AssistantDirectiveEntity[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const AssistantDirectiveEntitiesContext = createContext<AssistantDirectiveEntitiesState>({
|
||||
entities: [],
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const AssistantDirectiveEntitiesProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { data: entities = [], isLoading } = clientApi.assistant.getContextEntities.useQuery(undefined, {
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
return (
|
||||
<AssistantDirectiveEntitiesContext.Provider value={{ entities, isLoading }}>
|
||||
{children}
|
||||
</AssistantDirectiveEntitiesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const AssistantMarkdownSpan = ({
|
||||
children,
|
||||
@@ -417,7 +438,7 @@ const AssistantMarkdownSpan = ({
|
||||
"data-directive-type": directiveType,
|
||||
...props
|
||||
}: MarkdownSpanProps) => {
|
||||
const entities = useContext(AssistantDirectiveEntitiesContext);
|
||||
const { entities } = useContext(AssistantDirectiveEntitiesContext);
|
||||
if (assistantDirective === "true" && directiveId && directiveLabel && directiveType) {
|
||||
const entity = resolveAssistantDirectiveEntity(entities, {
|
||||
id: directiveId,
|
||||
@@ -444,26 +465,20 @@ const assistantMarkdownComponents = unstable_memoizeMarkdownComponents({
|
||||
SyntaxHighlighter: AssistantSyntaxHighlighter,
|
||||
});
|
||||
|
||||
const AssistantTextPart = () => {
|
||||
const { data: entities = [] } = clientApi.assistant.getContextEntities.useQuery(undefined, { staleTime: 60_000 });
|
||||
|
||||
return (
|
||||
<AssistantDirectiveEntitiesContext.Provider value={entities}>
|
||||
<MarkdownTextPrimitive
|
||||
className={classes.messageMarkdown}
|
||||
preprocess={normalizeAssistantMarkdown}
|
||||
remarkPlugins={markdownRemarkPlugins}
|
||||
components={assistantMarkdownComponents}
|
||||
componentsByLanguage={{ mermaid: { SyntaxHighlighter: MermaidDiagram } }}
|
||||
defer
|
||||
/>
|
||||
</AssistantDirectiveEntitiesContext.Provider>
|
||||
);
|
||||
};
|
||||
const AssistantTextPart = () => (
|
||||
<MarkdownTextPrimitive
|
||||
className={classes.messageMarkdown}
|
||||
preprocess={normalizeAssistantMarkdown}
|
||||
remarkPlugins={markdownRemarkPlugins}
|
||||
components={assistantMarkdownComponents}
|
||||
componentsByLanguage={{ mermaid: { SyntaxHighlighter: MermaidDiagram } }}
|
||||
defer
|
||||
/>
|
||||
);
|
||||
|
||||
const UserTextPart = () => {
|
||||
const { text } = useMessagePartText();
|
||||
const { data: entities = [] } = clientApi.assistant.getContextEntities.useQuery(undefined, { staleTime: 60_000 });
|
||||
const { entities } = useContext(AssistantDirectiveEntitiesContext);
|
||||
const segments = parseAssistantDirectives(text);
|
||||
return (
|
||||
<span className={classes.messageText}>
|
||||
@@ -548,7 +563,7 @@ const SentAttachment = () => <Attachment />;
|
||||
const HiddenMessagePart = () => null;
|
||||
|
||||
const ReasoningPart = (_props: ReasoningMessagePartProps) => (
|
||||
<AssistantDirectiveEntitiesContext.Provider value={[]}>
|
||||
<AssistantDirectiveEntitiesContext.Provider value={{ entities: [], isLoading: false }}>
|
||||
<MarkdownTextPrimitive
|
||||
className={`${classes.messageMarkdown} ${classes.reasoningText}`}
|
||||
preprocess={normalizeAssistantMarkdown}
|
||||
@@ -2264,9 +2279,7 @@ const ComposerTriggers = () => {
|
||||
const aui = useAui();
|
||||
const mentionViewportRef = useRef<HTMLDivElement>(null);
|
||||
const slashViewportRef = useRef<HTMLDivElement>(null);
|
||||
const { data: entities = [], isLoading } = clientApi.assistant.getContextEntities.useQuery(undefined, {
|
||||
staleTime: 60_000,
|
||||
});
|
||||
const { entities, isLoading } = useContext(AssistantDirectiveEntitiesContext);
|
||||
const categories = useMemo(
|
||||
() =>
|
||||
(["app", "integration", "board", "widget"] as const).map((type) => ({
|
||||
@@ -2914,7 +2927,7 @@ const AssistantActivityBar = ({
|
||||
|
||||
return (
|
||||
<Box component="output" className={classes.activityBar} aria-live="polite">
|
||||
<UnstyledButton className={classes.activityTrigger} onClick={onOpen} aria-label={t("activity.expand")}>
|
||||
<UnstyledButton className={classes.activityTrigger} onClick={onOpen}>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ThemeIcon variant="light" color={activityColor} radius="xl">
|
||||
<AssistantDotMatrix state={activityState} label={title} role="presentation" aria-hidden />
|
||||
@@ -2996,7 +3009,7 @@ export const AssistantConversationSurface = ({
|
||||
const scrollToLatestBehavior = isRunning || reducedMotion ? "instant" : "smooth";
|
||||
|
||||
return (
|
||||
<>
|
||||
<AssistantDirectiveEntitiesProvider>
|
||||
<Group className={classes.panelHeader} justify="space-between" wrap="nowrap" gap="xs">
|
||||
{onWindowClose && onMinimize && (
|
||||
<Group
|
||||
@@ -3107,7 +3120,7 @@ export const AssistantConversationSurface = ({
|
||||
</ThreadPrimitive.Root>
|
||||
</ReasoningVisibilityContext.Provider>
|
||||
</AssistantQuestionPortalProvider>
|
||||
</>
|
||||
</AssistantDirectiveEntitiesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3135,9 +3148,19 @@ export const AssistantPanel = ({
|
||||
}: AssistantPanelProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const previousFocusRef = useRef<HTMLElement | null>(null);
|
||||
const panelRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
useWindowEvent("keydown", (event) => {
|
||||
if (opened && event.key === "Escape") onClose();
|
||||
if (
|
||||
!opened ||
|
||||
event.key !== "Escape" ||
|
||||
event.defaultPrevented ||
|
||||
event.isComposing ||
|
||||
isEscapeOwnedByNestedOverlay(event.target, panelRef.current)
|
||||
)
|
||||
return;
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!opened) {
|
||||
@@ -3174,7 +3197,7 @@ export const AssistantPanel = ({
|
||||
/>
|
||||
)}
|
||||
{opened && (
|
||||
<dialog className={classes.floatingPanel} aria-label={t("title")} open>
|
||||
<dialog ref={panelRef} className={classes.floatingPanel} aria-label={t("title")} open>
|
||||
<AssistantConversationSurface
|
||||
isRunning={isRunning}
|
||||
pendingAction={pendingAction}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { ActionIcon, Box, Center, Group, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core";
|
||||
import { IconArrowsMaximize, IconRobot } from "@tabler/icons-react";
|
||||
import { useAui, useAuiState } from "@assistant-ui/react";
|
||||
import { ActionIcon, Box, Button, Center, Group, Loader, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core";
|
||||
import { IconAlertTriangle, IconArrowsMaximize, IconMessage, IconRobot } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "@homarr/widgets";
|
||||
@@ -30,81 +29,22 @@ export const AssistantBoardWidget = (props: WidgetComponentProps<"assistant">) =
|
||||
);
|
||||
};
|
||||
|
||||
const EnabledAssistantBoardWidget = ({ options, width, height, isEditMode }: WidgetComponentProps<"assistant">) => {
|
||||
const t = useI18n();
|
||||
const EnabledAssistantBoardWidget = ({ width, height, isEditMode }: WidgetComponentProps<"assistant">) => {
|
||||
const board = useRequiredBoard();
|
||||
const assistant = useHomarrAssistant();
|
||||
const preferences = useAssistantPreferences();
|
||||
const aui = useAui();
|
||||
const currentThreadId = useAuiState((state) => state.threadListItem.remoteId);
|
||||
const messages = useAuiState((state) => state.thread.messages);
|
||||
const isLoading = useAuiState((state) => state.thread.isLoading);
|
||||
const latestAssistantMessage = messages.findLast((message) => message.role === "assistant");
|
||||
const pendingAction = getPendingAssistantAction(latestAssistantMessage);
|
||||
const pinnedConversation = options.conversationMode === "pinned" ? options.conversation : null;
|
||||
const pinnedConversationIsActive = pinnedConversation === null || pinnedConversation.value === currentThreadId;
|
||||
const [switching, setSwitching] = useState(false);
|
||||
const [switchErrorConversationId, setSwitchErrorConversationId] = useState<string | null>(null);
|
||||
const hasSwitchError = switchErrorConversationId === pinnedConversation?.value;
|
||||
|
||||
const selectModel = useCallback(
|
||||
(modelId: string) => {
|
||||
preferences.setModelId(modelId);
|
||||
const threadListItem = aui.threadListItem();
|
||||
if (!threadListItem.getState().remoteId) return;
|
||||
threadListItem.updateCustom({ ...threadListItem.getState().custom, modelId });
|
||||
},
|
||||
[aui, preferences],
|
||||
);
|
||||
|
||||
const openPinnedConversation = async () => {
|
||||
if (!pinnedConversation) return;
|
||||
setSwitchErrorConversationId(null);
|
||||
setSwitching(true);
|
||||
try {
|
||||
await aui.threads().switchToThread(pinnedConversation.value);
|
||||
} catch {
|
||||
setSwitchErrorConversationId(pinnedConversation.value);
|
||||
} finally {
|
||||
setSwitching(false);
|
||||
}
|
||||
const selectModel = (modelId: string) => {
|
||||
preferences.setModelId(modelId);
|
||||
const threadListItem = aui.threadListItem();
|
||||
if (!threadListItem.getState().remoteId) return;
|
||||
threadListItem.updateCustom({ ...threadListItem.getState().custom, modelId });
|
||||
};
|
||||
|
||||
if (options.conversationMode === "pinned" && pinnedConversation === null) {
|
||||
return (
|
||||
<AssistantWidgetState
|
||||
icon={IconMessage}
|
||||
title={t("widget.assistant.pinned.selectTitle")}
|
||||
description={t("widget.assistant.pinned.selectDescription")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!pinnedConversationIsActive && pinnedConversation) {
|
||||
return (
|
||||
<AssistantWidgetState
|
||||
icon={hasSwitchError ? IconAlertTriangle : IconMessage}
|
||||
color={hasSwitchError ? "red" : "gray"}
|
||||
title={t("widget.assistant.pinned.title")}
|
||||
description={
|
||||
hasSwitchError
|
||||
? t("widget.assistant.pinned.error")
|
||||
: t("widget.assistant.pinned.description", { conversation: pinnedConversation.label })
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="light"
|
||||
color={hasSwitchError ? "red" : "gray"}
|
||||
onClick={() => void openPinnedConversation()}
|
||||
loading={switching}
|
||||
leftSection={switching ? <Loader type="bars" size="xs" /> : <IconMessage size={16} />}
|
||||
>
|
||||
{switching ? t("widget.assistant.pinned.opening") : t("widget.assistant.pinned.open")}
|
||||
</Button>
|
||||
</AssistantWidgetState>
|
||||
);
|
||||
}
|
||||
|
||||
if (width < 300 || height < 280) {
|
||||
return <CompactAssistantWidget onOpen={assistant.open} />;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user