enhance assistant batch responses

This commit is contained in:
ajnart committed 2026-08-14 11:32:10 +02:00
1 parent edf3f6cd84
commit 8d06201b41
10 files changed
+207 -38

No files matched your search

@@ -0,0 +1,19 @@
import { describe, expect, test } from "vitest";
import { getSafeAssistantMarkdownImageSource } from "./assistant-markdown-image";
describe("getSafeAssistantMarkdownImageSource", () => {
test("allows Homarr-relative and ordinary remote images", () => {
expect(getSafeAssistantMarkdownImageSource("/logo/logo.png")).toBe("/logo/logo.png");
expect(getSafeAssistantMarkdownImageSource("https://cdn.example.com/icons/discord.svg")).toBe(
"https://cdn.example.com/icons/discord.svg",
);
});
test("rejects unsafe protocols, protocol-relative URLs, and embedded credentials", () => {
expect(getSafeAssistantMarkdownImageSource("javascript:alert(1)")).toBeNull();
expect(getSafeAssistantMarkdownImageSource("data:image/svg+xml;base64,PHN2Zy8+")).toBeNull();
expect(getSafeAssistantMarkdownImageSource("//example.com/icon.svg")).toBeNull();
expect(getSafeAssistantMarkdownImageSource("https://user:secret@example.com/icon.svg")).toBeNull();
});
});
@@ -0,0 +1,12 @@
export const getSafeAssistantMarkdownImageSource = (source: string | Blob | undefined) => {
if (typeof source !== "string" || !source) return null;
if (source.startsWith("/") && !source.startsWith("//")) return source;
try {
const url = new URL(source);
if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password) return null;
return url.toString();
} catch {
return null;
}
};
@@ -466,11 +466,13 @@
border-top: 1px solid var(--mantine-color-default-border);
}
.messageMarkdown img {
.markdownImage {
display: block;
max-width: 100%;
max-height: 24rem;
height: auto;
margin-block: 0.65rem;
object-fit: contain;
border-radius: var(--mantine-radius-md);
}
@@ -511,6 +513,13 @@
border-bottom: 0;
}
.markdownTableWrap .markdownImage {
width: 2rem;
height: 2rem;
margin: 0 auto;
border-radius: var(--mantine-radius-sm);
}
.messageQuote {
margin: 0 0 0.55rem;
padding-inline-start: 0.65rem;
@@ -104,6 +104,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import classes from "./assistant-panel.module.css";
import { useAssistantAutoApproval, useAssistantAutomaticAction } from "./assistant-auto-approval";
import { normalizeAssistantMarkdown } from "./assistant-markdown";
import { getSafeAssistantMarkdownImageSource } from "./assistant-markdown-image";
import { getAssistantTelemetry, getAssistantUsage } from "./assistant-message-metadata";
import type { AssistantPendingAction } from "./assistant-pending-action";
import { AssistantQuestionPortalProvider } from "./assistant-question-portal";
@@ -153,12 +154,29 @@ const MarkdownTable = ({ children, ...props }: ComponentPropsWithoutRef<"table">
</div>
);
const MarkdownImage = ({ src, alt = "", ...props }: ComponentPropsWithoutRef<"img">) => {
const safeSource = getSafeAssistantMarkdownImageSource(src);
if (!safeSource) return null;
return (
<img
{...props}
src={safeSource}
alt={alt}
className={classes.markdownImage}
loading="lazy"
decoding="async"
referrerPolicy="no-referrer"
/>
);
};
const AssistantTextPart = () => (
<MarkdownTextPrimitive
className={classes.messageMarkdown}
preprocess={normalizeAssistantMarkdown}
remarkPlugins={markdownRemarkPlugins}
components={{ a: MarkdownLink, table: MarkdownTable }}
components={{ a: MarkdownLink, img: MarkdownImage, table: MarkdownTable }}
defer
/>
);