import { useEffect, useRef } from 'react'; import { useArtifact } from '../context/ArtifactContext'; import type { ArtifactData } from '../types/artifact.types'; import { Message } from '@memori.ai/memori-api-client/dist/types'; // Queue for calls made before the component is ready const pendingCalls: Array<{ method: string; args: any[] }> = []; let isReady = false; /** * Initialize the MemoriArtifactAPI stub on window * This should be called as early as possible to prevent "undefined" errors */ export const initMemoriArtifactAPI = () => { if (typeof window === 'undefined') return; // Only initialize if not already present if ((window as any).MemoriArtifactAPI) { return; } (window as any).MemoriArtifactAPI = { _isReady: () => isReady, _setReady: (ready: boolean) => { isReady = ready; }, _getPendingCalls: () => pendingCalls, _clearPendingCalls: () => { pendingCalls.length = 0; }, openArtifact: (artifact: ArtifactData) => { if (!isReady) { pendingCalls.push({ method: 'openArtifact', args: [artifact] }); } }, createAndOpenArtifact: (content: string, mimeType?: string, title?: string) => { if (!isReady) { pendingCalls.push({ method: 'createAndOpenArtifact', args: [content, mimeType, title] }); } }, createFromOutputElement: (outputElement: HTMLOutputElement) => { if (!isReady) { pendingCalls.push({ method: 'createFromOutputElement', args: [outputElement] }); return 'pending'; } return 'not-ready'; }, closeArtifact: () => { if (!isReady) { pendingCalls.push({ method: 'closeArtifact', args: [] }); } }, toggleFullscreen: () => { if (!isReady) { pendingCalls.push({ method: 'toggleFullscreen', args: [] }); } }, getState: () => { if (!isReady) { return { currentArtifact: null, isDrawerOpen: false, isFullscreen: false }; } return { currentArtifact: null, isDrawerOpen: false, isFullscreen: false }; }, }; }; // Initialize immediately when this module is loaded initMemoriArtifactAPI(); /** * Componente che espone le funzioni dell'Artifact System come API globale * Questo permette di controllare gli artifacts da JavaScript vanilla esterno */ export const ArtifactAPIBridge = ({ pushMessage, }: { pushMessage: (message: Message) => void; }) => { const { openArtifact, closeArtifact, toggleFullscreen, state } = useArtifact(); const apiRef = useRef(null); useEffect(() => { // Update API with actual implementations if (typeof window !== 'undefined') { const windowApi = (window as any).MemoriArtifactAPI; // Store reference to actual implementations apiRef.current = { /** * Apri un artifact esistente * @param artifact - Oggetto ArtifactData completo */ openArtifact: (artifact: ArtifactData) => { openArtifact(artifact); }, /** * Crea e apri un artifact con parametri semplici * @param content - Contenuto dell'artifact (HTML, markdown, ecc.) * @param mimeType - Tipo MIME (default: 'html') * @param title - Titolo da mostrare (default: auto-generato) */ createAndOpenArtifact: ( content: string, mimeType: string = 'html', title?: string ) => { const autoTitle = title || getTitleFromMimeType(mimeType); // Create the artifact object const artifact: ArtifactData = { id: `artifact-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`, content, mimeType, title: autoTitle, timestamp: new Date(), size: content.length, }; // Create wrapped message text for artifact detection // Don't reassign content parameter - use a new variable let messageText = content; if (!messageText.includes('')) { messageText = `${content}`; } //we have to push in the history the artifact as message pushMessage({ text: messageText, timestamp: new Date().toISOString(), fromUser: false as const, media: [], initial: false, translatedText: undefined, questionAnswered: undefined, generatedByAI: false, contextVars: undefined, date: undefined, placeName: undefined, placeLatitude: undefined, placeLongitude: undefined, placeUncertaintyKm: undefined, }); // Open the artifact immediately openArtifact(artifact); }, /** * Crea un artifact da un elemento esistente nel DOM * @param outputElement - Elemento DOM * @returns artifactId */ createFromOutputElement: (outputElement: HTMLOutputElement) => { const content = outputElement.innerHTML; const mimeType = outputElement.getAttribute('data-mimetype') || 'html'; const title = outputElement.getAttribute('data-title') || getTitleFromMimeType(mimeType); // Nascondi l'elemento originale outputElement.style.display = 'none'; outputElement.setAttribute('data-memori-processed', 'true'); const artifact: ArtifactData = { id: `artifact-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`, content, mimeType, title, timestamp: new Date(), size: content.length, }; // Create wrapped message text for artifact detection // Don't reassign content - use a new variable let messageText = content; if (!messageText.includes('')) { messageText = `${content}`; } pushMessage({ text: messageText, timestamp: new Date().toISOString(), fromUser: false as const, media: [], initial: false, translatedText: undefined, questionAnswered: undefined, generatedByAI: false, contextVars: undefined, date: undefined, placeName: undefined, placeLatitude: undefined, placeLongitude: undefined, placeUncertaintyKm: undefined, }); // Open the artifact immediately openArtifact(artifact); return artifact.id; }, /** * Chiudi l'artifact drawer corrente */ closeArtifact: () => { closeArtifact(); }, /** * Toggle fullscreen dell'artifact drawer */ toggleFullscreen: () => { toggleFullscreen(); }, /** * Ottieni lo stato corrente del sistema artifacts */ getState: () => { return { currentArtifact: state.currentArtifact, isDrawerOpen: state.isDrawerOpen, isFullscreen: state.isFullscreen, }; }, }; // Update window API methods to use the real implementations if (windowApi) { windowApi.openArtifact = apiRef.current.openArtifact; windowApi.createAndOpenArtifact = apiRef.current.createAndOpenArtifact; windowApi.createFromOutputElement = apiRef.current.createFromOutputElement; windowApi.closeArtifact = apiRef.current.closeArtifact; windowApi.toggleFullscreen = apiRef.current.toggleFullscreen; windowApi.getState = apiRef.current.getState; // Mark API as ready windowApi._setReady(true); // Process any pending calls that were queued before ready const pendingCalls = windowApi._getPendingCalls(); if (pendingCalls.length > 0) { pendingCalls.forEach((call: { method: string; args: any[] }) => { try { const method = (apiRef.current as any)[call.method]; if (method) { method(...call.args); } } catch (error) { // Swallow error, remove log } }); windowApi._clearPendingCalls(); } } else { // If windowApi doesn't exist yet, initialize it now (window as any).MemoriArtifactAPI = { ...apiRef.current, _isReady: true, _pendingCalls: [], _setReady: (ready: boolean) => { (window as any).MemoriArtifactAPI._isReady = ready; }, _getPendingCalls: () => { return (window as any).MemoriArtifactAPI._pendingCalls; }, _clearPendingCalls: () => { (window as any).MemoriArtifactAPI._pendingCalls = []; }, }; } } return () => { // Clean up the API on unmount if (typeof window !== 'undefined') { delete (window as any).MemoriArtifactAPI; } }; }, [openArtifact, closeArtifact, toggleFullscreen, state, pushMessage]); return null; }; // Helper functions function getTitleFromMimeType(mimeType: string): string { if (mimeType.includes('html')) return 'HTML Document'; if (mimeType.includes('markdown')) return 'Markdown Document'; if (mimeType.includes('javascript')) return 'JavaScript Code'; if (mimeType.includes('python')) return 'Python Code'; if (mimeType.includes('json')) return 'JSON Data'; if (mimeType.includes('css')) return 'CSS Stylesheet'; if (mimeType.includes('typescript')) return 'TypeScript Code'; if (mimeType.includes('xml')) return 'XML Document'; if (mimeType.includes('svg')) return 'SVG Image'; return 'Document'; } function createArtifactHandler( content: string, mimeType: string, title: string ): HTMLDivElement { const handler = document.createElement('div'); handler.className = 'memori-artifact-handler'; handler.style.cssText = ` display: flex; align-items: center; gap: 12px; padding: 16px; margin: 12px 0; background: white; border: 1px solid #e5e7eb; border-radius: 12px; cursor: pointer; transition: all 0.2s ease; `; const icon = getIconForMimeType(mimeType); const size = `${(content.length / 1024).toFixed(1)} KB`; handler.innerHTML = `
${icon}
${escapeHtml( title )}
${mimeType} • ${size}
`; handler.addEventListener('mouseenter', () => { handler.style.background = '#faf5ff'; handler.style.borderColor = '#9333ea'; }); handler.addEventListener('mouseleave', () => { handler.style.background = 'white'; handler.style.borderColor = '#e5e7eb'; }); return handler; } function getIconForMimeType(mimeType: string): string { if (mimeType.includes('html')) return '🌐'; if (mimeType.includes('markdown')) return '📝'; if (mimeType.includes('javascript') || mimeType.includes('typescript')) return '📜'; if (mimeType.includes('python')) return '🐍'; if (mimeType.includes('json')) return '📊'; if (mimeType.includes('css')) return '🎨'; if (mimeType.includes('xml')) return '📋'; if (mimeType.includes('svg')) return '🖼️'; return '📄'; } function escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }