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(' from the svg', () => { + const out = sanitizeSvg(''); + expect(out).not.toMatch(/