fix: address assistant capability review
This commit is contained in:
1 parent
40c7b1523b
commit
b5162a9b95
8 files changed
+260
-141
No files matched your search
@@ -2,7 +2,7 @@
|
||||
position: fixed;
|
||||
z-index: 290;
|
||||
inset-inline-start: 50%;
|
||||
bottom: max(1rem, env(safe-area-inset-bottom));
|
||||
bottom: max(1rem, env(safe-area-inset-bottom, 0px));
|
||||
display: flex;
|
||||
width: min(31rem, calc(100vw - 1rem));
|
||||
min-height: 3.5rem;
|
||||
@@ -540,14 +540,14 @@
|
||||
|
||||
@media (max-width: 48em) {
|
||||
.activityBar {
|
||||
bottom: max(0.5rem, env(safe-area-inset-bottom));
|
||||
bottom: max(0.5rem, env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.floatingPanel {
|
||||
bottom: max(0.5rem, env(safe-area-inset-bottom));
|
||||
bottom: max(0.5rem, env(safe-area-inset-bottom, 0px));
|
||||
width: calc(100vw - 1rem);
|
||||
height: min(44rem, calc(100dvh - 1rem - env(safe-area-inset-bottom)));
|
||||
min-height: min(20rem, calc(100dvh - 1rem - env(safe-area-inset-bottom)));
|
||||
height: min(44rem, calc(100dvh - 1rem - env(safe-area-inset-bottom, 0px)));
|
||||
min-height: min(20rem, calc(100dvh - 1rem - env(safe-area-inset-bottom, 0px)));
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useMemo, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type {
|
||||
FileMessagePartProps,
|
||||
ImageMessagePartProps,
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
Button,
|
||||
Collapse,
|
||||
Divider,
|
||||
FocusTrap,
|
||||
Group,
|
||||
Loader,
|
||||
Popover,
|
||||
@@ -184,7 +185,24 @@ const FilePart = ({ data, filename, mimeType }: FileMessagePartProps) => {
|
||||
|
||||
const ImagePart = ({ image, filename }: ImageMessagePartProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
return <Box component="img" src={image} alt={filename ?? t("attachedImage")} className={classes.messageImage} />;
|
||||
const source = (() => {
|
||||
if (/^data:image\/(?:gif|jpeg|png|webp);base64,/u.test(image)) return image;
|
||||
try {
|
||||
const url = new URL(image, window.location.origin);
|
||||
return url.origin === window.location.origin ? url.href : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
if (!source) {
|
||||
return (
|
||||
<Group className={classes.messageFile} gap="xs" wrap="nowrap">
|
||||
<IconAlertTriangle size={16} />
|
||||
<Text size="xs">{t("externalImageBlocked")}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
return <Box component="img" src={source} alt={filename ?? t("attachedImage")} className={classes.messageImage} />;
|
||||
};
|
||||
|
||||
const ToolPart = ({
|
||||
@@ -1007,6 +1025,7 @@ const Composer = () => {
|
||||
className={classes.composerInput}
|
||||
placeholder={t("composerPlaceholder")}
|
||||
rows={1}
|
||||
data-autofocus
|
||||
/>
|
||||
{running ? (
|
||||
<ComposerPrimitive.Cancel asChild>
|
||||
@@ -1128,10 +1147,19 @@ export const AssistantPanel = ({
|
||||
latestStatus,
|
||||
}: AssistantPanelProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const previousFocusRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useWindowEvent("keydown", (event) => {
|
||||
if (opened && event.key === "Escape") onClose();
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!opened) return;
|
||||
previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
return () => {
|
||||
previousFocusRef.current?.focus();
|
||||
previousFocusRef.current = null;
|
||||
};
|
||||
}, [opened]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1147,73 +1175,75 @@ export const AssistantPanel = ({
|
||||
/>
|
||||
)}
|
||||
{opened && (
|
||||
<dialog className={classes.floatingPanel} aria-label={t("title")} open>
|
||||
<Group className={classes.panelHeader} justify="space-between" wrap="nowrap" gap="xs">
|
||||
<Group className={classes.panelIdentity} gap="xs" wrap="nowrap">
|
||||
<ThemeIcon variant="light" color="red" radius="xl">
|
||||
<IconRobot size={18} />
|
||||
</ThemeIcon>
|
||||
<div className={classes.panelIdentityText}>
|
||||
<Text fw={700} lh={1.1} lineClamp={1}>
|
||||
{t("title")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
{isRunning ? t("activity.thinking") : t("subtitle")}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group className={classes.panelActions} gap={2} wrap="nowrap">
|
||||
<ConversationHistory />
|
||||
<Tooltip label={t("newConversation")}>
|
||||
<ThreadListPrimitive.New asChild>
|
||||
<ActionIcon
|
||||
className={classes.panelAction}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("newConversation")}
|
||||
>
|
||||
<IconPlus size={17} />
|
||||
</ActionIcon>
|
||||
</ThreadListPrimitive.New>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
className={classes.panelAction}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={onClose}
|
||||
aria-label={t("close")}
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
<ThreadPrimitive.Root className={classes.thread}>
|
||||
<ThreadPrimitive.Viewport className={classes.viewport}>
|
||||
<Box className={classes.messages}>
|
||||
<EmptyThread />
|
||||
<ThreadPrimitive.Messages components={{ UserMessage, AssistantMessage }} />
|
||||
</Box>
|
||||
<SelectionToolbarPrimitive.Root className={classes.selectionToolbar}>
|
||||
<SelectionToolbarPrimitive.Quote asChild>
|
||||
<Button variant="filled" color="dark" size="compact-sm" leftSection={<IconQuote size={14} />}>
|
||||
{t("quoteSelection")}
|
||||
</Button>
|
||||
</SelectionToolbarPrimitive.Quote>
|
||||
</SelectionToolbarPrimitive.Root>
|
||||
<ThreadPrimitive.ScrollToBottom asChild>
|
||||
<FocusTrap active>
|
||||
<dialog className={classes.floatingPanel} aria-label={t("title")} open>
|
||||
<Group className={classes.panelHeader} justify="space-between" wrap="nowrap" gap="xs">
|
||||
<Group className={classes.panelIdentity} gap="xs" wrap="nowrap">
|
||||
<ThemeIcon variant="light" color="red" radius="xl">
|
||||
<IconRobot size={18} />
|
||||
</ThemeIcon>
|
||||
<div className={classes.panelIdentityText}>
|
||||
<Text fw={700} lh={1.1} lineClamp={1}>
|
||||
{t("title")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
{isRunning ? t("activity.thinking") : t("subtitle")}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group className={classes.panelActions} gap={2} wrap="nowrap">
|
||||
<ConversationHistory />
|
||||
<Tooltip label={t("newConversation")}>
|
||||
<ThreadListPrimitive.New asChild>
|
||||
<ActionIcon
|
||||
className={classes.panelAction}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("newConversation")}
|
||||
>
|
||||
<IconPlus size={17} />
|
||||
</ActionIcon>
|
||||
</ThreadListPrimitive.New>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
className={classes.scrollToBottom}
|
||||
variant="default"
|
||||
radius="xl"
|
||||
aria-label={t("scrollToLatest")}
|
||||
className={classes.panelAction}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={onClose}
|
||||
aria-label={t("close")}
|
||||
>
|
||||
<IconArrowUp size={16} style={{ transform: "rotate(180deg)" }} />
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</ThreadPrimitive.ScrollToBottom>
|
||||
</ThreadPrimitive.Viewport>
|
||||
<Composer />
|
||||
</ThreadPrimitive.Root>
|
||||
</dialog>
|
||||
</Group>
|
||||
</Group>
|
||||
<ThreadPrimitive.Root className={classes.thread}>
|
||||
<ThreadPrimitive.Viewport className={classes.viewport}>
|
||||
<Box className={classes.messages}>
|
||||
<EmptyThread />
|
||||
<ThreadPrimitive.Messages components={{ UserMessage, AssistantMessage }} />
|
||||
</Box>
|
||||
<SelectionToolbarPrimitive.Root className={classes.selectionToolbar}>
|
||||
<SelectionToolbarPrimitive.Quote asChild>
|
||||
<Button variant="filled" color="dark" size="compact-sm" leftSection={<IconQuote size={14} />}>
|
||||
{t("quoteSelection")}
|
||||
</Button>
|
||||
</SelectionToolbarPrimitive.Quote>
|
||||
</SelectionToolbarPrimitive.Root>
|
||||
<ThreadPrimitive.ScrollToBottom asChild>
|
||||
<ActionIcon
|
||||
className={classes.scrollToBottom}
|
||||
variant="default"
|
||||
radius="xl"
|
||||
aria-label={t("scrollToLatest")}
|
||||
>
|
||||
<IconArrowUp size={16} style={{ transform: "rotate(180deg)" }} />
|
||||
</ActionIcon>
|
||||
</ThreadPrimitive.ScrollToBottom>
|
||||
</ThreadPrimitive.Viewport>
|
||||
<Composer />
|
||||
</ThreadPrimitive.Root>
|
||||
</dialog>
|
||||
</FocusTrap>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
|
||||
|
||||
import { clientApi, fetchApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { createId } from "@homarr/common";
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
import { showErrorNotification, showWarningNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
@@ -99,7 +100,7 @@ const createAssistantAttachmentAdapter = (allowImages: boolean): AttachmentAdapt
|
||||
if (file.size > sizeLimit) {
|
||||
throw new Error(isImage ? "Images must be smaller than 1 MB." : "Documents must be smaller than 350 KB.");
|
||||
}
|
||||
const id = crypto.randomUUID();
|
||||
const id = createId();
|
||||
pendingAttachmentIds.add(id);
|
||||
return {
|
||||
id,
|
||||
@@ -286,10 +287,12 @@ const AssistantThreadRuntime = () => {
|
||||
() => ({
|
||||
submit: ({ message, type }: { message: ThreadMessage; type: "positive" | "negative" }) => {
|
||||
if (!threadId) return;
|
||||
void fetchApi.assistant.submitFeedback.mutate({ threadId, messageId: message.id, type });
|
||||
void fetchApi.assistant.submitFeedback
|
||||
.mutate({ threadId, messageId: message.id, type })
|
||||
.catch(() => showErrorNotification({ title: t("responseError.title"), message: t("feedbackError") }));
|
||||
},
|
||||
}),
|
||||
[threadId],
|
||||
[t, threadId],
|
||||
);
|
||||
const onError = useCallback(
|
||||
() =>
|
||||
|
||||
Reference in new issue
Block a user