emo-online-api / emo /frontend /src /components /ChatPreviewBubble.jsx
Emo Online
Deploy Emo API
dd87944
Raw
History Blame Contribute Delete
5.88 kB
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>
);
}