diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 88807306..01abd8d7 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -27,7 +27,7 @@ import UserProfile from "./pages/UserProfile"; import UserCollections from "./pages/UserCollections"; import Collections from "./pages/Collections"; import NewCollection from "./pages/NewCollection"; -import UserPage from "./pages/UserPage"; +const UserPage = React.lazy(() => import("./pages/UserPage")); import NewPage from "./pages/NewPage"; import TagPage from "./pages/TagPage"; import SearchResults from "./pages/SearchResults"; @@ -35,6 +35,8 @@ import Wizard from "./pages/Wizard"; import NewPost from "./pages/NewPost"; import Organizations from "./pages/Organizations"; +import LogsPage from "./components/logging/LogsPage"; + const ProviderSettings = React.lazy(() => import("./pages/ProviderSettings")); const PlaygroundEditor = React.lazy(() => import("./pages/PlaygroundEditor")); const PlaygroundEditorLLM = React.lazy(() => import("./pages/PlaygroundEditorLLM")); @@ -49,7 +51,9 @@ const VideoGenPlayground = React.lazy(() => import("./pages/VideoGenPlayground") const PlaygroundCanvas = React.lazy(() => import("./pages/PlaygroundCanvas")); const TypesPlayground = React.lazy(() => import("./components/types/TypesPlayground")); const Tetris = React.lazy(() => import("./apps/tetris/Tetris")); -import LogsPage from "./components/logging/LogsPage"; +const I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground")); + + const queryClient = new QueryClient(); @@ -79,7 +83,7 @@ const AppWrapper = () => { } /> } /> } /> - } /> + Loading...}>} /> } /> } /> } /> @@ -112,7 +116,7 @@ const AppWrapper = () => { } /> } /> } /> - } /> + Loading...}>} /> } /> } /> } /> @@ -138,7 +142,9 @@ const AppWrapper = () => { Loading...}>} /> Loading...}>} /> Loading...}>} /> - Loading...}>} /> + Loading...}>} /> + Loading...}>} /> + Loading...}>} /> } /> {/* Logs */} @@ -167,44 +173,53 @@ import { StreamInvalidator } from "@/components/StreamInvalidator"; // ... (imports) +import { ActionProvider } from "@/actions/ActionProvider"; +import { HelmetProvider } from 'react-helmet-async'; + +// ... previous imports ... + const App = () => { React.useEffect(() => { initFormatDetection(); }, []); return ( - new Map() }}> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + new Map() }}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/packages/ui/src/actions/ActionProvider.tsx b/packages/ui/src/actions/ActionProvider.tsx index 7a2f48d5..0202310e 100644 --- a/packages/ui/src/actions/ActionProvider.tsx +++ b/packages/ui/src/actions/ActionProvider.tsx @@ -15,8 +15,5 @@ export const ActionProvider: React.FC = ({ children }) => { registerAction(action); }); }, [registerAction]); - - // TODO: Add keyboard shortcut listener here - return <>{children}; }; diff --git a/packages/ui/src/actions/store.ts b/packages/ui/src/actions/store.ts index 09fd4ed3..e611aba3 100644 --- a/packages/ui/src/actions/store.ts +++ b/packages/ui/src/actions/store.ts @@ -12,7 +12,7 @@ export const useActionStore = create((set, get) => ({ // Prevent duplicate registration if not needed, or overwrite // For now, we overwrite based on ID if (state.actions[action.id]) { - console.warn(`Action with id ${action.id} already exists. Overwriting.`); + // console.warn(`Action with id ${action.id} already exists. Overwriting.`); } return { actions: { diff --git a/packages/ui/src/components/PageActions.tsx b/packages/ui/src/components/PageActions.tsx index eee819c2..30e42ad4 100644 --- a/packages/ui/src/components/PageActions.tsx +++ b/packages/ui/src/components/PageActions.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Eye, EyeOff, Edit3, Trash2, Share2, Link as LinkIcon, FileText, Download, FolderTree, FileJson, LayoutTemplate } from "lucide-react"; @@ -12,7 +12,8 @@ import { DropdownMenuGroup } from "@/components/ui/dropdown-menu"; import { T, translate } from "@/i18n"; -import { CategoryManager } from "./widgets/CategoryManager"; +// import { CategoryManager } from "./widgets/CategoryManager"; // Lazy loaded below +const CategoryManager = React.lazy(() => import("./widgets/CategoryManager").then(module => ({ default: module.CategoryManager }))); import { cn } from "@/lib/utils"; import { Database } from '@/integrations/supabase/types'; @@ -467,15 +468,19 @@ draft: ${!page.visible} {showLabels && Categories} - setShowCategoryManager(false)} - currentPageId={page.id} - currentPageMeta={page.meta} - onPageMetaUpdate={handleMetaUpdate} - filterByType="pages" - defaultMetaType="pages" - /> + + {showCategoryManager && ( + setShowCategoryManager(false)} + currentPageId={page.id} + currentPageMeta={page.meta} + onPageMetaUpdate={handleMetaUpdate} + filterByType="pages" + defaultMetaType="pages" + /> + )} + diff --git a/packages/ui/src/components/PhotoCard.tsx b/packages/ui/src/components/PhotoCard.tsx index e5ca72bb..ae657f38 100644 --- a/packages/ui/src/components/PhotoCard.tsx +++ b/packages/ui/src/components/PhotoCard.tsx @@ -1,4 +1,4 @@ -import { Heart, Download, Share2, User, MessageCircle, Edit3, Trash2, Maximize, Layers } from "lucide-react"; +import { Heart, Download, Share2, User, MessageCircle, Edit3, Trash2, Maximize, Layers, ExternalLink } from "lucide-react"; import { Button } from "@/components/ui/button"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/useAuth"; @@ -41,6 +41,7 @@ interface PhotoCardProps { variant?: 'grid' | 'feed'; apiUrl?: string; versionCount?: number; + isExternal?: boolean; } const PhotoCard = ({ @@ -66,7 +67,8 @@ const PhotoCard = ({ responsive, variant = 'grid', apiUrl, - versionCount + versionCount, + isExternal = false }: PhotoCardProps) => { const { user } = useAuth(); const navigate = useNavigate(); @@ -104,6 +106,8 @@ const PhotoCard = ({ const handleLike = async (e: React.MouseEvent) => { e.stopPropagation(); + if (isExternal) return; + if (!user) { toast.error(translate('Please sign in to like pictures')); return; @@ -141,6 +145,8 @@ const PhotoCard = ({ const handleDelete = async (e: React.MouseEvent) => { e.stopPropagation(); + if (isExternal) return; + if (!user || !isOwner) { toast.error(translate('You can only delete your own images')); return; @@ -297,6 +303,11 @@ const PhotoCard = ({ }; const handlePublish = async (option: 'overwrite' | 'new', imageUrl: string, newTitle: string, description?: string) => { + if (isExternal) { + toast.error(translate('Cannot publish external images')); + return; + } + if (!user) { toast.error(translate('Please sign in to publish images')); return; @@ -409,6 +420,13 @@ const PhotoCard = ({ data={responsive} apiUrl={apiUrl} /> + {/* Helper Badge for External Images */} + {isExternal && ( +
+ + External +
+ )} {/* Desktop Hover Overlay - hidden on mobile, and hidden in feed variant */} @@ -428,27 +446,31 @@ const PhotoCard = ({ )}
- - {localLikes > 0 && {localLikes}} + {!isExternal && ( + <> + + {localLikes > 0 && {localLikes}} - - {comments} + + {comments} + + )} - {isOwner && ( + {isOwner && !isExternal && ( <> - + {!isExternal && ( + + )}
@@ -555,27 +579,31 @@ const PhotoCard = ({ {/* Actions */}
- - {localLikes > 0 && ( - {localLikes} - )} + {!isExternal && ( + <> + + {localLikes > 0 && ( + {localLikes} + )} - - {comments > 0 && ( - {comments} + + {comments > 0 && ( + {comments} + )} + )} - - {isOwner && ( + {!isExternal && ( + + )} + {isOwner && !isExternal && (
}> + + )} diff --git a/packages/ui/src/components/hmi/GenericCanvas.tsx b/packages/ui/src/components/hmi/GenericCanvas.tsx index a90c43ae..a61953ea 100644 --- a/packages/ui/src/components/hmi/GenericCanvas.tsx +++ b/packages/ui/src/components/hmi/GenericCanvas.tsx @@ -14,13 +14,14 @@ interface GenericCanvasProps { showControls?: boolean; className?: string; selectedWidgetId?: string | null; - onSelectWidget?: (widgetId: string) => void; + onSelectWidget?: (widgetId: string, pageId?: string) => void; selectedContainerId?: string | null; - onSelectContainer?: (containerId: string | null) => void; + onSelectContainer?: (containerId: string | null, pageId?: string) => void; initialLayout?: any; editingWidgetId?: string | null; onEditWidget?: (widgetId: string | null) => void; newlyAddedWidgetId?: string | null; + contextVariables?: Record; } const GenericCanvasComponent: React.FC = ({ @@ -36,7 +37,8 @@ const GenericCanvasComponent: React.FC = ({ initialLayout, editingWidgetId, onEditWidget, - newlyAddedWidgetId + newlyAddedWidgetId, + contextVariables }) => { const { loadedPages, @@ -72,13 +74,14 @@ const GenericCanvasComponent: React.FC = ({ const [internalSelectedContainer, setInternalSelectedContainer] = useState(null); const selectedContainer = propSelectedContainerId !== undefined ? propSelectedContainerId : internalSelectedContainer; - const setSelectedContainer = (id: string | null) => { + const setSelectedContainer = (id: string | null, pageId?: string) => { if (propOnSelectContainer) { - propOnSelectContainer(id); + propOnSelectContainer(id, pageId); } else { setInternalSelectedContainer(id); } }; + const [showWidgetPalette, setShowWidgetPalette] = useState(false); const [targetContainerId, setTargetContainerId] = useState(null); const [targetColumn, setTargetColumn] = useState(undefined); @@ -96,8 +99,8 @@ const GenericCanvasComponent: React.FC = ({ ); } - const handleSelectContainer = (containerId: string) => { - setSelectedContainer(containerId); + const handleSelectContainer = (containerId: string, pageId?: string) => { + setSelectedContainer(containerId, pageId); }; const handleAddWidget = (containerId: string, columnIndex?: number) => { @@ -313,6 +316,7 @@ const GenericCanvasComponent: React.FC = ({ editingWidgetId={editingWidgetId} onEditWidget={onEditWidget} newlyAddedWidgetId={newlyAddedWidgetId} + contextVariables={contextVariables} onRemoveWidget={async (widgetId) => { try { await removeWidgetFromPage(pageId, widgetId); diff --git a/packages/ui/src/components/hmi/LayoutContainer.tsx b/packages/ui/src/components/hmi/LayoutContainer.tsx index 9a7da833..f4c098d3 100644 --- a/packages/ui/src/components/hmi/LayoutContainer.tsx +++ b/packages/ui/src/components/hmi/LayoutContainer.tsx @@ -15,7 +15,7 @@ interface LayoutContainerProps { isEditMode: boolean; pageId: string; selectedContainerId?: string | null; - onSelect?: (containerId: string) => void; + onSelect?: (containerId: string, pageId?: string) => void; onAddWidget?: (containerId: string, targetColumn?: number) => void; onRemoveWidget?: (widgetInstanceId: string) => void; onMoveWidget?: (widgetInstanceId: string, direction: 'up' | 'down' | 'left' | 'right') => void; @@ -27,12 +27,13 @@ interface LayoutContainerProps { canMoveContainerUp?: boolean; canMoveContainerDown?: boolean; selectedWidgetId?: string | null; - onSelectWidget?: (widgetId: string) => void; + onSelectWidget?: (widgetId: string, pageId?: string) => void; depth?: number; isCompactMode?: boolean; editingWidgetId?: string | null; onEditWidget?: (widgetId: string | null) => void; newlyAddedWidgetId?: string | null; + contextVariables?: Record; } const LayoutContainerComponent: React.FC = ({ @@ -58,6 +59,7 @@ const LayoutContainerComponent: React.FC = ({ editingWidgetId, onEditWidget, newlyAddedWidgetId, + contextVariables, }) => { const maxDepth = 3; // Limit nesting depth const canNest = depth < maxDepth; @@ -116,7 +118,7 @@ const LayoutContainerComponent: React.FC = ({ isEditMode={isEditMode} pageId={pageId} isSelected={selectedWidgetId === widget.id} - onSelect={() => onSelectWidget?.(widget.id)} + onSelect={() => onSelectWidget?.(widget.id, pageId)} canMoveUp={index > 0} canMoveDown={index < container.widgets.length - 1} onRemove={onRemoveWidget} @@ -124,6 +126,10 @@ const LayoutContainerComponent: React.FC = ({ isEditing={editingWidgetId === widget.id} onEditWidget={onEditWidget} isNew={newlyAddedWidgetId === widget.id} + selectedWidgetId={selectedWidgetId} + onSelectWidget={onSelectWidget} + editingWidgetId={editingWidgetId} + contextVariables={contextVariables} /> ))} @@ -182,6 +188,7 @@ const LayoutContainerComponent: React.FC = ({ editingWidgetId={editingWidgetId} onEditWidget={onEditWidget} newlyAddedWidgetId={newlyAddedWidgetId} + contextVariables={contextVariables} /> ))} @@ -196,7 +203,7 @@ const LayoutContainerComponent: React.FC = ({ )} onDoubleClick={isEditMode ? (e) => { e.stopPropagation(); - onSelect?.(container.id); + onSelect?.(container.id, pageId); setTimeout(() => onAddWidget?.(container.id), 100); // Small delay to ensure selection happens first, no column = append } : undefined} title={isEditMode ? "Double-click to add widget" : undefined} @@ -375,7 +382,7 @@ const LayoutContainerComponent: React.FC = ({ onClick={(e) => { e.stopPropagation(); if (isEditMode) { - onSelect?.(container.id); + onSelect?.(container.id, pageId); } }} > @@ -463,6 +470,10 @@ interface WidgetItemProps { isEditing?: boolean; onEditWidget?: (widgetId: string | null) => void; isNew?: boolean; + selectedWidgetId?: string | null; + onSelectWidget?: (widgetId: string, pageId?: string) => void; + editingWidgetId?: string | null; + contextVariables?: Record; } const WidgetItem: React.FC = ({ @@ -477,7 +488,11 @@ const WidgetItem: React.FC = ({ onSelect, isEditing, onEditWidget, - isNew + isNew, + selectedWidgetId, + onSelectWidget, + editingWidgetId, + contextVariables, }) => { const widgetDefinition = widgetRegistry.get(widget.widgetId); const { updateWidgetProps, renameWidget } = useLayout(); @@ -639,6 +654,11 @@ const WidgetItem: React.FC = ({ console.error('Failed to update widget props:', error); } }} + selectedWidgetId={selectedWidgetId} + onSelectWidget={onSelectWidget} + editingWidgetId={editingWidgetId} + onEditWidget={onEditWidget} + contextVariables={contextVariables} /> diff --git a/packages/ui/src/components/playground/I18nPlayground.tsx b/packages/ui/src/components/playground/I18nPlayground.tsx new file mode 100644 index 00000000..ccb62bc6 --- /dev/null +++ b/packages/ui/src/components/playground/I18nPlayground.tsx @@ -0,0 +1,228 @@ +import React, { useState, useEffect } from 'react'; +import { translateText, fetchGlossaries, createGlossary, deleteGlossary, TargetLanguageCodeSchema, Glossary } from '@/lib/db'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Loader2, Trash2, Plus } from 'lucide-react'; +import { toast } from 'sonner'; + +// Safely extract options from ZodUnion of ZodEnums +const TARGET_LANGS = [ + ...TargetLanguageCodeSchema.options[0].options, + ...TargetLanguageCodeSchema.options[1].options +].sort(); + +export default function I18nPlayground() { + // Translation State + const [srcLang, setSrcLang] = useState('en'); + const [dstLang, setDstLang] = useState('fr'); + const [text, setText] = useState(''); + const [translation, setTranslation] = useState(''); + const [selectedGlossaryId, setSelectedGlossaryId] = useState(''); + const [isTranslating, setIsTranslating] = useState(false); + + // Glossary State + const [glossaries, setGlossaries] = useState([]); + const [loadingGlossaries, setLoadingGlossaries] = useState(false); + + // New Glossary State + const [newGlossaryName, setNewGlossaryName] = useState(''); + const [newGlossarySrc, setNewGlossarySrc] = useState('en'); + const [newGlossaryDst, setNewGlossaryDst] = useState('fr'); + const [newGlossaryEntries, setNewGlossaryEntries] = useState(''); // CSV format: term,translation + + useEffect(() => { + loadGlossaries(); + }, []); + + const loadGlossaries = async () => { + setLoadingGlossaries(true); + try { + const data = await fetchGlossaries(); + setGlossaries(data); + } catch (e) { + toast.error('Failed to load glossaries'); + } finally { + setLoadingGlossaries(false); + } + }; + + const handleTranslate = async () => { + if (!text) return; + setIsTranslating(true); + try { + const res = await translateText(text, srcLang, dstLang, selectedGlossaryId === 'none' ? undefined : selectedGlossaryId); + setTranslation(res.translation); + } catch (e) { + console.error(e); + toast.error('Translation failed'); + } finally { + setIsTranslating(false); + } + }; + + const handleCreateGlossary = async () => { + if (!newGlossaryName || !newGlossaryEntries) return; + + const entries: Record = {}; + newGlossaryEntries.split('\n').forEach(line => { + const parts = line.split(','); + if (parts.length >= 2) { + const term = parts[0].trim(); + const trans = parts.slice(1).join(',').trim(); // Handle commas in translation? Simple CSV logic. + if (term && trans) entries[term] = trans; + } + }); + + if (Object.keys(entries).length === 0) { + toast.error('No valid entries found'); + return; + } + + try { + await createGlossary(newGlossaryName, newGlossarySrc, newGlossaryDst, entries); + toast.success('Glossary created'); + loadGlossaries(); + setNewGlossaryName(''); + setNewGlossaryEntries(''); + } catch (e: any) { + toast.error(`Failed to create glossary: ${e.message}`); + } + }; + + const handleDeleteGlossary = async (id: string) => { + try { + await deleteGlossary(id); + toast.success('Glossary deleted'); + loadGlossaries(); + if (selectedGlossaryId === id) setSelectedGlossaryId(''); + } catch (e) { + toast.error('Failed to delete glossary'); + } + }; + + return ( +
+

i18n / DeepL Playground

+ +
+ {/* Translation Section */} + + + Translation + + +
+
+ + setSrcLang(e.target.value)} placeholder="en" /> +
+
+ + +
+
+ +
+ + +
+ +