Commit 1c05548e authored by Administrator's avatar Administrator

Update frontend/src/components/MessageBubble.jsx via Son of Anton

parent c324caf4
...@@ -2,8 +2,9 @@ import React, { useState, useMemo, useCallback } from "react"; ...@@ -2,8 +2,9 @@ import React, { useState, useMemo, useCallback } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import CodeBlock from "./CodeBlock"; import CodeBlock from "./CodeBlock";
import UIPreview, { buildPreviewHTML, isPreviewable } from "./UIPreview";
import { getAttachmentUrl, extractCodeBlocks, commitFromChat, exportPptx, exportDocx } from "../api"; import { getAttachmentUrl, extractCodeBlocks, commitFromChat, exportPptx, exportDocx } from "../api";
import { User, Flame, ChevronDown, ChevronRight, Brain, Copy, Check, Image, Film, FileText, ExternalLink, GitCommitVertical, Loader2, Presentation, FileOutput } from "lucide-react"; import { User, Flame, ChevronDown, ChevronRight, Brain, Copy, Check, Image, Film, FileText, ExternalLink, GitCommitVertical, Loader2, Presentation, FileOutput, Eye } from "lucide-react";
const FILE_TYPE_ICONS = { image: Image, video: Film, document: FileText, text: FileText }; const FILE_TYPE_ICONS = { image: Image, video: Film, document: FileText, text: FileText };
...@@ -16,6 +17,7 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, ...@@ -16,6 +17,7 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
const [batchCommitting, setBatchCommitting] = useState(false); const [batchCommitting, setBatchCommitting] = useState(false);
const [batchDone, setBatchDone] = useState(false); const [batchDone, setBatchDone] = useState(false);
const [exportingType, setExportingType] = useState(""); const [exportingType, setExportingType] = useState("");
const [showUIPreview, setShowUIPreview] = useState(false);
const handleCopy = useCallback(() => { navigator.clipboard.writeText(content || ""); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [content]); const handleCopy = useCallback(() => { navigator.clipboard.writeText(content || ""); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [content]);
...@@ -24,6 +26,19 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, ...@@ -24,6 +26,19 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
return extractCodeBlocks(content).filter(b => b.filename); return extractCodeBlocks(content).filter(b => b.filename);
}, [content, isUser, linkedRepo]); }, [content, isUser, linkedRepo]);
// Check if this message has previewable UI code
const codeBlocks = useMemo(() => {
if (isUser || !content) return [];
return extractCodeBlocks(content);
}, [content, isUser]);
const previewable = useMemo(() => isPreviewable(codeBlocks), [codeBlocks]);
const previewHTML = useMemo(() => {
if (!previewable) return null;
return buildPreviewHTML(codeBlocks);
}, [previewable, codeBlocks]);
async function handleBatchCommit() { async function handleBatchCommit() {
if (!committableBlocks.length || !linkedRepo || !chatId) return; if (!committableBlocks.length || !linkedRepo || !chatId) return;
const msg = prompt(`Commit ${committableBlocks.length} file(s) to ${linkedRepo.name}/${linkedRepo.default_branch}:`, `Update ${committableBlocks.length} files via Son of Anton`); const msg = prompt(`Commit ${committableBlocks.length} file(s) to ${linkedRepo.name}/${linkedRepo.default_branch}:`, `Update ${committableBlocks.length} files via Son of Anton`);
...@@ -88,6 +103,25 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, ...@@ -88,6 +103,25 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
)} )}
</div> </div>
{/* Preview UI Card — shown when message has previewable code */}
{!isUser && !isStreaming && previewable && previewHTML && (
<button
onClick={() => setShowUIPreview(true)}
className="mt-2 w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-blue-500/30 bg-blue-500/5 hover:bg-blue-500/10 hover:border-blue-500/50 transition group"
>
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center shrink-0 group-hover:bg-blue-500/30 transition">
<Eye size={18} className="text-blue-400" />
</div>
<div className="text-left min-w-0">
<p className="text-sm text-white font-medium">Preview UI Design</p>
<p className="text-[10px] text-blue-400/70">
{codeBlocks.length} code block{codeBlocks.length > 1 ? "s" : ""} • Click to open live preview
</p>
</div>
<Eye size={16} className="text-blue-400/50 group-hover:text-blue-400 transition ml-auto shrink-0" />
</button>
)}
{!isUser && !isStreaming && content && ( {!isUser && !isStreaming && content && (
<div className="flex items-center gap-2 sm:gap-3 mt-1.5 px-1 flex-wrap"> <div className="flex items-center gap-2 sm:gap-3 mt-1.5 px-1 flex-wrap">
<button onClick={handleCopy} className="flex items-center gap-1 text-[10px] text-anton-muted hover:text-white transition">{copied ? <Check size={11} className="text-anton-success" /> : <Copy size={11} />} {copied ? "Copied" : "Copy"}</button> <button onClick={handleCopy} className="flex items-center gap-1 text-[10px] text-anton-muted hover:text-white transition">{copied ? <Check size={11} className="text-anton-success" /> : <Copy size={11} />} {copied ? "Copied" : "Copy"}</button>
...@@ -109,10 +143,19 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming, ...@@ -109,10 +143,19 @@ const MessageBubble = React.memo(function MessageBubble({ message, isStreaming,
)} )}
</div> </div>
{isUser && (<div className="shrink-0 mt-1"><div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-anton-card border border-anton-border flex items-center justify-center"><User size={14} className="text-anton-muted" /></div></div>)} {isUser && (<div className="shrink-0 mt-1"><div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-anton-card border border-anton-border flex items-center justify-center"><User size={14} className="text-anton-muted" /></div></div>)}
{/* UI Preview Modal */}
{showUIPreview && previewHTML && (
<UIPreview
html={previewHTML}
title={`UI Preview`}
onClose={() => setShowUIPreview(false)}
/>
)}
</div> </div>
); );
}); });
function _stripPrefixes(text) { if (!text) return ""; return text.replace(/^\[(?:Image|Video|Document|File):\s[^\]]*\]\n?/gm, "").trim(); } function _stripPrefixes(text) { if (!text) return ""; return text.replace(/^\[(?:Image|Video|Document|File):\s[^\]]*\]\n?/gm, "").replace(/^\[UI DESIGN MODE\][\s\S]*?\n\n/m, "").trim(); }
export default MessageBubble; export default MessageBubble;
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment