diff --git a/desktop/src/components/ai/ArtifactPanel.tsx b/desktop/src/components/ai/ArtifactPanel.tsx index 2aa0452..d272001 100644 --- a/desktop/src/components/ai/ArtifactPanel.tsx +++ b/desktop/src/components/ai/ArtifactPanel.tsx @@ -6,9 +6,10 @@ import { Image as ImageIcon, Download, Copy, - ChevronLeft, + ChevronDown, File, } from 'lucide-react'; +import { MarkdownRenderer } from './MarkdownRenderer'; // --------------------------------------------------------------------------- // Types @@ -76,6 +77,7 @@ export function ArtifactPanel({ className = '', }: ArtifactPanelProps) { const [viewMode, setViewMode] = useState<'preview' | 'code'>('preview'); + const [fileMenuOpen, setFileMenuOpen] = useState(false); const selected = useMemo( () => artifacts.find((a) => a.id === selectedId), [artifacts, selectedId] @@ -135,22 +137,59 @@ export function ArtifactPanel({ return (
- {selected.content}
-
- ) : (
-
- {selected.content}
-
- )}
-
{selected.content}
@@ -217,6 +244,37 @@ export function ArtifactPanel({
);
}
+// ---------------------------------------------------------------------------
+// ArtifactContentPreview — renders artifact based on type
+// ---------------------------------------------------------------------------
+
+function ArtifactContentPreview({ artifact }: { artifact: ArtifactFile }) {
+ if (artifact.type === 'markdown') {
+ return ;
+ }
+
+ if (artifact.type === 'code') {
+ return (
+
+ {artifact.language && (
+
+ {artifact.language}
+
+ )}
+
+ {artifact.content}
+
+
+ );
+ }
+
+ return (
+
+ {artifact.content}
+
+ );
+}
+
// ---------------------------------------------------------------------------
// ActionButton
// ---------------------------------------------------------------------------
@@ -243,50 +301,6 @@ function ActionButton({ icon, label, onClick }: { icon: React.ReactNode; label:
);
}
-// ---------------------------------------------------------------------------
-// Simple Markdown preview (no external deps)
-// ---------------------------------------------------------------------------
-
-function MarkdownPreview({ content }: { content: string }) {
- // Basic markdown rendering: headings, bold, code blocks, lists
- const lines = content.split('\n');
-
- return (
-
- {lines.map((line, i) => {
- // Heading
- if (line.startsWith('### ')) {
- return {line.slice(4)}
;
- }
- if (line.startsWith('## ')) {
- return {line.slice(3)}
;
- }
- if (line.startsWith('# ')) {
- return {line.slice(2)}
;
- }
- // Code block (simplified)
- if (line.startsWith('```')) return null;
- // List item
- if (line.startsWith('- ') || line.startsWith('* ')) {
- return {renderInline(line.slice(2))} ;
- }
- // Empty line
- if (!line.trim()) return ;
- // Regular paragraph
- return {renderInline(line)}
;
- })}
-
- );
-}
-
-function renderInline(text: string): React.ReactNode {
- // Bold
- const parts = text.split(/\*\*(.*?)\*\*/g);
- return parts.map((part, i) =>
- i % 2 === 1 ? {part} : part
- );
-}
-
// ---------------------------------------------------------------------------
// Download helper
// ---------------------------------------------------------------------------
diff --git a/desktop/src/components/ai/MarkdownRenderer.tsx b/desktop/src/components/ai/MarkdownRenderer.tsx
new file mode 100644
index 0000000..4cac15e
--- /dev/null
+++ b/desktop/src/components/ai/MarkdownRenderer.tsx
@@ -0,0 +1,123 @@
+/**
+ * MarkdownRenderer — shared Markdown rendering with styled components.
+ *
+ * Extracted from StreamingText.tsx so ArtifactPanel and other consumers
+ * can reuse the same rich rendering (GFM tables, syntax blocks, etc.)
+ * without duplicating the component overrides.
+ */
+
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import type { Components } from 'react-markdown';
+
+// ---------------------------------------------------------------------------
+// Shared component overrides for react-markdown
+// ---------------------------------------------------------------------------
+
+export const markdownComponents: Components = {
+ pre({ children }) {
+ return (
+
+ {children}
+
+ );
+ },
+ code({ className, children, ...props }) {
+ const isBlock = className?.startsWith('language-');
+ if (isBlock) {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+ },
+ table({ children }) {
+ return (
+
+
+ {children}
+
+
+ );
+ },
+ thead({ children }) {
+ return {children};
+ },
+ th({ children }) {
+ return (
+
+ {children}
+
+ );
+ },
+ td({ children }) {
+ return (
+
+ {children}
+
+ );
+ },
+ ul({ children }) {
+ return + {children} ++ ); + }, + p({ children }) { + return
{children}
; + }, + a({ href, children }) { + return ( + + {children} + + ); + }, + hr() { + return
- {children}
-
- );
- },
- // Inline code (`...`)
- code({ className, children, ...props }) {
- // If it has a language class, it's inside a code block — render as block
- const isBlock = className?.startsWith('language-');
- if (isBlock) {
- return (
-
- {children}
-
- );
- }
- return (
-
- {children}
-
- );
- },
- // Tables
- table({ children }) {
- return (
- - {children} -- ); - }, - // Paragraphs - p({ children }) { - return
{children}
; - }, - // Links - a({ href, children }) { - return ( - - {children} - - ); - }, - // Horizontal rules - hr() { - return