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:
@@ -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",
|
||||||
|
|||||||
@@ -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...": "Задайте вопрос...",
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user