Spaces:
Running
Running
| import React, { useState } from "react"; | |
| import { Globe, FileCode2, Copy, Check, Image as ImageIcon } from "lucide-react"; | |
| import { toast } from "sonner"; | |
| import SquarePreviewFrame from "./SquarePreviewFrame"; | |
| import GeneratedImagePreview from "./GeneratedImagePreview"; | |
| import SearchResultPreview from "./SearchResultPreview"; | |
| import InteractiveBrowser from "./InteractiveBrowser"; | |
| import ErrorBoundary from "./ErrorBoundary"; | |
| import LiveHtmlPreview from "./LiveHtmlPreview"; | |
| import { resolveToolPreview } from "../lib/resolveToolPreview"; | |
| import { normalizeFilePath } from "../lib/filePreview"; | |
| /** Bulle d'aperçu dans le chat — navigateur interactif inline pour les sites web. */ | |
| export default function ChatPreviewBubble({ event, className = "", liveHtmlByPath = {}, showCopyCode = false }) { | |
| const data = resolveToolPreview(event); | |
| const [copied, setCopied] = useState(false); | |
| if (!data) return null; | |
| const liveHtmlContent = | |
| data.kind === "html" && data.path | |
| ? liveHtmlByPath[normalizeFilePath(data.path)] ?? liveHtmlByPath[data.path] ?? data.fullContent | |
| : data.fullContent; | |
| const isBrowser = data.kind === "interactive"; | |
| const copyContent = | |
| data.kind === "html" ? liveHtmlContent || data.fullContent : data.fullContent; | |
| const canCopy = showCopyCode && (data.kind === "text" || data.kind === "html") && copyContent; | |
| const handleCopy = async () => { | |
| if (!copyContent) return; | |
| try { | |
| await navigator.clipboard.writeText(copyContent); | |
| setCopied(true); | |
| toast.success("Code copié"); | |
| setTimeout(() => setCopied(false), 1500); | |
| } catch { | |
| toast.error("Copie impossible"); | |
| } | |
| }; | |
| return ( | |
| <div | |
| className={`rounded-2xl overflow-hidden ${className}`} | |
| data-testid="chat-preview-bubble" | |
| style={{ | |
| background: "var(--emo-surface)", | |
| border: "1px solid var(--emo-border)", | |
| boxShadow: "0 4px 24px rgba(0,0,0,0.08)", | |
| }} | |
| > | |
| <div | |
| className="flex items-center gap-2 px-3 py-2 em-border-b" | |
| style={{ background: "var(--emo-subtle-bg, var(--emo-surface))" }} | |
| > | |
| {isBrowser ? ( | |
| <span | |
| className="w-2 h-2 rounded-full flex-shrink-0" | |
| style={{ background: "var(--mode-color)" }} | |
| aria-hidden | |
| /> | |
| ) : data.kind === "html" ? ( | |
| <FileCode2 size={12} style={{ color: "var(--emo-text-muted)" }} /> | |
| ) : data.kind === "image" ? ( | |
| <ImageIcon size={12} style={{ color: "var(--emo-text-muted)" }} /> | |
| ) : ( | |
| <Globe size={12} style={{ color: "var(--emo-text-muted)" }} /> | |
| )} | |
| <span className="truncate flex-1 text-xs font-medium" style={{ color: "var(--emo-text)" }}> | |
| {isBrowser | |
| ? data.title || data.url?.replace(/^https?:\/\//, "") || "Navigateur" | |
| : data.title || data.url || "Aperçu"} | |
| </span> | |
| {canCopy && ( | |
| <button | |
| type="button" | |
| data-testid="chat-copy-code-btn" | |
| title="Copier tout le code" | |
| onClick={handleCopy} | |
| className="flex-shrink-0 flex items-center gap-1 px-2 py-1 rounded-md text-[10px] em-hover-subtle transition" | |
| style={{ color: copied ? "var(--emo-success-text)" : "var(--emo-text-muted)" }} | |
| > | |
| {copied ? <Check size={11} /> : <Copy size={11} />} | |
| <span className="hidden sm:inline">{copied ? "Copié" : "Copier"}</span> | |
| </button> | |
| )} | |
| </div> | |
| <div className={isBrowser ? "p-2" : "p-3"}> | |
| {data.kind === "search" && ( | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> | |
| {(data.results || []).map((r, i) => ( | |
| <SearchResultPreview | |
| key={r.url || i} | |
| url={r.url} | |
| title={r.title} | |
| subtitle={r.domain || r.url} | |
| snippet={r.snippet} | |
| testId={`bubble-search-${i}`} | |
| /> | |
| ))} | |
| </div> | |
| )} | |
| {data.kind === "interactive" && ( | |
| <ErrorBoundary label="Navigateur interactif"> | |
| <InteractiveBrowser | |
| autoOpen | |
| compact={false} | |
| embedded | |
| liveSync | |
| frame={{ | |
| url: data.url, | |
| title: data.title, | |
| preview: data.previewText, | |
| screenshot_base64: data.screenshot_base64, | |
| elements: data.elements, | |
| session_id: data.session_id, | |
| }} | |
| sessionId={data.session_id} | |
| /> | |
| </ErrorBoundary> | |
| )} | |
| {data.kind === "html" && ( | |
| <LiveHtmlPreview | |
| path={data.path} | |
| title={data.title} | |
| content={liveHtmlContent || ""} | |
| compact={false} | |
| /> | |
| )} | |
| {data.kind === "image" && data.imageFields && ( | |
| <GeneratedImagePreview | |
| sources={[data.imageFields, event?.inlinePreview, event?.result]} | |
| title={data.title} | |
| showActions={event?.tool === "generate_image"} | |
| className="max-w-none border-0 shadow-none" | |
| testId="bubble-image-preview" | |
| /> | |
| )} | |
| {data.kind === "image" && !data.imageFields && data.src && ( | |
| <SquarePreviewFrame | |
| kind="image" | |
| src={data.src} | |
| title={data.title} | |
| subtitle={data.path} | |
| testId="bubble-image-preview" | |
| className="max-w-none" | |
| /> | |
| )} | |
| {data.kind === "text" && ( | |
| <SquarePreviewFrame | |
| kind="text" | |
| text={data.text} | |
| title={data.title} | |
| subtitle={data.path} | |
| testId="bubble-text-preview" | |
| className="max-w-none" | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |