feat: Implement Prompt Builder functionality in Popup and Options
Some checks failed
CI — Test & Build / Test & Build (push) Failing after 39s

- Added a new "Prompt Builder" tab in the Popup for generating AI prompts with customizable parameters.
- Introduced new state variables for managing prompt styles, personas, formats, and models.
- Enhanced the Options page to fetch and display models based on the provided API key.
- Updated the actions and types to include the new 'prompt' action and its associated parameters.
- Implemented migration logic for legacy plaintext API keys to encrypted storage.
- Updated the getSystemPrompt function to incorporate prompt parameters for better instruction generation.
- Added tests for the new functionality, including context menu entries and prompt generation logic.
This commit is contained in:
john kevin asprec
2026-07-15 15:27:41 +08:00
parent 0fef9848cb
commit acea99d7ad
40 changed files with 1971 additions and 177 deletions

View File

@@ -39,11 +39,21 @@ describe('callProvider request shapes (parity with old implementations)', () =>
{ role: 'system', content: 'SYS' },
{ role: 'user', content: 'hello' },
],
max_tokens: 1024,
max_completion_tokens: 1024,
temperature: 0.7,
});
});
it('OpenAI reasoning models (o-series / gpt-5): max_completion_tokens, NO temperature', async () => {
const fetch = mockFetchOnce(openAiResponse);
await callProvider({ provider: 'openai', apiKey: 'sk-x', model: 'gpt-5-mini' }, 'hello', 'SYS', { maxTokens: 1024 });
const body = JSON.parse(fetch.mock.calls[0][1].body);
expect(body.max_completion_tokens).toBe(1024);
expect(body.max_tokens).toBeUndefined();
expect(body.temperature).toBeUndefined();
});
it('Anthropic: x-api-key + version headers, top-level system, no temperature', async () => {
const fetch = mockFetchOnce(anthropicResponse);
const res = await callProvider({ provider: 'anthropic', apiKey: 'sk-ant' }, 'hello', 'SYS', { maxTokens: 1024 });
@@ -126,6 +136,16 @@ describe('callProvider error handling (parity with old implementations)', () =>
expect(res).toEqual({ error: 'OpenRouter returned an empty response.' });
});
it('returns a clean HTTP error when the error body is not JSON (e.g. HTML 502)', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: false,
status: 502,
json: async () => { throw new SyntaxError('Unexpected token < in JSON'); },
}));
const res = await callProvider({ provider: 'openai', apiKey: 'k' }, 'x', 'SYS');
expect(res).toEqual({ error: 'OpenAI error: HTTP 502' });
});
it('rejects unknown providers without fetching', async () => {
const fetch = mockFetchOnce({});
const res = await callProvider({ provider: 'bogus', apiKey: 'k' }, 'x', 'SYS');
@@ -155,6 +175,31 @@ describe('getSystemPrompt', () => {
expect(getSystemPrompt('rephrase', 'Default')).not.toContain('style.');
expect(getSystemPrompt('rephrase')).not.toContain('Write in a');
});
it("'prompt' uses the prompt-engineer prompt with a prompt-directed style modifier", () => {
expect(getSystemPrompt('prompt')).toContain('expert prompt engineer');
expect(getSystemPrompt('prompt', 'Formal')).toMatch(/instruct the model to respond in a formal style\.$/);
expect(getSystemPrompt('prompt', 'Formal')).not.toContain('Write in a');
expect(getSystemPrompt('prompt', 'Default')).toBe(getSystemPrompt('prompt'));
});
it('Prompt Builder params add instructions; Auto adds nothing', () => {
const base = getSystemPrompt('prompt');
expect(getSystemPrompt('prompt', undefined, { promptStyle: 'Auto', persona: 'Auto', format: 'Auto' })).toBe(base);
const full = getSystemPrompt('prompt', undefined, {
promptStyle: 'Few-shot',
persona: 'Data Analyst',
format: 'JSON',
});
expect(full).toContain('few-shot');
expect(full).toContain('persona of Data Analyst');
expect(full).toContain('final output as json');
expect(getSystemPrompt('prompt', undefined, { persona: 'None' })).toContain('Do not assign a persona');
// Params are prompt-action-only — other actions ignore them.
expect(getSystemPrompt('rephrase', undefined, { persona: 'Teacher' })).toBe(getSystemPrompt('rephrase'));
});
});
describe('defaultMaxTokens', () => {