feat(share-ai): label public chat with the assistant identity name

The anonymous public-share "Ask AI" chat labeled every assistant turn
with the generic "AI agent" even when an Assistant identity (agent role)
was configured. Surface the configured identity name instead, falling
back to "AI agent" when no identity is set.

- server: AiSettingsService.resolvePublicShareAssistantName resolves the
  configured role's name (null when unset/missing/disabled), mirroring
  PublicShareChatService.resolveShareRole; ShareController returns it as
  aiAssistantName on /shares/page-info (only when the assistant is on).
- client: thread aiAssistantName -> ShareAiWidget -> MessageList ->
  MessageItem/TypingIndicator via an optional assistantName prop; the
  internal chat omits it and keeps showing "AI agent".
- i18n: add "{{name}} is typing…" (en-US, ru-RU) for the typing line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-21 05:01:07 +03:00
parent 3936c482d9
commit 18105ff6db
8 changed files with 55 additions and 13 deletions

View File

@@ -22,6 +22,11 @@ interface MessageItemProps {
* UUIDs/routes in the assistant's markdown don't leak as clickable links.
*/
neutralizeInternalLinks?: boolean;
/**
* Display name for the dimmed assistant label. Defaults to "AI agent" when
* absent; the public share passes the configured identity (agent role) name.
*/
assistantName?: string;
}
/**
@@ -40,6 +45,7 @@ export default function MessageItem({
message,
showCitations = true,
neutralizeInternalLinks = false,
assistantName,
}: MessageItemProps) {
const { t } = useTranslation();
const isUser = message.role === "user";
@@ -61,7 +67,7 @@ export default function MessageItem({
return (
<Box className={classes.messageRow}>
<Text size="xs" c="dimmed" mb={4}>
{t("AI agent")}
{assistantName?.trim() || t("AI agent")}
</Text>
{message.parts.map((part, index) => {
if (part.type === "text") {

View File

@@ -30,6 +30,12 @@ interface MessageListProps {
* UUIDs/routes don't leak as clickable links to anonymous readers.
*/
neutralizeInternalLinks?: boolean;
/**
* Display name for the assistant's dimmed row label and typing indicator.
* Defaults to "AI agent" when absent. The public share passes the configured
* identity (agent role) name; the internal chat omits it.
*/
assistantName?: string;
}
// Distance (px) from the bottom within which the viewport still counts as
@@ -67,6 +73,7 @@ export default function MessageList({
emptyState,
showCitations = true,
neutralizeInternalLinks = false,
assistantName,
}: MessageListProps) {
const { t } = useTranslation();
const viewportRef = useRef<HTMLDivElement>(null);
@@ -148,9 +155,10 @@ export default function MessageList({
message={message}
showCitations={showCitations}
neutralizeInternalLinks={neutralizeInternalLinks}
assistantName={assistantName}
/>
))}
{typing && <TypingIndicator />}
{typing && <TypingIndicator assistantName={assistantName} />}
</Stack>
</ScrollArea>
);

View File

@@ -2,22 +2,33 @@ import { Box, Group, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import classes from "@/features/ai-chat/components/ai-chat.module.css";
interface TypingIndicatorProps {
/**
* Display name for the dimmed label and the "… is typing…" line. Defaults to
* "AI agent" when absent; the public share passes the configured identity
* (agent role) name.
*/
assistantName?: string;
}
/**
* Live "AI agent is typing…" placeholder shown while a turn is in flight but the
* latest assistant message has no visible content yet (no rendered text/tool
* parts). It covers the gap between sending and the first streamed token, and is
* replaced by the real assistant message once content starts arriving.
* Live " is typing…" placeholder shown while a turn is in flight but the latest
* assistant message has no visible content yet (no rendered text/tool parts). It
* covers the gap between sending and the first streamed token, and is replaced by
* the real assistant message once content starts arriving.
*
* Mirrors the assistant row layout in MessageItem (the dimmed "AI agent" label),
* so it reads as the assistant's bubble taking shape.
* Mirrors the assistant row layout in MessageItem (the dimmed label), so it reads
* as the assistant's bubble taking shape. The label and typing line use the
* configured identity name when provided, otherwise the generic "AI agent".
*/
export default function TypingIndicator() {
export default function TypingIndicator({ assistantName }: TypingIndicatorProps) {
const { t } = useTranslation();
const name = assistantName?.trim();
return (
<Box className={classes.messageRow}>
<Text size="xs" c="dimmed" mb={4}>
{t("AI agent")}
{name || t("AI agent")}
</Text>
<Group gap={8} align="center">
<span className={classes.typingDots} aria-hidden="true">
@@ -26,7 +37,7 @@ export default function TypingIndicator() {
<span />
</span>
<Text size="sm" c="dimmed">
{t("AI agent is typing…")}
{name ? t("{{name}} is typing…", { name }) : t("AI agent is typing…")}
</Text>
</Group>
</Box>

View File

@@ -27,6 +27,8 @@ interface ShareAiWidgetProps {
shareId: string;
/** The page the reader currently has open (context for "this page"). */
pageId: string;
/** Display name of the configured assistant identity; falls back to 'AI agent' when absent. */
assistantName?: string;
}
/**
@@ -47,7 +49,11 @@ interface ShareAiWidgetProps {
* links (so internal UUIDs/auth-gated routes in the answer don't leak as
* clickable links), and a documentation-focused empty state.
*/
export default function ShareAiWidget({ shareId, pageId }: ShareAiWidgetProps) {
export default function ShareAiWidget({
shareId,
pageId,
assistantName,
}: ShareAiWidgetProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
@@ -152,6 +158,7 @@ export default function ShareAiWidget({ shareId, pageId }: ShareAiWidgetProps) {
<MessageList
messages={messages}
isStreaming={isStreaming}
assistantName={assistantName}
showCitations={false}
// Anonymous reader: neutralize internal/relative links in the
// assistant's markdown so internal UUIDs/auth-gated routes don't

View File

@@ -45,6 +45,10 @@ export interface ISharedPage extends IShare {
// Whether the anonymous public-share AI assistant is enabled for the
// workspace (server-resolved). Gates the "Ask AI" widget.
aiAssistant?: boolean;
// Display name of the configured assistant identity (agent role name), used
// to label the public-share chat. Null/absent when no identity is set →
// the widget falls back to the generic "AI agent" label.
aiAssistantName?: string | null;
}
export interface IShareForPage extends IShare {

View File

@@ -79,7 +79,11 @@ export default function SharedPage() {
{/* Anonymous "Ask AI" widget — only when the workspace enables the
public-share assistant (server-resolved flag on /shares/page-info). */}
{data?.aiAssistant && data.share?.id && data.page?.id && (
<ShareAiWidget shareId={data.share.id} pageId={data.page.id} />
<ShareAiWidget
shareId={data.share.id}
pageId={data.page.id}
assistantName={data.aiAssistantName ?? undefined}
/>
)}
</div>
);