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

@@ -1143,6 +1143,7 @@
"Current context size": "Current context size", "Current context size": "Current context size",
"AI agent": "AI agent", "AI agent": "AI agent",
"AI agent is typing…": "AI agent is typing…", "AI agent is typing…": "AI agent is typing…",
"{{name}} is typing…": "{{name}} is typing…",
"Send": "Send", "Send": "Send",
"Stop": "Stop", "Stop": "Stop",
"Chat menu": "Chat menu", "Chat menu": "Chat menu",

View File

@@ -666,6 +666,7 @@
"AI search": "Поиск ИИ", "AI search": "Поиск ИИ",
"AI Answer": "Ответ ИИ", "AI Answer": "Ответ ИИ",
"Ask AI": "Спросить ИИ", "Ask AI": "Спросить ИИ",
"{{name}} is typing…": "{{name}} печатает…",
"AI is thinking...": "ИИ обрабатывает запрос...", "AI is thinking...": "ИИ обрабатывает запрос...",
"Thinking": "Думаю", "Thinking": "Думаю",
"Ask a question...": "Задайте вопрос...", "Ask a question...": "Задайте вопрос...",

View File

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

View File

@@ -30,6 +30,12 @@ interface MessageListProps {
* UUIDs/routes don't leak as clickable links to anonymous readers. * UUIDs/routes don't leak as clickable links to anonymous readers.
*/ */
neutralizeInternalLinks?: boolean; 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 // Distance (px) from the bottom within which the viewport still counts as
@@ -67,6 +73,7 @@ export default function MessageList({
emptyState, emptyState,
showCitations = true, showCitations = true,
neutralizeInternalLinks = false, neutralizeInternalLinks = false,
assistantName,
}: MessageListProps) { }: MessageListProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const viewportRef = useRef<HTMLDivElement>(null); const viewportRef = useRef<HTMLDivElement>(null);
@@ -148,9 +155,10 @@ export default function MessageList({
message={message} message={message}
showCitations={showCitations} showCitations={showCitations}
neutralizeInternalLinks={neutralizeInternalLinks} neutralizeInternalLinks={neutralizeInternalLinks}
assistantName={assistantName}
/> />
))} ))}
{typing && <TypingIndicator />} {typing && <TypingIndicator assistantName={assistantName} />}
</Stack> </Stack>
</ScrollArea> </ScrollArea>
); );

View File

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

View File

@@ -27,6 +27,8 @@ interface ShareAiWidgetProps {
shareId: string; shareId: string;
/** The page the reader currently has open (context for "this page"). */ /** The page the reader currently has open (context for "this page"). */
pageId: string; 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 * links (so internal UUIDs/auth-gated routes in the answer don't leak as
* clickable links), and a documentation-focused empty state. * 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 { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
@@ -152,6 +158,7 @@ export default function ShareAiWidget({ shareId, pageId }: ShareAiWidgetProps) {
<MessageList <MessageList
messages={messages} messages={messages}
isStreaming={isStreaming} isStreaming={isStreaming}
assistantName={assistantName}
showCitations={false} showCitations={false}
// Anonymous reader: neutralize internal/relative links in the // Anonymous reader: neutralize internal/relative links in the
// assistant's markdown so internal UUIDs/auth-gated routes don't // 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 // Whether the anonymous public-share AI assistant is enabled for the
// workspace (server-resolved). Gates the "Ask AI" widget. // workspace (server-resolved). Gates the "Ask AI" widget.
aiAssistant?: boolean; 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 { 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 {/* Anonymous "Ask AI" widget — only when the workspace enables the
public-share assistant (server-resolved flag on /shares/page-info). */} public-share assistant (server-resolved flag on /shares/page-info). */}
{data?.aiAssistant && data.share?.id && data.page?.id && ( {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> </div>
); );