Feature/superchat shared components#274
Open
garrity-miepub wants to merge 46 commits into
Open
Conversation
Add a native SuperChat module that composes the AI module's renderTextContent seam (Decisions 1-3 of superchat-plan.md): - Participant model (human/agent/system) generalizing AI + chat-component roles; chat-component-compatible conversation/thread/ref/linkBuilder shape - createMarkdownRenderer: composes render plugins into one renderTextContent; Markdown core (remark-gfm) with rehype-sanitize on untrusted output - Opt-in rich plugins behind a subpath: code (rehype-highlight + copy), math (KaTeX), genui (fenced JSON -> host-registered, lazy, schema-validated widget registry with component-vs-data prefetch) - SuperChat shell: sidebar/thread/composer, controlled props, timestamp-ordered interleaved replies, per-participant color/avatar cues, read-only - Composer @-mention menu (keyboard + mouse) and password-manager opt-out attrs - Stories (Markdown core / rich plugins / read-only), MAINTAINERS.md, 9 tests - Wire package.json optional peers + tsup subpath entries; not in base bundle
…plugins (Milestone 4) - mermaid: lazy-loaded strict-mode diagram rendering for fenced mermaid blocks - image: click-to-zoom Markdown images via Messaging LightboxModal (portaled to body) - nitro-table: GFM tables through DataVis NITRO grid, lazy-loaded, degrades to themed HTML table via GridErrorBoundary when datavis is unavailable - wire plugins into plugins/index.ts, tsup externals, and optional mermaid peer dep - add rich-plugins story + 3 tests (12 total), update MAINTAINERS.md
…ource + trust boundary
react-i18next → html-parse-stringify → void-elements live only in the pnpm virtual store, so Vite served void-elements raw without CJS→ESM default-export interop, breaking the datavis NITRO grid (GridErrorBoundary). Alias and force-include the chain in optimizeDeps, resolved against the workspace-local .pnpm/node_modules store.
… README A11y: landmark roles and accessible names on every structural region — group root, complementary sidebar, labelled main section, participants group, role=log live message thread, per-message article labels, and a proper mention combobox wired to its listbox. Add semantic data-slot hooks (header/thread/composer/bubble/meta/etc.) for styling and tests. Docs: new consumer README with a getting-started guide, a vocabulary glossary mapping each subcomponent term to its data-slot/role/name, an annotated visual-layout diagram, and cross-references to the AI, Messaging, and standalone chat-component surfaces. Point MAINTAINERS at it.
…ns, and SuperChatInbox
Break the monolithic SuperChat (sidebar + main panel) into three composable
components sharing one folder and one import path
(@mieweb/ui/components/SuperChat):
- SuperChat — single-conversation panel (takes one `conversation`);
root data-slot="superchat".
- SuperChatConversations — the conversation list (sidebar) with controlled/
uncontrolled selection; root data-slot="superchat-conversations".
- SuperChatInbox — wrapper composing the two; accepts the original full
SuperChatProps so it is a drop-in for the old component;
root data-slot="superchat-inbox".
Shared presentational pieces and helpers (ParticipantAvatar, ReferenceChip,
MessageRow, Composer, sidebarItem, time/mention utils) move to an internal
parts.tsx (not exported). No package.json/tsup change — the existing single
entry + ./components/* wildcard cover the new exports.
Stories: split per component (Panel / Conversations / Inbox) with shared
sample data in storyData.tsx; add args-driven Playground stories so the
Controls panel is populated. Tests: retarget panel tests to a single
conversation and add SuperChatConversations + SuperChatInbox coverage
(21 passing). Docs: README + MAINTAINERS remapped to the three-component
model and new data-slot vocabulary.
…tories - Add SuperChat.mdx Overview page that renders README.md verbatim (?raw), rewriting relative .md links to absolute GitHub URLs so they stay clickable in Storybook while remaining correct on GitHub/npm. - Trim duplicated component descriptions in the Panel/Inbox story metas to short blurbs that defer to the Overview (DRY). - Enable the full render-plugin set on the Playground stories so math/code/ GenUI/mermaid/image/table render; rename Panel 'MarkdownCore' -> 'CoreNoPlugins' (the lone plugin-less baseline, with an explanatory note). - Drop redundant stories (WithRichPlugins, Panel ReadOnly/Closable, Inbox Default/ReadOnly); keep Inbox Sources & Guards security reference.
… table contrast - Repoint code-badge and doc-link colors to injected --mieweb-primary-400 and hover backgrounds to --mieweb-muted (the previously referenced --mieweb-primary / --mieweb-accent tokens are never injected, so they always fell back to hardcoded blue/grey regardless of brand). - Add dark-mode prose-table rules for Markdown/README docs using injected --mieweb-foreground/--mieweb-card/--mieweb-border tokens. - Manager brand-theme switcher button now uses brand.primary so it reflects the active brand instead of the muted toolbar grey.
Move the design rationale (goal, background, the 3 decisions incl. rejected alternatives, resolved decisions) from the top-level superchat-plan.md into src/components/SuperChat/MAINTAINERS.md under a new Mission section, reframed from a plan to a mission now that the module is implemented. Update the README/types/MDX cross-references and delete superchat-plan.md.
# Conflicts: # pnpm-lock.yaml # tsup.config.ts
- detectMentions: match whole mention tokens via word-boundary regex so @triage no longer matches @TriageAgent (also escapes regex metachars). - ParticipantAvatar: mark the avatar <img> decorative (alt="") since the name is already exposed by surrounding face-pile / message-meta labels, avoiding double screen-reader announcement. - SuperChat panel: drop redundant aria-label so the region is labelled by its visible <h2> via aria-labelledby (aria-label was overriding it). - SuperChatInbox/SuperChatConversations: resolve the active id from a conversation that actually exists so the sidebar highlight and panel stay consistent when the active conversation is removed. - README: clarify the base SuperChat entry requires the three Markdown-core peers (react-markdown/remark-gfm/rehype-sanitize) rather than being dependency-free.
- Composer Enter/Tab mention insert now falls back to the first suggestion when the highlighted index is out of range (suggestions shrank while the menu was open), avoiding passing undefined into insertMention. - Add the primary-* / arbitrary utilities introduced by SuperChat (participant chips, unread badge, active/hover/focus states) to miewebUISafelist so Tailwind 3 consumers that don't scan node_modules still generate the styles.
…l screens On viewports below the `sm` breakpoint the w-64 conversation list and the chat panel no longer fit side by side. SuperChatInbox now uses a master-detail pattern on mobile: the list and panel each take the full width and only one is shown at a time. Selecting a conversation reveals the chat; a new mobile-only back button in the SuperChat header returns to the list. At `sm`+ both panes remain visible side by side as before.
…ge whitespace The story decorators used `height: 90vh`, which in Docs view resolves against the tall docs iframe and produced ~800px-tall `.docs-story` blocks with large empty space below the chat. Cap with `min(90vh, 600px)` so standalone story canvases still feel full-height on short/mobile viewports while the embedded Docs previews stay a sensible fixed height.
- Wrap MessageRow in React.memo so appending a message only renders the new
row; existing rows (and their Markdown parses) are skipped, cutting the
re-render/re-parse cost on long threads.
- Add an `order` prop ('asc' default | 'desc') to SuperChat and SuperChatInbox.
'desc' renders newest-first (social-feed style) and anchors scroll to the top;
'asc' keeps the classic messenger bottom-anchored behavior.
- Document performance guidance for long conversations (cap/paginate thread)
and the new order prop in the README; add a Storybook control and a unit test.
Adds a SuperChat panel story demonstrating order="desc" — the social-feed layout where the most recent message leads. Same conversation and plugins as the Playground.
Synthetic 300-message thread to exercise long-history rendering, scroll anchoring, and asc/desc ordering. Long uses order='asc' (bottom-anchored); Long Reverse uses order='desc' (top-anchored feed).
Add a `virtualized` prop (default false) to SuperChat/SuperChatInbox that windows the message thread with @tanstack/react-virtual. Only rows near the viewport mount (with dynamic height measurement), bounding first render, memory, and Markdown parse cost by what's on screen. Scroll anchoring is handled per order (bottom for asc, top for desc). - New VirtualThread component (headless virtualizer over orderedThread) - Thread virtualized through SuperChatInbox - Long/LongReverse stories enable virtualized; add argType control - README Performance section documents the prop - Test covers virtualized mount
Add an opt-in `onMessageEdited` callback to SuperChat/SuperChatInbox. When
provided (and not readOnly), self-authored plain-text messages show an inline
Edit pencil that swaps the bubble for a textarea with Save/Cancel (Enter saves,
Escape cancels). Saving fires onMessageEdited(messageId, text, { conversation });
the host applies it and can stamp the new optional `editedAt` field to surface
an "(edited)" indicator.
- New editedAt field on SuperChatMessage
- MessageRow inline editor (memo-safe: stable handler via refs)
- Threaded through VirtualThread and SuperChatInbox
- InteractivePanel story wires editing; README documents it
- 6 new tests (28 total pass)
The edit textarea sized from newline count only, so a wrapped single-line message showed a one-row (too short) box. Grow it to scrollHeight on open and on input (accounting for wrapped lines), with a taller min/max height.
Markdown collapses single newlines into spaces, so multi-line chat messages lost their line breaks. Add a self-contained remark transform (no extra unist/mdast deps) that splits text nodes on \n into hard `break` nodes, matching messaging-app expectations. Code blocks are untouched (not text nodes); <br> is already allow-listed by the sanitize schema.
The 'prose' classes are inert here (no @tailwindcss/typography plugin), so Tailwind preflight's list reset left <ul>/<ol> with no bullets/numbers. Style the ul/ol/li node components explicitly (list-disc/list-decimal + padding) so markers render independent of the typography plugin.
…kdown / plain) Every content message gets a margin copy control (left for incoming, right for own) that appears on hover/focus. The primary Copy writes both text/html and text/plain in one clipboard write so the paste target decides; explicit 'Copy as Markdown' and 'Copy as plain text' options are also offered.
… bottom On long messages the copy affordance now sticks to the bottom of the viewport (self-aligned to the message's end) so it stays reachable while scrolling and settles at the message bottom once fully in view. The menu opens upward to suit the low anchor.
Make the AI chat surface the source of truth for shared chat UI and de-duplicate SuperChat against it: - Extract ChatBubble shell (with accent + variant) in AI/AIMessage and export AITypingIndicator/ChatBubble for reuse - SuperChat ReferenceChip refType tag now renders via shared Badge - Wire AITypingIndicator into SuperChat streaming messages (parity with AIMessageDisplay) - Rewrite SuperChat CopyMenu on shared Dropdown/DropdownItem; drop the bespoke menu shell, outside-click/Escape effect, and CopyMenuItem - Add upward placement (top/top-start/top-end) to Dropdown so the copy menu opens above its trigger - Replace SuperChat composer with shared Messaging MessageComposer
There was a problem hiding this comment.
Pull request overview
This PR introduces a new SuperChat feature module and refactors chat UI to reuse shared AI chat primitives, with a pluggable Markdown rendering pipeline (core + opt-in rich plugins), improved performance options (virtualized threads), and updated shared components to support the new surface.
Changes:
- Added
components/SuperChatsubpath exports (panel, inbox, conversation list) plus internal types, rendering, virtualization, and Storybook/README documentation. - Added opt-in rich Markdown plugins (code, math/KaTeX, GenUI widgets, Mermaid, images/lightbox, NITRO DataVis tables, attachments + offline cache).
- Enhanced shared components/utilities to support SuperChat (Dropdown top placements, AI
ChatBubbleexport, Messaging composer mentions + paste-to-attach) and updated build/peer dependency configuration.
Reviewed changes
Copilot reviewed 40 out of 41 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tsup.config.ts | Adds tsup entry points for SuperChat and externalizes Markdown/plugin deps. |
| src/tailwind-preset.ts | Extends Tailwind safelist for new SuperChat state styles. |
| src/components/SuperChat/VirtualThread.tsx | Introduces optional windowed rendering for long message threads. |
| src/components/SuperChat/types.ts | Defines participant/conversation/thread model + plugin/GenUI contracts. |
| src/components/SuperChat/SuperChatInbox.tsx | New combined inbox surface (list + panel) with mobile master-detail behavior. |
| src/components/SuperChat/SuperChatInbox.stories.tsx | Storybook stories for inbox + security “Sources & Guards” documentation demo. |
| src/components/SuperChat/SuperChatConversations.tsx | New conversation list component with unread badge + sorting. |
| src/components/SuperChat/SuperChatConversations.stories.tsx | Storybook coverage for conversation list, incl. controlled/uncontrolled examples. |
| src/components/SuperChat/SuperChat.tsx | New conversation panel composed from shared MessageComposer + shared bubble shell. |
| src/components/SuperChat/SuperChat.stories.tsx | Storybook scenarios for ordering, virtualization, and plugin-enabled rendering. |
| src/components/SuperChat/SuperChat.mdx | Storybook overview page rendering README with link rewriting. |
| src/components/SuperChat/storyData.tsx | Story fixtures and example GenUI widget registry. |
| src/components/SuperChat/render/renderContext.ts | Provides render-time context for plugins/widgets (messageId/streaming). |
| src/components/SuperChat/render/createMarkdownRenderer.tsx | Composes remark/rehype plugins + sanitization into renderTextContent. |
| src/components/SuperChat/render/attachmentCache.ts | Adds IndexedDB-backed offline attachment blob cache with LRU eviction. |
| src/components/SuperChat/render/attachmentCache.test.ts | Adds Vitest coverage for attachment cache behavior (incl. eviction). |
| src/components/SuperChat/README.md | Consumer guide for installing/using SuperChat and its plugins. |
| src/components/SuperChat/plugins/nitroTableGrid.tsx | Lazy chunk for rendering GFM tables via DataVis NITRO grid. |
| src/components/SuperChat/plugins/nitroTable.tsx | Table plugin with graceful fallback to themed HTML tables. |
| src/components/SuperChat/plugins/mermaid.tsx | Mermaid plugin with lazy load + strict securityLevel rendering. |
| src/components/SuperChat/plugins/math.tsx | Math plugin wiring remark-math + rehype-katex with sanitize allowlist. |
| src/components/SuperChat/plugins/index.ts | Public exports for rich plugins + attachment cache/types. |
| src/components/SuperChat/plugins/image.tsx | Image plugin adding click-to-zoom via Messaging LightboxModal. |
| src/components/SuperChat/plugins/genui.tsx | GenUI widget plugin with registry-based lazy loading + schema validation. |
| src/components/SuperChat/plugins/code.tsx | Code plugin with rehype-highlight and an inline copy affordance. |
| src/components/SuperChat/plugins/attachment.tsx | Attachment plugin rendering inline media blocks and integrating offline cache. |
| src/components/SuperChat/parts.tsx | Shared internal UI pieces (rows, copy menu, reference chip, styling helpers). |
| src/components/SuperChat/MAINTAINERS.md | Maintainer documentation + architecture and security notes. |
| src/components/SuperChat/index.ts | Public API exports for SuperChat surfaces and types. |
| src/components/Messaging/MessageComposer.tsx | Adds mention autocomplete + paste-to-attach support used by SuperChat. |
| src/components/Messaging/index.ts | Re-exports MentionOption type. |
| src/components/Dropdown/Dropdown.tsx | Adds top* placement support for menus opening upward. |
| src/components/AI/index.ts | Re-exports shared ChatBubble + its props for reuse. |
| src/components/AI/AIMessage.tsx | Extracts ChatBubble presentational shell from AI message rendering. |
| package.json | Adds peers/dev deps needed for SuperChat Markdown + plugins + virtualization + tests. |
| eslint.config.js | Adds IndexedDB globals for linting. |
| .storybook/preview.css | Improves dark-mode readability of Markdown tables in docs. |
| .storybook/manager.ts | Styles brand theme switcher in Storybook toolbar. |
| .storybook/main.ts | Adds pnpm-virtual-store CJS interop handling for Storybook/Vite prebundling. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (1)
src/components/Messaging/MessageComposer.tsx:521
- The mention menu state is only recalculated on
onChange/onClick. If the caret moves without changing text (ArrowLeft/ArrowRight/Home/End), the menu can remain open even though the caret is no longer in an@query. Consider syncing/closing the mention state after caret-navigation keys so the picker tracks the caret position.
// Send on Enter (without Shift)
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (canSend) {
handleSubmit(event);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…aria clamp) - attachment.tsx: sanitize attachment payload `src` via a scheme allowlist (blob:/http(s):/data:) and reject data: MIME types a browser executes as an active document (text/html, image/svg+xml, xml). The payload bypasses rehype-sanitize, so this closes a javascript:/data:text/html XSS vector in the PDF iframe and download anchors. Generic file downloads (zip, etc.) still work. - MessageComposer.tsx: clamp the mention highlight index when computing aria-activedescendant so it never points at an out-of-range option id when the suggestion list shrinks.
Deploying ui with
|
| Latest commit: |
5d5a07f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7f2aa533.ui-6d0.pages.dev |
| Branch Preview URL: | https://feature-superchat-shared-com.ui-6d0.pages.dev |
The per-participant accent border was visually distracting. The participant color is still used for the author name; the bubble itself is now plain. The ChatBubble `accent` prop remains for future use.
- MessageComposer: paste-to-attach now matches .ext accept tokens (.pdf/ .doc/.docx) against the file name via isFileAccepted, mirroring the picker/drag-drop native <input accept> behavior. Previously pasted PDFs/ Docs were silently rejected because extension tokens were ignored. - attachmentCache.put: open IndexedDB before converting the data URL so SSR/ non-browser callers bail out without touching window.atob/window.Blob, honoring the module's 'safe in any environment' contract. Close the db handle on the unusable-blob early return.
- lastActivityOf now uses lastMessageByTime(thread) instead of the last array element, so inbox sorting/highlighting is correct even when the host provides an unsorted or backfilled thread. - CopyMenu.writeText rejects when the Clipboard API is unavailable (insecure context / old browser) instead of silently resolving, so the UI no longer flashes a false 'Copied'. - CopyMenu.run only flashes on a fulfilled write and swallows rejections, avoiding an unhandled promise rejection on failed clipboard writes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
refactor(superchat): reuse shared AI chat components (AI chat as source of truth)
Summary
De-duplicates SuperChat against the AI chat surface by making AI chat the single source of truth for shared chat UI. SuperChat previously carried its own hand-rolled bubble, composer, copy menu, and reference-tag styling that drifted from the equivalent AI chat components. This PR extracts the reusable shells into the AI/shared layer and rewires SuperChat to consume them.
Net result: +461 / −662 lines (9 files) — less bespoke code, more shared surface, and one genuine new feature (typing indicator parity).
What changed
Shared layer (the new source of truth)
ChatBubbleshell withvariant(user/assistant/etc.) and an optionalaccentprop for multi-participant tinting. ExportedChatBubble,ChatBubbleProps, andAITypingIndicatorfor reuse.top,top-start,top-end) alongside the existingbottom-*placements, so menus anchored near the bottom of a panel can open above their trigger.SuperChat (now a consumer)
refTypetag now renders via the sharedBadgecomponent instead of a hand-styledspan.AITypingIndicatorinto streaming messages, matchingAIMessageDisplay(shows the indicator only while streaming and no body content yet). SuperChat previously had no typing affordance — this is added parity.Dropdown/DropdownItem. Deleted the bespoke menu shell, the custom outside-click/EscapeuseEffect, and theCopyMenuItemhelper. Preserved behavior: dual-clipboard write (text/html+text/plainviaClipboardItemwith fallbacks) and the copied-flash state. Menu opens upward (top-endfor self,top-startotherwise) using the new Dropdown placement.MessagingMessageComposer.Why
SuperChat and AI chat were maintaining parallel implementations of the same chat primitives. Each fix or design tweak had to be applied twice and routinely fell out of sync. Consolidating on the AI/shared components removes the duplication, guarantees visual/behavioral consistency, and gives both surfaces upstream improvements for free.
Notes / non-goals
CountBadgeis a heavy labeled workflow component, a poor fit for the bare unread bubble.Verification
tsc --noEmit(full project) — clean