fix assistant exports and tool calls

This commit is contained in:
ajnart committed 2026-08-10 14:09:44 +02:00
1 parent f7eed7ce8a
commit 305c9af2da
15 files changed
+756 -29

No files matched your search

@@ -114,7 +114,7 @@ import remarkDirective from "remark-directive";
import remarkGfm from "remark-gfm";
import { useRouter } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import { clientApi, fetchApi } from "@homarr/api/client";
import { assistantProviderIds, assistantProviderPresets, assistantReasoningModes } from "@homarr/definitions";
import type { AssistantProvider } from "@homarr/definitions";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
@@ -123,6 +123,11 @@ import { useScopedI18n } from "@homarr/translation/client";
import classes from "./assistant-panel.module.css";
import { getAssistantActivityState } from "./assistant-activity-state";
import { useAssistantAutoApproval, useAssistantAutomaticAction } from "./assistant-auto-approval";
import {
buildAssistantConversationMarkdown,
buildAssistantMessageMarkdown,
getAssistantConversationExportFilename,
} from "./assistant-conversation-export";
import { AssistantDotMatrix } from "./assistant-dot-matrix";
import { AssistantImage } from "./assistant-image";
import { getAssistantDirectiveTranslationKey, parseAssistantDirectives } from "./assistant-directives";
@@ -1068,6 +1073,46 @@ const BranchPicker = () => {
);
};
const downloadAssistantMarkdown = (markdown: string, filename: string) => {
const blobUrl = URL.createObjectURL(new Blob([markdown], { type: "text/markdown;charset=utf-8" }));
const link = document.createElement("a");
link.href = blobUrl;
link.download = filename;
link.hidden = true;
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(blobUrl), 0);
};
const AssistantMessageExportAction = () => {
const t = useScopedI18n("common.assistant");
const message = useAuiState((state) => state.message);
return (
<Tooltip label={t("exportMarkdown")}>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
aria-label={t("exportMarkdown")}
onClick={() => {
const markdown = buildAssistantMessageMarkdown({
id: message.id,
parentId: message.parentId,
format: "assistant-ui/runtime",
content: message,
createdAt: message.createdAt,
});
downloadAssistantMarkdown(markdown, `assistant-message-${message.id.slice(0, 8)}.md`);
}}
>
<IconFileExport size={14} />
</ActionIcon>
</Tooltip>
);
};
const AssistantMessageActions = () => {
const t = useScopedI18n("common.assistant");
return (
@@ -1096,13 +1141,7 @@ const AssistantMessageActions = () => {
</ActionIcon>
</ActionBarPrimitive.FeedbackNegative>
</Tooltip>
<Tooltip label={t("exportMarkdown")}>
<ActionBarPrimitive.ExportMarkdown asChild>
<ActionIcon variant="subtle" color="gray" size="sm" aria-label={t("exportMarkdown")}>
<IconFileExport size={14} />
</ActionIcon>
</ActionBarPrimitive.ExportMarkdown>
</Tooltip>
<AssistantMessageExportAction />
<Tooltip label={t("regenerate")}>
<ActionBarPrimitive.Reload asChild>
<ActionIcon variant="subtle" color="gray" size="sm" aria-label={t("regenerate")}>
@@ -1892,6 +1931,30 @@ const HistorySelectContext = createContext<() => void>(() => undefined);
const ThreadListItem = () => {
const t = useScopedI18n("common.assistant");
const onSelect = useContext(HistorySelectContext);
const remoteId = useAuiState((state) => state.threadListItem.remoteId);
const title = useAuiState((state) => state.threadListItem.title);
const [exporting, setExporting] = useState(false);
const exportConversation = async () => {
if (!remoteId || exporting) return;
setExporting(true);
try {
const conversation = await fetchApi.assistant.getThread.query({ threadId: remoteId });
const markdown = buildAssistantConversationMarkdown(conversation);
downloadAssistantMarkdown(
markdown,
getAssistantConversationExportFilename(conversation.thread.title ?? title, remoteId),
);
} catch {
showErrorNotification({
title: t("exportConversation.failedTitle"),
message: t("exportConversation.failedDescription"),
});
} finally {
setExporting(false);
}
};
return (
<ThreadListItemPrimitive.Root className={classes.historyItem}>
<Group gap={4} wrap="nowrap">
@@ -1908,6 +1971,20 @@ const ThreadListItem = () => {
<ThreadListItemPrimitive.Title fallback={t("newConversation")} />
</Button>
</ThreadListItemPrimitive.Trigger>
<Tooltip label={t("exportConversation.action")}>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
loading={exporting}
loaderProps={{ type: "bars" }}
disabled={!remoteId}
aria-label={t("exportConversation.action")}
onClick={() => void exportConversation()}
>
<IconFileExport size={14} />
</ActionIcon>
</Tooltip>
<ThreadListItemPrimitive.Delete asChild>
<ActionIcon variant="subtle" color="red" size="sm" aria-label={t("delete")}>
<IconTrash size={14} />