mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 01:45:52 +02:00
Collapse the five separate attachment render paths (file-card NodeView,
image NodeView, readonly markdown img/fileCard renderers, AttachmentList
standalone fallback, and the parallel packages/ui/markdown renderer) into
one <Attachment attachment={a} /> dispatcher.
Fixes a P0 visual regression: a PNG attached to a message but not inlined
in the markdown body used to render as a gray "file card" because
getPreviewKind() lacked an "image" branch and image rendering bypassed
the dispatcher entirely. Now every surface routes through <Attachment>,
so the same PNG renders as a real <img> with hover toolbar and
preview-modal everywhere.
Key changes:
- PreviewKind gains "image"; getPreviewKind() detects image/* + common
extensions before the html/text branches (so svg stays image, not text).
- AttachmentPreviewModal gains case "image" (replaces the standalone
ImageLightbox, which is deleted).
- New packages/views/editor/attachment.tsx owns all kind-aware routing
(image | html | file) and dispatches preview modal + download via the
existing useAttachmentPreview / useDownloadAttachment hooks. Subsumes
the deleted AttachmentBlock.
- AttachmentInput.url accepts a forceKind hint so callers that *know*
the structural kind (markdown , Tiptap image node) skip the
filename-based autodetect — fixes a regression where empty or
descriptive alt text would route an image to the file-card chrome.
- Tiptap NodeViews (file-card.tsx, image-view.tsx) shrink to thin
wrappers that forward editor hints (selected, deleteNode, uploading)
to <Attachment>.
- ReadonlyContent and AttachmentList each mount their own
AttachmentDownloadProvider so url → record resolution works outside
ContentEditor's provider.
- packages/ui/markdown gains optional renderImage / renderFileCard slot
props; packages/views/common/markdown.tsx injects <Attachment> into
those slots and threads message attachments through to chat /
skill-file viewers.
- chat-message-list passes message.attachments to every <Markdown> call
site and renders a standalone AttachmentList under each bubble for
attachments not referenced in the body.
Tests: attachment.test.tsx covers 9 scenarios (record image / pdf / html;
url-only image with resolver hit and miss; uploading state; editable
delete; forceKind regression). attachment-preview-modal.test.tsx gains
image-dispatch cases. 652/652 unit tests pass.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import {
|
|
Markdown as MarkdownBase,
|
|
type MarkdownProps as MarkdownBaseProps,
|
|
type RenderMode,
|
|
} from "@multica/ui/markdown";
|
|
import { useConfigStore } from "@multica/core/config";
|
|
import type { Attachment as AttachmentRecord } from "@multica/core/types";
|
|
import { IssueMentionCard } from "../issues/components/issue-mention-card";
|
|
import {
|
|
Attachment as AttachmentRenderer,
|
|
AttachmentDownloadProvider,
|
|
} from "../editor";
|
|
|
|
export type { RenderMode };
|
|
|
|
export interface MarkdownProps extends MarkdownBaseProps {
|
|
/**
|
|
* Attachments associated with the surrounding entity (chat message, skill
|
|
* file). When passed, the renderer resolves inline image / file-card URLs
|
|
* to full attachment records via AttachmentDownloadProvider, unlocking the
|
|
* unified hover toolbar / lightbox / preview-modal behavior used in
|
|
* editor surfaces.
|
|
*/
|
|
attachments?: AttachmentRecord[];
|
|
}
|
|
|
|
/**
|
|
* Default renderMention that delegates to IssueMentionCard for issue mentions
|
|
* and renders a styled span for other mention types.
|
|
*/
|
|
function defaultRenderMention({
|
|
type,
|
|
id,
|
|
}: {
|
|
type: string;
|
|
id: string;
|
|
}): React.ReactNode {
|
|
if (type === "issue") {
|
|
return <IssueMentionCard issueId={id} />;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function renderImage({ src, alt }: { src: string; alt: string }): React.ReactNode {
|
|
return (
|
|
<AttachmentRenderer
|
|
attachment={{
|
|
kind: "url",
|
|
url: src,
|
|
filename: alt,
|
|
// chat / skill markdown `![]()` is structurally an image. Without
|
|
// forceKind, empty/descriptive alt strings would route to the
|
|
// file-card chrome via getPreviewKind autodetect.
|
|
forceKind: "image",
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function renderFileCard({
|
|
href,
|
|
filename,
|
|
}: {
|
|
href: string;
|
|
filename: string;
|
|
}): React.ReactNode {
|
|
return (
|
|
<AttachmentRenderer
|
|
attachment={{ kind: "url", url: href, filename }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* App-level Markdown wrapper. Injects:
|
|
* - IssueMentionCard for issue mentions
|
|
* - cdnDomain from the config store (drives fileCard preprocessing)
|
|
* - unified <Attachment> as the image / file-card renderer
|
|
* - AttachmentDownloadProvider so url → record resolution works inside
|
|
* the injected <Attachment> components
|
|
*/
|
|
export function Markdown(props: MarkdownProps): React.JSX.Element {
|
|
const cdnDomain = useConfigStore((s) => s.cdnDomain);
|
|
const { attachments, ...rest } = props;
|
|
return (
|
|
<AttachmentDownloadProvider attachments={attachments}>
|
|
<MarkdownBase
|
|
renderMention={defaultRenderMention}
|
|
renderImage={renderImage}
|
|
renderFileCard={renderFileCard}
|
|
cdnDomain={cdnDomain}
|
|
{...rest}
|
|
/>
|
|
</AttachmentDownloadProvider>
|
|
);
|
|
}
|
|
|
|
export const MemoizedMarkdown = React.memo(Markdown);
|
|
MemoizedMarkdown.displayName = "MemoizedMarkdown";
|