diff --git a/src/islands/agent/AskAgent.tsx b/src/islands/agent/AskAgent.tsx
index 9fd05db..5176adb 100644
--- a/src/islands/agent/AskAgent.tsx
+++ b/src/islands/agent/AskAgent.tsx
@@ -140,7 +140,7 @@ export default function AskAgent({ lang = 'en' }: { lang?: 'en' | 'id' }) {
{t.text}
- {t.imgUrl &&
}
+ {t.imgUrl &&
}
{(t.blobUrl || t.imgUrl) && (
Download
)}
diff --git a/src/tools/agent/executors.test.ts b/src/tools/agent/executors.test.ts
index cb85045..2f193ac 100644
--- a/src/tools/agent/executors.test.ts
+++ b/src/tools/agent/executors.test.ts
@@ -56,6 +56,11 @@ describe('executor registry', () => {
expect(scopeExecutors('crop this video').map(e => e.toolId)).toContain('media-trim');
expect(scopeExecutors('cut this mp3').map(e => e.toolId)).toContain('media-trim');
});
+ it('scopes svg generation (svg-viewer) for draw/create requests', () => {
+ expect(scopeExecutors('make a download icon').map(e => e.toolId)).toContain('svg-viewer');
+ expect(scopeExecutors('draw a flowchart of a login process').map(e => e.toolId)).toContain('svg-viewer');
+ expect(scopeExecutors('create an svg logo').map(e => e.toolId)).toContain('svg-viewer');
+ });
it('does not scope any media compressor for small talk', () => {
expect(scopeExecutors('hello how are you today')).toEqual([]);
});
diff --git a/src/tools/agent/executors.ts b/src/tools/agent/executors.ts
index 04426f6..f133514 100644
--- a/src/tools/agent/executors.ts
+++ b/src/tools/agent/executors.ts
@@ -374,6 +374,20 @@ export const AGENT_EXECUTORS: AgentExecutor[] = [
return { dataUrl: await QRCode.toDataURL(text), filename: 'qr.png', text: 'made a QR code' };
},
},
+ {
+ // Generative: the model itself "draws" the icon/diagram by writing SVG markup;
+ // we sanitize it and hand back a rendered, downloadable graphic. Mapped to the
+ // real svg-viewer tool. Pass the full in the `svg` arg.
+ toolId: 'svg-viewer', description: 'Draw/create an SVG icon, logo, illustration, badge or simple diagram — YOU write the full markup and pass it as "svg".',
+ match: re(/\b(make|create|draw|generate|design|build|render)\b.*\b(icon|logo|svg|illustration|graphic|badge|emblem|diagram|flow ?chart|sketch|shape|avatar)\b|\bsvg\b.*(icon|logo|graphic|diagram|shape)/i),
+ files: [], params: [{ key: 'svg', type: 'string', label: 'SVG markup ()' }],
+ execute: async ({ params }) => {
+ const { sanitizeSvg, svgToDataUrl } = await import('@/tools/image/svg-gen.lib');
+ const clean = sanitizeSvg(String(params.svg ?? ''));
+ if (!clean) throw new Error('that was not valid SVG — write complete markup');
+ return { blob: new Blob([clean], { type: 'image/svg+xml' }), dataUrl: svgToDataUrl(clean), filename: 'graphic.svg', text: 'drew an SVG' };
+ },
+ },
{
toolId: 'image-compress', description: 'Compress an image to a target size in KB',
match: re(/(image|img|photo|pic(ture)?|jpe?g|png|webp).*(compress|small|reduce|shrink|kb|mb|size)|(compress|small|reduce|shrink).*(image|img|photo|pic(ture)?|jpe?g|png|webp)/i),
diff --git a/src/tools/image/svg-gen.lib.test.ts b/src/tools/image/svg-gen.lib.test.ts
new file mode 100644
index 0000000..2b5357c
--- /dev/null
+++ b/src/tools/image/svg-gen.lib.test.ts
@@ -0,0 +1,39 @@
+import { describe, it, expect } from 'vitest';
+import { extractSvg, sanitizeSvg, svgToDataUrl } from './svg-gen.lib';
+
+describe('extractSvg', () => {
+ it('pulls the svg out of a markdown code fence', () => {
+ const out = extractSvg('Sure!\n```svg\n\n```\nEnjoy');
+ expect(out).toBe('');
+ });
+ it('returns empty when there is no svg', () => {
+ expect(extractSvg('just some text')).toBe('');
+ });
+});
+
+describe('sanitizeSvg', () => {
+ it('keeps drawing elements', () => {
+ const out = sanitizeSvg('');
+ expect(out).toContain('