diff --git a/demo/.env.example b/demo/.env.example new file mode 100644 index 0000000..5afabdc --- /dev/null +++ b/demo/.env.example @@ -0,0 +1,4 @@ +# Optional. A projectId from https://console.unlayer.com/ with the AI +# Assistant feature enabled. The demo hides the AI controls when this is +# unset, so the rest of the demo works without an account. +VITE_UNLAYER_PROJECT_ID= diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 8444ef1..699fc35 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -20,6 +20,15 @@ const MAX_UPLOAD_BYTES = 20 * 1024 * 1024; // overlay. Starting it open at these widths hides the editor behind it. const WIDE_VIEWPORT = '(min-width: 768px)'; +// The AI Assistant needs a projectId with the feature enabled, so the demo +// only offers it when one is configured. See demo/.env.example. +const PROJECT_ID = Number(import.meta.env.VITE_UNLAYER_PROJECT_ID) || undefined; + +// Demonstrates features.imageEditor.tools..icon. Raw markup is +// the form that works today; see #64 for the Font Awesome name form. +const CUSTOM_TEXT_ICON = + ''; + const allToolsEnabled = () => Object.fromEntries(TOOL_NAMES.map((tool) => [tool, true])) as Record< ToolName, @@ -38,6 +47,7 @@ export default function App() { const [sidebarOpen, setSidebarOpen] = useState( () => window.matchMedia(WIDE_VIEWPORT).matches ); + const [ai, setAi] = useState(false); const [saved, setSaved] = useState(null); const [status, setStatus] = useState('Loading editor…'); @@ -94,12 +104,14 @@ export default function App() { setStatus(editor.hasChanges() ? 'Unsaved changes' : 'No unsaved changes'); }; - const snapshot = () => { + const snapshot = async () => { const dataUrl = editorRef.current?.editor?.getImage(); - if (dataUrl) { - setSaved({ dataUrl, blob: new Blob() }); - setStatus('Snapshot taken via getImage()'); - } + if (!dataUrl) return; + // Build the real blob rather than an empty one: this object is shaped + // like an ImageEditorSaveResult, so it should not lie about `blob`. + const blob = await (await fetch(dataUrl)).blob(); + setSaved({ dataUrl, blob }); + setStatus(`Snapshot taken via getImage() (${blob.size} bytes)`); }; const changeDock = (next: 'left' | 'right') => { @@ -147,6 +159,14 @@ export default function App() { onDockChange={changeDock} tools={tools} onToolToggle={toggleTool} + aiAvailable={PROJECT_ID !== undefined} + ai={ai} + onAiChange={(enabled) => { + setAi(enabled); + setStatus( + `AI Assistant ${enabled ? 'enabled' : 'disabled'} (remount)` + ); + }} onChangeImage={nextImage} onUploadImage={uploadImage} onCheckChanges={checkChanges} @@ -161,7 +181,24 @@ export default function App() { options={{ theme, locale, - features: { imageEditor: { dock, tools } }, + projectId: PROJECT_ID, + features: { + ai: { enabled: ai, assistant: ai }, + imageEditor: { + dock, + tools: { + ...tools, + // A tool entry can also be an object, which is how a + // custom icon is supplied. Raw markup, not a Font + // Awesome name — the name form is silently ignored + // (see #64). + text: { + enabled: tools.text, + icon: CUSTOM_TEXT_ICON, + }, + }, + }, + }, }} onLoad={() => setStatus('Editor ready')} onSave={(result) => { diff --git a/demo/src/Sidebar.tsx b/demo/src/Sidebar.tsx index 9552fd9..760ece9 100644 --- a/demo/src/Sidebar.tsx +++ b/demo/src/Sidebar.tsx @@ -4,6 +4,9 @@ import type { UnlayerLocale } from '@unlayer/types'; export const TOOL_NAMES = [ 'crop', + // Configured through features.imageEditor.tools like any other tool, but + // it is the rounded-corners control inside Crop rather than its own tab. + 'corners', 'resize', 'filter', 'draw', @@ -30,6 +33,9 @@ interface SidebarProps { onLocaleChange(locale: UnlayerLocale): void; dock: 'left' | 'right'; onDockChange(dock: 'left' | 'right'): void; + aiAvailable: boolean; + ai: boolean; + onAiChange(enabled: boolean): void; tools: Record; onToolToggle(tool: ToolName): void; onChangeImage(): void; @@ -113,6 +119,25 @@ export default function Sidebar(props: SidebarProps) { +
+

AI Assistant

+ {props.aiAvailable ? ( + + ) : ( +

+ Set VITE_UNLAYER_PROJECT_ID in demo/.env{' '} + to try the AI Assistant. See demo/.env.example. +

+ )} + +

Tools (remounts editor)

diff --git a/demo/src/styles.css b/demo/src/styles.css index a95dcd8..1913140 100644 --- a/demo/src/styles.css +++ b/demo/src/styles.css @@ -297,3 +297,17 @@ body, font-size: 14px; } } + +.hint { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: #8f96a3; +} + +.hint code { + font-size: 11px; + padding: 1px 4px; + border-radius: 4px; + background: #232732; +} diff --git a/demo/src/vite-env.d.ts b/demo/src/vite-env.d.ts index 11f02fe..fff2fb5 100644 --- a/demo/src/vite-env.d.ts +++ b/demo/src/vite-env.d.ts @@ -1 +1,10 @@ /// + +interface ImportMetaEnv { + /** Optional Unlayer projectId; enables the AI Assistant controls. */ + readonly VITE_UNLAYER_PROJECT_ID?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}