From 0b0e1853c2df5047264f699f98da9ba0f34d3ec4 Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Fri, 4 Sep 2026 12:59:07 +0530 Subject: [PATCH] feat(demo): exercise AI Assistant, corners and custom tool icons The demo covered theme, locale and the eight tool toggles, leaving several documented options with nothing exercising them. Adds an AI Assistant toggle, gated on a VITE_UNLAYER_PROJECT_ID read from demo/.env.example so the rest of the demo still works without an account; a corners toggle; and a custom icon on the text tool, which demonstrates the object form of a tool entry. Also fixes Snapshot, which fabricated `blob: new Blob()`. The object is shaped like an ImageEditorSaveResult and people read the demo to learn the API, so it should not lie about a field. It now builds the real blob. The dock control originally in this branch is gone: #30 landed the same control on main first, so this uses main's version and keeps its section wording. --- demo/.env.example | 4 ++++ demo/src/App.tsx | 49 ++++++++++++++++++++++++++++++++++++------ demo/src/Sidebar.tsx | 25 +++++++++++++++++++++ demo/src/styles.css | 14 ++++++++++++ demo/src/vite-env.d.ts | 9 ++++++++ 5 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 demo/.env.example 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; +}