feat(share): public-share AI chat reuses internal chat presentation (#41) #51
Reference in New Issue
Block a user
Delete Branch "feat/share-chat-reuse-internal"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Реализация #41 — публичный share-чат переиспользует отлаженный презентационный слой внутреннего чата (стриминг, typing, markdown, tool-cards). Ветка от develop. Closes #41.
Было
Внешний виджет (
share-ai-widget.tsx) — отдельная минимальная реализация: ответ plain-text, статичное «Thinking…», без markdown и tool-cards, не переиспользовал ничего изfeatures/ai-chat.Стало
Share рендерит через общий
MessageList/MessageItem/TypingIndicator/ToolCallCard→ тот же инкрементальный стриминг, анимированный typing-индикатор до первых токенов, markdown и карточки инструментов, что и внутри. Транспорт share не тронут (анонимныйuseChat+DefaultChatTransport('/api/shares/ai/stream', credentials:'omit')).Как
Внутренние компоненты уже были prop-driven (
UIMessage[]+isStreaming), без привязки к transport/auth. Новые пропсы (emptyState,showCitations,neutralizeInternalLinks) — аддитивные опциональные с дефолтами «как сейчас», поэтому внутренний чат не изменён (подтверждено ревью:ChatThreadзовётMessageListбез новых пропсов).Безопасность (CRITICAL, пойман ревью)
Рендер markdown ассистента на АНОНИМНОМ share делал кликабельными внутренние ссылки (
/p/{id},/settings/...) — в старом plain-text они были инертны. Фикс:renderChatMarkdownполучилneutralizeInternalLinks(true только на share): одноразовый DOMPurify-хукafterSanitizeAttributes(add/remove по ссылке вокруг одного sanitize, вfinally) снимаетhrefу внутренних/относительных/не-http(s) ссылок (инертный текст), внешние http(s) сохраняет сrel=noopener noreferrer nofollow target=_blank. Тесты покрывают и нейтрализацию (включаяjavascript:/data:/#///), и отсутствие утечки глобального хука во внутренние рендеры. Второе ревью — APPROVE.Проверка
tsc --noEmit— exit 0. vitest — 11/11 (markdown + typing-indicator).Замечание ревью (неблокирующее): стоит визуально проверить ширину/высоту обёртки MessageList в поповере share (360x480) — это share-only chrome, внутренний чат не затрагивает.
🤖 Generated with Claude Code
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>Ghost referenced this pull request2026-06-21 02:04:59 +03:00