Skip to content

Feature/superchat shared components#274

Open
garrity-miepub wants to merge 46 commits into
mainfrom
feature/superchat-shared-components
Open

Feature/superchat shared components#274
garrity-miepub wants to merge 46 commits into
mainfrom
feature/superchat-shared-components

Conversation

@garrity-miepub

Copy link
Copy Markdown
Contributor

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)

  • AI/AIMessage — Extracted a presentational ChatBubble shell with variant (user/assistant/etc.) and an optional accent prop for multi-participant tinting. Exported ChatBubble, ChatBubbleProps, and AITypingIndicator for reuse.
  • Dropdown — Added upward placement support (top, top-start, top-end) alongside the existing bottom-* placements, so menus anchored near the bottom of a panel can open above their trigger.

SuperChat (now a consumer)

  • ReferenceChip — The refType tag now renders via the shared Badge component instead of a hand-styled span.
  • Streaming — Wired AITypingIndicator into streaming messages, matching AIMessageDisplay (shows the indicator only while streaming and no body content yet). SuperChat previously had no typing affordance — this is added parity.
  • CopyMenu — Rewritten on top of the shared Dropdown / DropdownItem. Deleted the bespoke menu shell, the custom outside-click/Escape useEffect, and the CopyMenuItem helper. Preserved behavior: dual-clipboard write (text/html + text/plain via ClipboardItem with fallbacks) and the copied-flash state. Menu opens upward (top-end for self, top-start otherwise) using the new Dropdown placement.
  • Composer — Replaced SuperChat's bespoke composer with the shared Messaging MessageComposer.

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

  • AI chat is untouched behaviorally — only additive extraction/exports.
  • Unread count pill left bespoke intentionally: the existing CountBadge is a heavy labeled workflow component, a poor fit for the bare unread bubble.
  • SuperChat remains a walled-off feature module; no subpath/package promotion in this PR.

Verification

  • ✅ ESLint — clean on all touched components
  • tsc --noEmit (full project) — clean
  • ✅ Prettier — clean
  • ✅ Vitest — 75 passing across SuperChat (43), AI, Messaging, Dropdown, Badge
  • ✅ Visual check in Storybook (reference chip, copy dropdown opening upward, bubble parity)

horner and others added 30 commits June 13, 2026 14:11
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
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.
horner and others added 12 commits June 13, 2026 22:54
…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
Copilot AI review requested due to automatic review settings June 15, 2026 22:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/SuperChat subpath 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 ChatBubble export, 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.

Comment thread src/components/SuperChat/plugins/attachment.tsx
Comment thread src/components/SuperChat/parts.tsx
Comment thread src/components/SuperChat/parts.tsx
Comment thread src/components/SuperChat/parts.tsx
Comment thread src/components/SuperChat/SuperChatInbox.stories.tsx
Comment thread src/components/Messaging/MessageComposer.tsx
…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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 15, 2026

Copy link
Copy Markdown

Deploying ui with  Cloudflare Pages  Cloudflare Pages

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

View logs

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.
Copilot AI review requested due to automatic review settings June 15, 2026 23:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 41 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread src/components/Messaging/MessageComposer.tsx
Comment thread src/components/SuperChat/render/attachmentCache.ts
- 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.
@garrity-miepub garrity-miepub marked this pull request as ready for review June 16, 2026 01:13
Copilot AI review requested due to automatic review settings June 16, 2026 01:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 41 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread src/components/SuperChat/parts.tsx
Comment thread src/components/SuperChat/parts.tsx
Comment thread src/components/SuperChat/parts.tsx
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants