e0aac5aa04
The public-share widget was a separate minimal impl: plain-text answer, static
'Thinking…', no markdown, no tool-cards. Now it renders through the internal
chat's debugged presentational layer (MessageList/MessageItem/TypingIndicator/
ToolCallCard), so a share gets the same incremental streaming, animated typing
indicator, markdown, and tool-call cards. The share keeps its anonymous
transport (useChat + DefaultChatTransport '/api/shares/ai/stream',
credentials:'omit').
The shared components were already prop-driven (UIMessage[] + isStreaming) with
no transport/auth coupling; made the new props additive optionals (emptyState,
showCitations, neutralizeInternalLinks) all defaulting to current behavior, so
the internal chat is unchanged.
Security (review-caught): rendering assistant markdown on the ANONYMOUS share
made internal links (/p/{id}, /settings/...) clickable, which the old plain-text
render didn't. renderChatMarkdown gains neutralizeInternalLinks (true only on
the share): a one-shot DOMPurify afterSanitizeAttributes hook (added/removed by
reference around a single sanitize) strips href from internal/relative/non-http(s)
links (rendered inert) and keeps external http(s) links with
rel=noopener noreferrer nofollow target=_blank. Tests cover both the link
neutralization and the absence of any global-hook leak into internal renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
2.6 KiB
TypeScript
84 lines
2.6 KiB
TypeScript
import { Anchor, Group, Loader, Text, ThemeIcon } from "@mantine/core";
|
|
import { IconAlertCircle, IconCheck } from "@tabler/icons-react";
|
|
import { Link } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
getToolName,
|
|
toolCitations,
|
|
toolLabelKey,
|
|
toolRunState,
|
|
ToolUiPart,
|
|
} from "@/features/ai-chat/utils/tool-parts.tsx";
|
|
import classes from "@/features/ai-chat/components/ai-chat.module.css";
|
|
|
|
interface ToolCallCardProps {
|
|
part: ToolUiPart;
|
|
/**
|
|
* Whether to render page citation links. Defaults to true (the internal chat,
|
|
* where the reader is authenticated and the `/p/{id}` links resolve). The
|
|
* public share passes false: an anonymous reader cannot open internal pages,
|
|
* so the links would 404/redirect to login. Suppressing them keeps the card
|
|
* (the action log itself) while dropping the unusable links.
|
|
*/
|
|
showCitations?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Compact action-log card for a single agent tool invocation. It shows what the
|
|
* agent DID (the agent writes without confirmation — D2), its run state
|
|
* (running / done / error), and citation link(s) to any referenced page(s).
|
|
*/
|
|
export default function ToolCallCard({
|
|
part,
|
|
showCitations = true,
|
|
}: ToolCallCardProps) {
|
|
const { t } = useTranslation();
|
|
const toolName = getToolName(part);
|
|
const state = toolRunState(part.state);
|
|
const { key, values } = toolLabelKey(toolName);
|
|
const citations = showCitations ? toolCitations(part) : [];
|
|
|
|
return (
|
|
<div className={classes.toolCard}>
|
|
<Group gap={6} wrap="nowrap" align="center">
|
|
{state === "running" && <Loader size={14} />}
|
|
{state === "done" && (
|
|
<ThemeIcon size={16} radius="xl" color="green" variant="light">
|
|
<IconCheck size={12} />
|
|
</ThemeIcon>
|
|
)}
|
|
{state === "error" && (
|
|
<ThemeIcon size={16} radius="xl" color="red" variant="light">
|
|
<IconAlertCircle size={12} />
|
|
</ThemeIcon>
|
|
)}
|
|
<Text size="sm" fw={500}>
|
|
{t(key, values)}
|
|
</Text>
|
|
</Group>
|
|
|
|
{state === "error" && part.errorText && (
|
|
<Text size="xs" c="red" mt={2}>
|
|
{part.errorText}
|
|
</Text>
|
|
)}
|
|
|
|
{citations.length > 0 && (
|
|
<Group gap={6} mt={4} wrap="wrap">
|
|
{citations.map((c) => (
|
|
<Anchor
|
|
key={c.pageId}
|
|
component={Link}
|
|
to={c.href}
|
|
size="xs"
|
|
lineClamp={1}
|
|
>
|
|
{c.title || t("Open page")}
|
|
</Anchor>
|
|
))}
|
|
</Group>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|