From f60e171051d5b137f51e88bd1db453fb25744fea Mon Sep 17 00:00:00 2001 From: Babayaga Date: Thu, 9 Apr 2026 23:40:49 +0200 Subject: [PATCH] supbase --- packages/ui/shared/src/commons.ts | 96 ++++ packages/ui/shared/src/index.ts | 1 + packages/ui/src/App.tsx | 6 - packages/ui/src/components/TopNavigation.tsx | 2 +- packages/ui/src/hooks/usePlaygroundLogic.tsx | 471 ------------------ packages/ui/src/lib/openai.ts | 4 +- packages/ui/src/modules/ai/useChatEngine.ts | 1 - .../ui/src/modules/layout/GenericCanvas.tsx | 2 +- .../src/modules/layout/PlaygroundCanvas.tsx | 260 ---------- .../ui/src/modules/layout/WidgetPalette.tsx | 3 +- .../ui/src/modules/layout/client-layouts.ts | 141 +----- packages/ui/src/modules/layout/useLayouts.ts | 11 +- .../src/modules/layout/useWidgetSnippets.ts | 3 +- packages/ui/src/modules/pages/PageActions.tsx | 6 +- .../modules/pages/editor/UserPageDetails.tsx | 3 +- .../pages/editor/UserPageDetailsEdit.tsx | 3 +- .../pages/editor/UserPageDetailsView.tsx | 3 +- .../pages/editor/hooks/useTemplateManager.ts | 3 +- .../pages/editor/ribbons/PageRibbonBar.tsx | 5 +- .../ui/src/modules/posts/client-pictures.ts | 24 +- .../ui/src/modules/posts/views/adapters.ts | 3 +- .../posts/views/components/SmartLightbox.tsx | 2 +- .../views/components/SmartLightboxEditor.tsx | 2 +- packages/ui/src/modules/posts/views/types.ts | 2 +- .../src/modules/posts/views/usePostActions.ts | 2 +- .../ui/src/modules/storage/plugins/types.ts | 2 +- 26 files changed, 143 insertions(+), 918 deletions(-) create mode 100644 packages/ui/shared/src/commons.ts delete mode 100644 packages/ui/src/hooks/usePlaygroundLogic.tsx delete mode 100644 packages/ui/src/modules/layout/PlaygroundCanvas.tsx diff --git a/packages/ui/shared/src/commons.ts b/packages/ui/shared/src/commons.ts new file mode 100644 index 00000000..ca69868b --- /dev/null +++ b/packages/ui/shared/src/commons.ts @@ -0,0 +1,96 @@ + + +export interface UserIdentity { + id: string + user_id: string + identity_data?: { + [key: string]: any + } + identity_id: string + provider: string + created_at?: string + last_sign_in_at?: string + updated_at?: string +} + +const FactorTypes = ['totp', 'phone', 'webauthn'] as const + +/** + * Type of factor. `totp` and `phone` supported with this version + */ +export type FactorType = (typeof FactorTypes)[number] + +const FactorVerificationStatuses = ['verified', 'unverified'] as const + +/** + * The verification status of the factor, default is `unverified` after `.enroll()`, then `verified` after the user verifies it with `.verify()` + */ +type FactorVerificationStatus = (typeof FactorVerificationStatuses)[number] + +export type Factor< + Type extends FactorType = FactorType, + Status extends FactorVerificationStatus = (typeof FactorVerificationStatuses)[number], +> = { + /** ID of the factor. */ + id: string + + /** Friendly name of the factor, useful to disambiguate between multiple factors. */ + friendly_name?: string + + /** + * Type of factor. `totp` and `phone` supported with this version + */ + factor_type: Type + + /** + * The verification status of the factor, default is `unverified` after `.enroll()`, then `verified` after the user verifies it with `.verify()` + */ + status: Status + + created_at: string + updated_at: string +} + +export interface UserAppMetadata { + /** + * The first provider that the user used to sign up with. + */ + provider?: string + /** + * A list of all providers that the user has linked to their account. + */ + providers?: string[] + [key: string]: any +} + +export interface UserMetadata { + [key: string]: any +} + +export interface User { + id: string + app_metadata: UserAppMetadata + user_metadata: UserMetadata + aud: string + confirmation_sent_at?: string + recovery_sent_at?: string + email_change_sent_at?: string + new_email?: string + new_phone?: string + invited_at?: string + action_link?: string + email?: string + phone?: string + created_at: string + confirmed_at?: string + email_confirmed_at?: string + phone_confirmed_at?: string + last_sign_in_at?: string + role?: string + updated_at?: string + identities?: UserIdentity[] + is_anonymous?: boolean + is_sso_user?: boolean + factors?: (Factor | Factor)[] + deleted_at?: string + } diff --git a/packages/ui/shared/src/index.ts b/packages/ui/shared/src/index.ts index 678106a1..ac19ed9d 100644 --- a/packages/ui/shared/src/index.ts +++ b/packages/ui/shared/src/index.ts @@ -1,3 +1,4 @@ +export * from './commons.js'; export * from './ui/schemas.js'; export * from './ui/page-iterator.js'; export * from './competitors/schemas.js'; diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 4e1f9073..e10ee9d0 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -46,8 +46,6 @@ const enablePlaygrounds = import.meta.env.VITE_ENABLE_PLAYGROUNDS === 'true'; let PlaygroundEditor: any; let PlaygroundEditorLLM: any; -let VideoPlayerPlayground: any; -let VideoFeedPlayground: any; let VideoPlayerPlaygroundIntern: any; let PlaygroundImages: any; let PlaygroundImageEditor: any; @@ -77,7 +75,6 @@ if (enablePlaygrounds) { PlaygroundImages = React.lazy(() => import("./pages/PlaygroundImages")); PlaygroundImageEditor = React.lazy(() => import("./pages/PlaygroundImageEditor")); VideoGenPlayground = React.lazy(() => import("./pages/VideoGenPlayground")); - PlaygroundCanvas = React.lazy(() => import("./modules/layout/PlaygroundCanvas")); VariablePlayground = React.lazy(() => import("./components/variables/VariablesEditor").then(module => ({ default: module.VariablesEditor }))); I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground")); PlaygroundChat = React.lazy(() => import("./pages/PlaygroundChat")); @@ -184,10 +181,7 @@ const AppWrapper = () => { <> Loading...}>} /> Loading...}>} /> - Loading...}>} /> Loading...}>} /> - Loading...}>} /> - Loading...}>} /> )} diff --git a/packages/ui/src/components/TopNavigation.tsx b/packages/ui/src/components/TopNavigation.tsx index 04288f39..2565aa10 100644 --- a/packages/ui/src/components/TopNavigation.tsx +++ b/packages/ui/src/components/TopNavigation.tsx @@ -271,7 +271,7 @@ const TopNavigation = () => { - {roles.includes("admin") && ( + {roles && roles.includes("admin") && ( <> diff --git a/packages/ui/src/hooks/usePlaygroundLogic.tsx b/packages/ui/src/hooks/usePlaygroundLogic.tsx deleted file mode 100644 index b2a4fd2b..00000000 --- a/packages/ui/src/hooks/usePlaygroundLogic.tsx +++ /dev/null @@ -1,471 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useLayout } from '@/modules/layout/LayoutContext'; -import { toast } from 'sonner'; -import { useWidgetLoader } from './useWidgetLoader.tsx'; -import { useLayouts } from '@/modules/layout/useLayouts'; -import { Database } from '@/integrations/supabase/types'; -import { apiClient } from "@/lib/db"; - -type Layout = Database['public']['Tables']['layouts']['Row']; -type LayoutVisibility = Database['public']['Enums']['layout_visibility']; - -export function usePlaygroundLogic() { - // UI State - const [viewMode, setViewMode] = useState<'design' | 'preview'>('design'); - const [previewHtml, setPreviewHtml] = useState(''); - const [htmlSize, setHtmlSize] = useState(0); - const [isAppReady, setIsAppReady] = useState(false); - const [isEditMode, setIsEditMode] = useState(true); - - // Page State - const pageId = 'playground-canvas-demo'; - const pageName = 'Playground Canvas'; - const [layoutJson, setLayoutJson] = useState(null); - - // Layout Context - const { - loadedPages, - importPageLayout, - loadPageLayout - } = useLayout(); - - // Template State (now from Supabase) - const [templates, setTemplates] = useState([]); - const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false); - const [newTemplateName, setNewTemplateName] = useState(''); - const [isLoadingTemplates, setIsLoadingTemplates] = useState(false); - - // Paste JSON State - const [isPasteDialogOpen, setIsPasteDialogOpen] = useState(false); - const [pasteJsonContent, setPasteJsonContent] = useState(''); - - const { loadWidgetBundle } = useWidgetLoader(); - const { getLayouts, getLayout, createLayout, updateLayout, deleteLayout } = useLayouts(); - - const handleSave = async () => { - try { - - - } catch (e) { - console.error("Failed to save", e); - } - }; - - useEffect(() => { - refreshTemplates(); - }, []); - - const refreshTemplates = async () => { - setIsLoadingTemplates(true); - try { - const { data, error } = await getLayouts({ type: 'canvas' }); - if (error) { - console.error('Failed to load layouts:', error); - toast.error('Failed to load layouts'); - } else { - setTemplates(data || []); - } - } catch (e) { - console.error('Failed to refresh templates:', e); - } finally { - setIsLoadingTemplates(false); - } - }; - - // Initialization Effect - useEffect(() => { - const init = async () => { - let layout = loadedPages.get(pageId); - if (!layout) { - console.log('[Playground] Loading layout...'); - try { - await loadPageLayout(pageId, pageName); - } catch (e) { - console.error("Failed to load layout", e); - } - } - }; - init(); - }, [pageId, pageName, loadPageLayout, loadedPages]); - - // Restoration Effect - useEffect(() => { - const restore = async () => { - const layout = loadedPages.get(pageId); - if (layout) { - if (!isAppReady) { - let detectedRootTemplate: string | undefined; - - if (layout.loadedBundles && layout.loadedBundles.length > 0) { - console.log('[Playground] Restoring bundles:', layout.loadedBundles); - for (const bundleUrl of layout.loadedBundles) { - try { - const result = await loadWidgetBundle(bundleUrl); - // Capture root template from the first bundle that has one - if (result.rootTemplateUrl && !detectedRootTemplate) { - detectedRootTemplate = result.rootTemplateUrl; - } - } catch (e) { - console.error("Failed to restore bundle", bundleUrl); - } - } - } - - // Self-repair: If layout is missing rootTemplate but we found one, update it. - if (detectedRootTemplate && !layout.rootTemplate) { - console.log('[Playground] Backfilling rootTemplate:', detectedRootTemplate); - layout.rootTemplate = detectedRootTemplate; - layout.rootTemplate = detectedRootTemplate; - await handleSave(); - } - - setIsAppReady(true); - } - } - }; - // If layout exists and we haven't marked ready, try restore - // We depend on loadedPages map or specific entry - if (loadedPages.get(pageId) && !isAppReady) { - restore(); - } - }, [loadedPages, pageId, isAppReady, loadWidgetBundle]); - - // Preview Generation Effect - // Preview Generation Effect - useEffect(() => { - const updatePreview = async () => { - const layout = loadedPages.get(pageId); - if (layout && layout.rootTemplate) { - try { - const { generateEmailHtml } = await import('@/lib/emailExporter'); - let html = await generateEmailHtml(layout, layout.rootTemplate); - - // Inject Preview-Only CSS to fix "font-size: 0" visibility issues - // This ensures the preview looks correct without modifying the actual email export structure - const previewFixStyles = ` - - `; - - if (html.includes('')) { - html = html.replace('', `${previewFixStyles}`); - } else { - html = html.replace('', `${previewFixStyles}`); - } - - setPreviewHtml(html); - - // Approximate size of the email body (excluding images, as per Gmail limit on HTML only) - const size = new Blob([html]).size; - setHtmlSize(size); - - } catch (e) { - console.error("Preview generation failed", e); - // toast.error("Failed to generate preview"); // Suppress toast on auto-update to avoid spam - } - } else if (!layout?.rootTemplate) { - setPreviewHtml("

No root template found. Please load the email context first.

"); - } - }; - updatePreview(); - }, [loadedPages, pageId]); // Removed viewMode dependency - - const handleDumpJson = async () => { - if (!currentLayout) { - toast.error("No layout loaded"); - return; - } - try { - // Use current state directly - const json = JSON.stringify(currentLayout, null, 2); - setLayoutJson(json); - await navigator.clipboard.writeText(json); - toast.success("JSON dumped to console, clipboard, and view"); - console.log(json); - } catch (e) { - console.error("Failed to dump JSON", e); - toast.error("Failed to dump JSON"); - } - }; - - const handleLoadTemplate = async (template: Layout) => { - try { - // Fetch fresh layout data to ensure we have the latest version - const { data, error } = await getLayout(template.id); - - if (error || !data) { - console.error("Failed to fetch fresh layout", error); - toast.error("Failed to load latest version of layout"); - return; - } - - // layout_json is already a parsed object, convert to string for importPageLayout - const layoutJsonString = JSON.stringify(data.layout_json); - await importPageLayout(pageId, layoutJsonString); - toast.success(`Loaded layout: ${data.name || template.name}`); - setLayoutJson(null); - } catch (e) { - console.error("Failed to load layout", e); - toast.error("Failed to load layout"); - } - }; - - const handleSaveTemplate = async () => { - if (!newTemplateName.trim()) { - toast.error("Please enter a layout name"); - return; - } - if (!currentLayout) { - toast.error("No layout loaded to save"); - return; - } - try { - const layoutObject = currentLayout; - - const { data, error } = await createLayout({ - name: newTemplateName.trim(), - layout_json: layoutObject as any, - type: 'canvas', - visibility: 'private' as LayoutVisibility, - meta: {} - }); - - if (error) { - console.error('Failed to save layout:', error); - toast.error('Failed to save layout'); - return; - } - - toast.success("Layout saved to database"); - setIsSaveDialogOpen(false); - setNewTemplateName(''); - await refreshTemplates(); - } catch (e) { - console.error("Failed to save layout", e); - toast.error("Failed to save layout"); - } - }; - - const handlePasteJson = async () => { - if (!pasteJsonContent.trim()) { - toast.error("Please enter JSON content"); - return; - } - try { - JSON.parse(pasteJsonContent); - await importPageLayout(pageId, pasteJsonContent); - toast.success("Layout imported from JSON"); - setIsPasteDialogOpen(false); - setPasteJsonContent(''); - setLayoutJson(null); - } catch (e) { - console.error("Failed to import JSON", e); - toast.error("Invalid JSON format"); - } - }; - - const handleDeleteTemplate = async (layoutId: string) => { - try { - const { error } = await deleteLayout(layoutId); - if (error) { - console.error('Failed to delete layout:', error); - toast.error('Failed to delete layout'); - return; - } - toast.success('Layout deleted'); - await refreshTemplates(); - } catch (e) { - console.error('Failed to delete layout:', e); - toast.error('Failed to delete layout'); - } - }; - - const handleToggleVisibility = async (layoutId: string, currentVisibility: LayoutVisibility) => { - try { - // Cycle through visibility options: private -> listed -> public -> private - const visibilityOrder: LayoutVisibility[] = ['private', 'listed', 'public']; - const currentIndex = visibilityOrder.indexOf(currentVisibility); - const newVisibility = visibilityOrder[(currentIndex + 1) % visibilityOrder.length]; - - const { error } = await updateLayout(layoutId, { - visibility: newVisibility - }); - - if (error) { - console.error('Failed to update visibility:', error); - toast.error('Failed to update visibility'); - return; - } - - toast.success(`Layout visibility: ${newVisibility}`); - await refreshTemplates(); - } catch (e) { - console.error('Failed to toggle visibility:', e); - toast.error('Failed to toggle visibility'); - } - }; - - const handleRenameLayout = async (layoutId: string, newName: string) => { - if (!newName.trim()) { - toast.error('Layout name cannot be empty'); - return; - } - - try { - const { error } = await updateLayout(layoutId, { - name: newName.trim() - }); - - if (error) { - console.error('Failed to rename layout:', error); - toast.error('Failed to rename layout'); - return; - } - - toast.success('Layout renamed'); - await refreshTemplates(); - } catch (e) { - console.error('Failed to rename layout:', e); - toast.error('Failed to rename layout'); - } - }; - - const handleLoadContext = async () => { - const bundleUrl = '/widgets/email/library.json'; - try { - const result = await loadWidgetBundle(bundleUrl); - const { count, rootTemplateUrl } = result; - - toast.success(`Loaded ${count} email widgets`); - - const currentLayout = loadedPages.get(pageId); - if (currentLayout) { - const bundles = new Set(currentLayout.loadedBundles || []); - let changed = false; - - if (!bundles.has(bundleUrl)) { - bundles.add(bundleUrl); - currentLayout.loadedBundles = Array.from(bundles); - changed = true; - } - - if (rootTemplateUrl && currentLayout.rootTemplate !== rootTemplateUrl) { - currentLayout.rootTemplate = rootTemplateUrl; - changed = true; - } - - if (changed) { - if (changed) { - await handleSave(); - toast.success("Context saved to layout"); - } - } - } - } catch (e) { - console.error("Failed to load context", e); - toast.error("Failed to load email context"); - } - }; - - const handleExportHtml = async () => { - const layout = loadedPages.get(pageId); - if (!layout) return; - - if (!layout.rootTemplate) { - toast.error("No root template found. Please load a context first."); - return; - } - - try { - const { generateEmailHtml } = await import('@/lib/emailExporter'); - const html = await generateEmailHtml(layout, layout.rootTemplate); - - const blob = new Blob([html], { type: 'text/html' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${layout.name.toLowerCase().replace(/\s+/g, '-')}.html`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - toast.success("Exported HTML"); - } catch (e) { - console.error("Failed to export HTML", e); - toast.error("Export failed"); - } - }; - - const handleSendTestEmail = async () => { - const layout = loadedPages.get(pageId); - if (!layout) { - toast.error("Layout not loaded"); - return; - } - - if (!layout.rootTemplate) { - toast.error("No root template found. Please load a context first."); - return; - } - - try { - toast.info("Generating email..."); - const { generateEmailHtml } = await import('@/lib/emailExporter'); - let html = await generateEmailHtml(layout, layout.rootTemplate); - - const dummyId = import.meta.env.DEFAULT_POST_TEST_ID || '00000000-0000-0000-0000-000000000000'; - const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL; - - toast.info("Sending test email..."); - - await apiClient(`/api/send/email/${dummyId}`, { - method: 'POST', - body: JSON.stringify({ - html, - subject: `[Test] ${layout.name} - ${new Date().toLocaleTimeString()}` - }) - }); - toast.success("Test email sent!"); - - } catch (e) { - console.error("Failed to send test email", e); - toast.error("Failed to send test email"); - } - }; - - const currentLayout = loadedPages.get(pageId); - - return { - // State - currentLayout, - viewMode, setViewMode, - previewHtml, - htmlSize, - isAppReady, - isEditMode, setIsEditMode, - pageId, - pageName, - layoutJson, - templates, - isLoadingTemplates, - isSaveDialogOpen, setIsSaveDialogOpen, - newTemplateName, setNewTemplateName, - isPasteDialogOpen, setIsPasteDialogOpen, - pasteJsonContent, setPasteJsonContent, - - // Handlers - handleDumpJson, - handleLoadTemplate, - handleSaveTemplate, - handleDeleteTemplate, - handleToggleVisibility, - handleRenameLayout, - handlePasteJson, - handleLoadContext, - handleExportHtml, - handleSendTestEmail, - importPageLayout // Expose importPageLayout for direct external updates - }; -} diff --git a/packages/ui/src/lib/openai.ts b/packages/ui/src/lib/openai.ts index 4b24ab82..5dc4c52d 100644 --- a/packages/ui/src/lib/openai.ts +++ b/packages/ui/src/lib/openai.ts @@ -95,13 +95,11 @@ const getAuthToken = async (): Promise => { // Create OpenAI client export const createOpenAIClient = async (apiKey?: string): Promise => { - // We use the Supabase session token as the "apiKey" for the proxy - // If a legacy OpenAI key (sk-...) is passed, we ignore it and use the session token let token = apiKey; if (!token || token.startsWith('sk-')) { if (token?.startsWith('sk-')) { - consoleLogger.warn('Legacy OpenAI key detected and ignored. Using Supabase session token for proxy.'); + consoleLogger.warn('Legacy OpenAI key detected and ignored. Using Zitadel session token for proxy.'); } token = (await getAuthToken()) || undefined; } diff --git a/packages/ui/src/modules/ai/useChatEngine.ts b/packages/ui/src/modules/ai/useChatEngine.ts index b11b44c6..7feacf17 100644 --- a/packages/ui/src/modules/ai/useChatEngine.ts +++ b/packages/ui/src/modules/ai/useChatEngine.ts @@ -234,7 +234,6 @@ export function useChatEngine(namespace = 'chat') { if (!user) return null; if (prov === 'openai') { // Return null since the createOpenAIClient will automatically grab - // the Supabase session token instead of requiring raw OpenAI keys. return null; } try { diff --git a/packages/ui/src/modules/layout/GenericCanvas.tsx b/packages/ui/src/modules/layout/GenericCanvas.tsx index 2f800d18..bca9b5a1 100644 --- a/packages/ui/src/modules/layout/GenericCanvas.tsx +++ b/packages/ui/src/modules/layout/GenericCanvas.tsx @@ -4,7 +4,7 @@ import GenericCanvasView from './GenericCanvasView'; // Re-export the full props type so existing consumers keep working export type { GenericCanvasEditProps as GenericCanvasProps } from './GenericCanvasEdit'; -// Lazy-load the heavy edit component (WidgetPalette, upload utils, supabase, etc.) +// Lazy-load the heavy edit component (WidgetPalette, upload utils, etc.) const GenericCanvasEdit = lazy(() => import('./GenericCanvasEdit')); import type { GenericCanvasEditProps } from './GenericCanvasEdit'; diff --git a/packages/ui/src/modules/layout/PlaygroundCanvas.tsx b/packages/ui/src/modules/layout/PlaygroundCanvas.tsx deleted file mode 100644 index 362b5882..00000000 --- a/packages/ui/src/modules/layout/PlaygroundCanvas.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { toast } from 'sonner'; -import React, { useEffect } from 'react'; -import { GenericCanvas } from '@/modules/layout/GenericCanvas'; -import { usePlaygroundLogic } from '@/hooks/usePlaygroundLogic.tsx'; -import { useWebSocket } from '@/contexts/WS_Socket'; -import { PlaygroundHeader } from '@/components/playground/PlaygroundHeader'; -import { TemplateDialogs } from '@/components/playground/TemplateDialogs'; -import { useSelection } from '@/hooks/useSelection'; -import { SelectionHandler } from '@/modules/layout/SelectionHandler'; -import { WidgetPropertyPanel } from '@/components/widgets/WidgetPropertyPanel'; -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { LayoutProvider } from '@/modules/layout/LayoutContext'; - -const PlaygroundCanvasContent = () => { - const { - // State - viewMode, setViewMode, - previewHtml, - isAppReady, - isEditMode, setIsEditMode, - pageId, - pageName, - layoutJson, - templates, - isSaveDialogOpen, setIsSaveDialogOpen, - newTemplateName, setNewTemplateName, - isPasteDialogOpen, setIsPasteDialogOpen, - pasteJsonContent, setPasteJsonContent, - - // Handlers - handleDumpJson, - handleLoadTemplate, - handleSaveTemplate, - handleDeleteTemplate, - handleToggleVisibility, - handleRenameLayout, - handlePasteJson, - handleLoadContext, - handleExportHtml, - handleSendTestEmail, - htmlSize, - currentLayout, - importPageLayout - } = usePlaygroundLogic(); - - const { connectToServer, isConnected, wsStatus, disconnectFromServer } = useWebSocket(); - const { selectedWidgetId, selectWidget, clearSelection, moveSelection } = useSelection({ pageId }); - - // Log "playground ready" when widgets are loaded - useEffect(() => { - if (isAppReady && isConnected) { - // We need to access the socket directly or expose a send method. - // The context currently only exposes connection methods. - // We should update the context or modbusService to allow sending generic messages. - // But modbusService.sendCommand exists. - import('@/services/modbusService').then(module => { - module.default.sendCommand('log', { - name: 'playground-canvas', - level: 'info', - message: 'Playground ready, loaded widgets', - - }).catch(err => console.error('Failed to send log:', err)); - }); - } - }, [isAppReady, isConnected]); - - // Auto-log Layout JSON on change - useEffect(() => { - if (isConnected && currentLayout) { - import('@/services/modbusService').then(module => { - const service = module.default; - service.log({ - name: 'canvas-page-latest', - message: currentLayout, - options: { mode: 'overwrite', format: 'json' } - }).catch(err => console.error('Failed to log layout json:', err)); - }); - } - }, [currentLayout, isConnected]); - - // Auto-log Preview HTML on change - useEffect(() => { - if (isConnected && previewHtml) { - import('@/services/modbusService').then(module => { - const service = module.default; - service.log({ - name: 'canvas-html-latest', - message: previewHtml, - options: { mode: 'overwrite', format: 'html' } - }).catch(err => console.error('Failed to log preview html:', err)); - }); - } - }, [previewHtml, isConnected]); - - // Handle external layout updates (from file watcher) - // Handle external layout updates (from file watcher) - useEffect(() => { - if (isConnected) { - let unsubscribe: (() => void) | undefined; - let isCancelled = false; - - import('@/services/modbusService').then(module => { - if (isCancelled) return; - const service = module.default; - unsubscribe = service.addMessageHandler('layout-update', (data) => { - console.log(`[Playground] Received layout update`, typeof data); - if (typeof data === 'string') { - // Handle Base64 content (HTML/MD) - try { - const decoded = atob(data); - if (decoded.trim().startsWith('<')) { - toast.info("Received HTML update (View in logs)"); - } - } catch (e) { - console.error('Failed to decode base64 layout update', e); - } - } else { - // Handle JSON Object (Layout) - importPageLayout(pageId, JSON.stringify(data)).then(() => { - console.log('[Playground] External layout applied successfully'); - toast.info(`Layout updated from watcher`); - }).catch(err => { - console.error('Failed to import external layout:', err); - toast.error(`Failed to update layout from watcher`); - }); - } - }); - }); - - return () => { - isCancelled = true; - if (unsubscribe) unsubscribe(); - }; - } - }, [isConnected, pageId, importPageLayout]); - - return ( -
- setIsSaveDialogOpen(true)} - onPasteJsonClick={() => setIsPasteDialogOpen(true)} - handleDumpJson={handleDumpJson} - handleLoadContext={handleLoadContext} - isEditMode={isEditMode} - setIsEditMode={setIsEditMode} - /> - -
- {!isAppReady ? ( -
-
-
-

Restoring Playground...

-
-
- ) : ( - <> -
- - - -
- - - {layoutJson && ( -
-

Layout JSON

-
-                                                        {layoutJson}
-                                                    
-
- )} -
-
-
- - {/* Property Panel - Desktop Only (Hidden on mobile via CSS) */} - {selectedWidgetId && isEditMode && ( - <> - - - - - - )} -
-
- -
-