Repository navigation
Add developer toolbar and AI generation tools - #9
Conversation
Register five new tools in the AI category, each following the existing tool layout with a service, unit tests and an e2e spec. Services call the Vercel AI Gateway when a key is available and otherwise fall back to a local deterministic generator, so every tool stays usable without any credentials. The key can be entered per browser and takes precedence over the optional VITE_AI_GATEWAY_API_KEY build-time variable, which keeps secrets out of the public bundle.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's GuideThe PR adds a globally mounted Vercel-inspired developer toolbar and extension redesign, exposes searchable shortcuts for newly registered AI tools, and implements configurable AI Gateway integration with local deterministic or browser-based fallbacks plus unit and end-to-end coverage. Sequence diagram for AI generation with gateway fallbacksequenceDiagram
actor User
participant Toolbar as VercelToolbar
participant Tool as GenerationTool
participant Service as GenerationService
participant Gateway as AI_Gateway
participant Fallback as Local_or_browser_fallback
User->>Toolbar: Search or select AI tool
Toolbar->>Tool: router.push(path)
User->>Tool: Submit prompt
Tool->>Service: generateText/generateObject/generateImage/generateSpeech/generateVideo
Service->>Gateway: callAiGateway(request)
alt gateway succeeds
Gateway-->>Service: Generated result
else key missing or request fails
Service->>Fallback: Build demo result or browser preview
Fallback-->>Service: Local result
end
Service-->>Tool: Result and source
Tool-->>User: Display generated output
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 11 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/composable/aiGatewayKey.ts" line_range="9" />
<code_context>
+ * Key baked at build time, for self-hosted deployments where the site operator owns the key.
+ * It is public by nature, so a key entered by the user always takes precedence over it.
+ */
+const envApiKey = (import.meta.env.VITE_AI_GATEWAY_API_KEY as string | undefined) ?? '';
+
+/** Key entered by the user in the interface, kept in local storage and never shipped in the bundle */
</code_context>
<issue_to_address>
**Deployment key exposed**
When the deployment sets `VITE_AI_GATEWAY_API_KEY` to a credential intended to remain secret, vite embeds the key in the client bundle, and `callAiGateway` sends it as a Bearer token, so any site visitor can extract and reuse it to consume the deployment’s AI Gateway quota.
Keep deployment credentials server-side and route Gateway requests through a server endpoint instead of exposing the key in the client bundle.
</issue_to_address>
### Comment 2
<location path="vite.config.ts" line_range="24" />
<code_context>
// https://vitejs.dev/config/
export default defineConfig({
+ server: {
+ allowedHosts: true,
+ },
plugins: [
</code_context>
<issue_to_address>
**Development server exposed**
When a developer runs the Vite server on a reachable interface while an attacker can use DNS rebinding, `allowedHosts: true` accepts arbitrary Host headers, allowing DNS rebinding to reach the development server and access served source or other dev-server resources.
Replace `allowedHosts: true` with an explicit allowlist of trusted hosts.
</issue_to_address>
### Comment 3
<location path="src/utils/ai-gateway.ts" line_range="33" />
<code_context>
+ throw new Error('No AI Gateway API key configured');
+ }
+
+ const response = await fetch(`${aiGatewayBaseUrl}/${request.modality}`, {
+ method: 'POST',
+ headers: {
</code_context>
<issue_to_address>
**Gateway generation fails**
When a user configures a key and tries to generate content, `callAiGateway` sends modality-specific `/v1/{modality}` requests with a custom `{ modality, model, prompt }` body instead of the Gateway’s supported inference endpoint and request format. The requests fail, and the generation services return demo output instead of generated content.
Use the Gateway’s supported inference endpoint and request format for the generation requests.
</issue_to_address>
### Comment 4
<location path="src/tools/generate-speech/generate-speech.vue" line_range="52" />
<code_context>
+ isGenerating.value = false;
+ }
+
+ await play();
+}
+
</code_context>
<issue_to_address>
**Generated audio is ignored**
When the Gateway returns generated audio, `onGenerate` calls `play()`, which uses browser speech synthesis on the text instead of playing the generated `audio`. Users hear browser-generated speech rather than the selected model’s output.
Play the generated audio when it is available, and use browser speech synthesis only when no generated audio is available.
Also at `src/tools/generate-speech/generate-speech.vue:58`.
</issue_to_address>
### Comment 5
<location path="src/tools/generate-image/generate-image.service.ts" line_range="127" />
<code_context>
+ size: options.size,
+ });
+
+ const image = String(data.image ?? data.url ?? '');
+
+ if (image === '') {
</code_context>
<issue_to_address>
**Remote media is unusable**
When the Gateway returns a remote media URL instead of inline base64 data, the image and video download composables treat remote URLs as base64, producing invalid downloads, while the speech service also treats URLs as base64, so its audio cannot play.
Handle remote media URLs as URLs instead of passing them to base64-only download and playback paths.
Also at `src/tools/generate-speech/generate-speech.service.ts:103`, `src/tools/generate-video/generate-video.service.ts:100`.
</issue_to_address>
### Comment 6
<location path="src/tools/generate-object/generate-object.service.ts" line_range="131" />
<code_context>
if (item.dataset.path !== '') openTool(item.dataset.path);
- else openTool('');
-});
+}
+
+toolSections.addEventListener('click', handleToolClick);
</code_context>
<issue_to_address>
**Structured results are discarded**
When the Gateway returns `object` as an object or `scenes` as an array, the object and video services convert structured values to strings before parsing them, so parsing throws and the catch blocks replace valid Gateway results with demo output; the video fallback also discards the generated video.
Parse native structured values directly, and only parse JSON strings when the Gateway returns text.
Also at `src/tools/generate-video/generate-video.service.ts:106`.
</issue_to_address>
### Comment 7
<location path="src/tools/generate-text/generate-text.service.ts" line_range="80" />
<code_context>
+ prompt: buildTextPrompt(options),
+ });
+
+ return { source: 'gateway', text: String(data.text ?? '') };
+ }
+ catch {
</code_context>
<issue_to_address>
**Text result is empty**
When the Gateway responds successfully without non-empty text, `generateText` returns an empty string as a successful Gateway result, so the UI hides the result card and shows no explanation.
Treat missing or empty `data.text` as an unusable response and return an appropriate failure or fallback result.
</issue_to_address>
### Comment 8
<location path="src/tools/generate-speech/generate-speech.service.ts" line_range="56" />
<code_context>
+ return Promise.resolve(false);
+ }
+
+ synthesis.cancel();
+
+ return new Promise((resolve) => {
</code_context>
<issue_to_address>
**Stop control disables early**
When the user starts another playback before the previous utterance finishes, a canceled utterance can finish after a newer one starts, and its `play()` completion sets `isSpeaking` to false while the newer utterance is still speaking. The Stop button then becomes disabled.
Ensure only the current playback can update `isSpeaking` when its promise completes.
Also at `src/tools/generate-speech/generate-speech.vue:61`.
</issue_to_address>
### Comment 9
<location path="src/tools/generate-text/generate-text.service.ts" line_range="82" />
<code_context>
+
+ return { source: 'gateway', image };
+ }
+ catch {
+ return { source: 'demo', image: buildDemoImage(options) };
+ }
</code_context>
<issue_to_address>
**Gateway failures lose diagnostic details**
When a Gateway request fails or returns an unusable response, each service catches Gateway, parsing, and response errors without logging or reporting them, then returns a demo result. The UI only identifies the result as locally generated, leaving operators without the cause or any signal that real generation is failing.
Report the failure through observable client-side telemetry or an error state while retaining the demo fallback.
Also at `src/tools/generate-object/generate-object.service.ts:132`, `src/tools/generate-image/generate-image.service.ts:135`, `src/tools/generate-speech/generate-speech.service.ts:111`, `src/tools/generate-video/generate-video.service.ts:108`.
</issue_to_address>
### Comment 10
<location path="src/tools/generate-video/generate-video.service.ts" line_range="60" />
<code_context>
+ const subject = keywords[0] ?? 'the subject';
+ const detail = keywords[1] ?? keywords[0] ?? 'the setting';
+ const sceneCount = countScenes(duration);
+ const scenesDuration = Math.round((duration / sceneCount) * 10) / 10;
+
+ return Array.from({ length: sceneCount }, (_, index): VideoScene => {
</code_context>
<issue_to_address>
**Storyboard duration is wrong**
When the local storyboard is generated for an 8- or 10-second duration, `buildDemoStoryboard` rounds each scene duration to a tenth and uses that value for every scene, so the displayed timeline totals 8.1 or 9.9 seconds instead of the selected duration.
Distribute scene durations so their total matches the requested duration.
</issue_to_address>
### Comment 11
<location path="src/tools/generate-object/generate-object.e2e.spec.ts" line_range="15" />
<code_context>
+ test('Generates a JSON object', async ({ page }) => {
+ await page.getByRole('button', { name: 'Generate object' }).click();
+
+ await expect(page.getByText('Generated object')).toBeVisible();
+ });
+
</code_context>
<issue_to_address>
**Empty object passes generation test**
When `generateObject` returns an empty object, the “Generates a JSON object” test only checks that the result card title is visible; `onGenerate` serializes `{}` to the truthy string `'{}'`, which renders that card, so the test passes even though no requested fields were generated.
Assert that the rendered JSON contains at least the requested `title` field and a value.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 11 findings to address first, and the change sends bearer API keys from the browser to a new external AI Gateway and stores user-entered keys in localStorage; a deployment key is also compiled into the client bundle. If that handling is wrong, keys may already be exposed or misused before a revert, so reverting does not undo the resulting credential exposure.
Blocking findings: src/composable/aiGatewayKey.ts:9, vite.config.ts:24, src/utils/ai-gateway.ts:33, src/tools/generate-speech/generate-speech.vue:52, src/tools/generate-image/generate-image.service.ts:127, and 6 more
| * Key baked at build time, for self-hosted deployments where the site operator owns the key. | ||
| * It is public by nature, so a key entered by the user always takes precedence over it. | ||
| */ | ||
| const envApiKey = (import.meta.env.VITE_AI_GATEWAY_API_KEY as string | undefined) ?? ''; |
There was a problem hiding this comment.
🔴 Critical · Deployment key exposed
When the deployment sets VITE_AI_GATEWAY_API_KEY to a credential intended to remain secret, vite embeds the key in the client bundle, and callAiGateway sends it as a Bearer token, so any site visitor can extract and reuse it to consume the deployment’s AI Gateway quota.
Keep deployment credentials server-side and route Gateway requests through a server endpoint instead of exposing the key in the client bundle.
Prompt for AI agents
In `src/composable/aiGatewayKey.ts` at line 9:
**Deployment key exposed**
When the deployment sets `VITE_AI_GATEWAY_API_KEY` to a credential intended to remain secret, vite embeds the key in the client bundle, and `callAiGateway` sends it as a Bearer token, so any site visitor can extract and reuse it to consume the deployment’s AI Gateway quota.
Keep deployment credentials server-side and route Gateway requests through a server endpoint instead of exposing the key in the client bundle.| // https://vitejs.dev/config/ | ||
| export default defineConfig({ | ||
| server: { | ||
| allowedHosts: true, |
There was a problem hiding this comment.
🟠 High · Development server exposed
When a developer runs the Vite server on a reachable interface while an attacker can use DNS rebinding, allowedHosts: true accepts arbitrary Host headers, allowing DNS rebinding to reach the development server and access served source or other dev-server resources.
Replace allowedHosts: true with an explicit allowlist of trusted hosts.
Prompt for AI agents
In `vite.config.ts` at line 24:
**Development server exposed**
When a developer runs the Vite server on a reachable interface while an attacker can use DNS rebinding, `allowedHosts: true` accepts arbitrary Host headers, allowing DNS rebinding to reach the development server and access served source or other dev-server resources.
Replace `allowedHosts: true` with an explicit allowlist of trusted hosts.| throw new Error('No AI Gateway API key configured'); | ||
| } | ||
|
|
||
| const response = await fetch(`${aiGatewayBaseUrl}/${request.modality}`, { |
There was a problem hiding this comment.
🟠 High · Gateway generation fails
When a user configures a key and tries to generate content, callAiGateway sends modality-specific /v1/{modality} requests with a custom { modality, model, prompt } body instead of the Gateway’s supported inference endpoint and request format. The requests fail, and the generation services return demo output instead of generated content.
Use the Gateway’s supported inference endpoint and request format for the generation requests.
Prompt for AI agents
In `src/utils/ai-gateway.ts` at line 33:
**Gateway generation fails**
When a user configures a key and tries to generate content, `callAiGateway` sends modality-specific `/v1/{modality}` requests with a custom `{ modality, model, prompt }` body instead of the Gateway’s supported inference endpoint and request format. The requests fail, and the generation services return demo output instead of generated content.
Use the Gateway’s supported inference endpoint and request format for the generation requests.| isGenerating.value = false; | ||
| } | ||
|
|
||
| await play(); |
There was a problem hiding this comment.
🟠 High · Generated audio is ignored
When the Gateway returns generated audio, onGenerate calls play(), which uses browser speech synthesis on the text instead of playing the generated audio. Users hear browser-generated speech rather than the selected model’s output.
Play the generated audio when it is available, and use browser speech synthesis only when no generated audio is available.
Also at src/tools/generate-speech/generate-speech.vue:58.
Prompt for AI agents
In `src/tools/generate-speech/generate-speech.vue` at line 52:
**Generated audio is ignored**
When the Gateway returns generated audio, `onGenerate` calls `play()`, which uses browser speech synthesis on the text instead of playing the generated `audio`. Users hear browser-generated speech rather than the selected model’s output.
Play the generated audio when it is available, and use browser speech synthesis only when no generated audio is available.
Also at `src/tools/generate-speech/generate-speech.vue:58`.| size: options.size, | ||
| }); | ||
|
|
||
| const image = String(data.image ?? data.url ?? ''); |
There was a problem hiding this comment.
🟠 High · Remote media is unusable
When the Gateway returns a remote media URL instead of inline base64 data, the image and video download composables treat remote URLs as base64, producing invalid downloads, while the speech service also treats URLs as base64, so its audio cannot play.
Handle remote media URLs as URLs instead of passing them to base64-only download and playback paths.
Also at src/tools/generate-speech/generate-speech.service.ts:103, src/tools/generate-video/generate-video.service.ts:100.
Prompt for AI agents
In `src/tools/generate-image/generate-image.service.ts` at line 127:
**Remote media is unusable**
When the Gateway returns a remote media URL instead of inline base64 data, the image and video download composables treat remote URLs as base64, producing invalid downloads, while the speech service also treats URLs as base64, so its audio cannot play.
Handle remote media URLs as URLs instead of passing them to base64-only download and playback paths.
Also at `src/tools/generate-speech/generate-speech.service.ts:103`, `src/tools/generate-video/generate-video.service.ts:100`.| prompt: buildTextPrompt(options), | ||
| }); | ||
|
|
||
| return { source: 'gateway', text: String(data.text ?? '') }; |
There was a problem hiding this comment.
🟡 Medium · Text result is empty
When the Gateway responds successfully without non-empty text, generateText returns an empty string as a successful Gateway result, so the UI hides the result card and shows no explanation.
Treat missing or empty data.text as an unusable response and return an appropriate failure or fallback result.
Prompt for AI agents
In `src/tools/generate-text/generate-text.service.ts` at line 80:
**Text result is empty**
When the Gateway responds successfully without non-empty text, `generateText` returns an empty string as a successful Gateway result, so the UI hides the result card and shows no explanation.
Treat missing or empty `data.text` as an unusable response and return an appropriate failure or fallback result.| return Promise.resolve(false); | ||
| } | ||
|
|
||
| synthesis.cancel(); |
There was a problem hiding this comment.
🟡 Medium · Stop control disables early
When the user starts another playback before the previous utterance finishes, a canceled utterance can finish after a newer one starts, and its play() completion sets isSpeaking to false while the newer utterance is still speaking. The Stop button then becomes disabled.
Ensure only the current playback can update isSpeaking when its promise completes.
Also at src/tools/generate-speech/generate-speech.vue:61.
Prompt for AI agents
In `src/tools/generate-speech/generate-speech.service.ts` at line 56:
**Stop control disables early**
When the user starts another playback before the previous utterance finishes, a canceled utterance can finish after a newer one starts, and its `play()` completion sets `isSpeaking` to false while the newer utterance is still speaking. The Stop button then becomes disabled.
Ensure only the current playback can update `isSpeaking` when its promise completes.
Also at `src/tools/generate-speech/generate-speech.vue:61`.|
|
||
| return { source: 'gateway', text: String(data.text ?? '') }; | ||
| } | ||
| catch { |
There was a problem hiding this comment.
🟡 Medium · Gateway failures lose diagnostic details
When a Gateway request fails or returns an unusable response, each service catches Gateway, parsing, and response errors without logging or reporting them, then returns a demo result. The UI only identifies the result as locally generated, leaving operators without the cause or any signal that real generation is failing.
Report the failure through observable client-side telemetry or an error state while retaining the demo fallback.
Also at src/tools/generate-object/generate-object.service.ts:132, src/tools/generate-image/generate-image.service.ts:135, src/tools/generate-speech/generate-speech.service.ts:111, src/tools/generate-video/generate-video.service.ts:108.
Prompt for AI agents
In `src/tools/generate-text/generate-text.service.ts` at line 82:
**Gateway failures lose diagnostic details**
When a Gateway request fails or returns an unusable response, each service catches Gateway, parsing, and response errors without logging or reporting them, then returns a demo result. The UI only identifies the result as locally generated, leaving operators without the cause or any signal that real generation is failing.
Report the failure through observable client-side telemetry or an error state while retaining the demo fallback.
Also at `src/tools/generate-object/generate-object.service.ts:132`, `src/tools/generate-image/generate-image.service.ts:135`, `src/tools/generate-speech/generate-speech.service.ts:111`, `src/tools/generate-video/generate-video.service.ts:108`.| const subject = keywords[0] ?? 'the subject'; | ||
| const detail = keywords[1] ?? keywords[0] ?? 'the setting'; | ||
| const sceneCount = countScenes(duration); | ||
| const scenesDuration = Math.round((duration / sceneCount) * 10) / 10; |
There was a problem hiding this comment.
🟡 Medium · Storyboard duration is wrong
When the local storyboard is generated for an 8- or 10-second duration, buildDemoStoryboard rounds each scene duration to a tenth and uses that value for every scene, so the displayed timeline totals 8.1 or 9.9 seconds instead of the selected duration.
Distribute scene durations so their total matches the requested duration.
Prompt for AI agents
In `src/tools/generate-video/generate-video.service.ts` at line 60:
**Storyboard duration is wrong**
When the local storyboard is generated for an 8- or 10-second duration, `buildDemoStoryboard` rounds each scene duration to a tenth and uses that value for every scene, so the displayed timeline totals 8.1 or 9.9 seconds instead of the selected duration.
Distribute scene durations so their total matches the requested duration.| test('Generates a JSON object', async ({ page }) => { | ||
| await page.getByRole('button', { name: 'Generate object' }).click(); | ||
|
|
||
| await expect(page.getByText('Generated object')).toBeVisible(); |
There was a problem hiding this comment.
🟡 Medium · Empty object passes generation test
When generateObject returns an empty object, the “Generates a JSON object” test only checks that the result card title is visible; onGenerate serializes {} to the truthy string '{}', which renders that card, so the test passes even though no requested fields were generated.
Assert that the rendered JSON contains at least the requested title field and a value.
Prompt for AI agents
In `src/tools/generate-object/generate-object.e2e.spec.ts` at line 15:
**Empty object passes generation test**
When `generateObject` returns an empty object, the “Generates a JSON object” test only checks that the result card title is visible; `onGenerate` serializes `{}` to the truthy string `'{}'`, which renders that card, so the test passes even though no requested fields were generated.
Assert that the rendered JSON contains at least the requested `title` field and a value.
v0 Session
Summary by Sourcery
Integrate a Vercel-inspired developer toolbar and AI generation suite into the extension and web application.
New Features:
Enhancements:
Build:
Tests: