CoDEVX / components /MarkdownContent.tsx
CodexMacTiger
feat: live package-scoped chat and thinking logs
837e3ac
"use client";
import type { ReactNode } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { HelpCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
function nodeText(value: ReactNode): string {
if (typeof value === "string" || typeof value === "number") {
return String(value);
}
if (Array.isArray(value)) {
return value.map((item) => nodeText(item)).join("");
}
if (value && typeof value === "object" && "props" in value) {
return nodeText((value as { props?: { children?: ReactNode } }).props?.children);
}
return "";
}
export function MarkdownContent(props: {
content: string;
onAskExcerpt?: (excerpt: string) => void;
}) {
const { content, onAskExcerpt } = props;
return (
<div className="markdown-content text-sm text-foreground">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={
onAskExcerpt
? {
p({ children }) {
const excerpt = nodeText(children).trim();
return (
<div className="mb-4 space-y-1.5 last:mb-0">
<p>{children}</p>
{excerpt ? (
<Button
type="button"
variant="ghost"
className="h-7 rounded-lg px-2 text-[11px] text-muted-foreground"
onClick={() => onAskExcerpt(excerpt)}
>
<HelpCircle className="h-3.5 w-3.5" />
<span className="ml-1">Ask about this paragraph</span>
</Button>
) : null}
</div>
);
},
li({ children }) {
const excerpt = nodeText(children).trim();
return (
<li>
<div className="space-y-1.5">
<div>{children}</div>
{excerpt ? (
<Button
type="button"
variant="ghost"
className="h-7 rounded-lg px-2 text-[11px] text-muted-foreground"
onClick={() => onAskExcerpt(excerpt)}
>
<HelpCircle className="h-3.5 w-3.5" />
<span className="ml-1">Ask about this item</span>
</Button>
) : null}
</div>
</li>
);
},
}
: undefined
}
>
{content}
</ReactMarkdown>
</div>
);
}