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

@@ -4,7 +4,7 @@
// headers, request bodies, default models, and error strings match the old
// implementations — pinned by tests/unit/providers.test.ts.
import type { LexAIConfig, LexAIResponse } from './types';
import type { LexAIConfig, LexAIResponse, PromptParams } from './types';
export async function fetchWithTimeout(
url: string,
@@ -22,7 +22,38 @@ export async function fetchWithTimeout(
// ─── System prompts ───────────────────────────────────────────────────────────
export function getSystemPrompt(action: string, style?: string): string {
// Prompt Builder parameters → extra instructions for the prompt-engineer
// action. 'Auto' (or absence) adds nothing — the base prompt already tells the
// engineer to decide these from the input.
function promptParamModifiers(params?: PromptParams): string {
if (!params) return '';
const parts: string[] = [];
const styleInstructions: Record<string, string> = {
Instructional: ' Compose the engineered prompt as clear, direct instructions.',
'Role-play': ' Compose the engineered prompt as a role-play scenario the model should stay in character for.',
'Step-by-step': ' The engineered prompt should ask the model to work through the task step by step before giving its final answer.',
'Few-shot': ' Include one or two short input→output examples (few-shot) in the engineered prompt.',
Structured: ' Organize the engineered prompt into labeled sections (Context, Task, Constraints, Output format).',
};
if (params.promptStyle && styleInstructions[params.promptStyle]) {
parts.push(styleInstructions[params.promptStyle]);
}
if (params.persona === 'None') {
parts.push(' Do not assign a persona or role in the engineered prompt.');
} else if (params.persona && params.persona !== 'Auto') {
parts.push(` The engineered prompt must assign the model the persona of ${params.persona.trim()}, including the skills and expertise that persona implies.`);
}
if (params.format && params.format !== 'Auto') {
parts.push(` The engineered prompt must require the final output as ${params.format.toLowerCase()}.`);
}
return parts.join('');
}
export function getSystemPrompt(action: string, style?: string, promptParams?: PromptParams): string {
// Normalize 'fix' (used by context menu and popup) to 'grammar'
const normalizedAction = action === 'fix' ? 'grammar' : action;
const prompts: Record<string, string> = {
@@ -46,12 +77,23 @@ export function getSystemPrompt(action: string, style?: string): string {
'You are a helpful teacher. Explain the following text in simple, easy-to-understand language. ' +
'Break down complex terms, jargon, or concepts so anyone can understand. ' +
'Be concise but clear. Return only the explanation, no extra commentary.',
prompt:
'You are an expert prompt engineer. Transform the provided text into one well-crafted prompt that will get the best possible result from an AI model. ' +
'First infer what the user is trying to achieve — the text may be a rough idea, a question, or a description of the output they want. ' +
'Then compose the prompt using only the components that goal actually needs: a persona/role and relevant skills when expertise helps, essential context, a clear task statement, constraints, and the desired output format or structure. ' +
'Make the prompt specific, self-contained, and unambiguous, and phrase it as instructions addressed to an AI model. ' +
'Return ONLY the engineered prompt, ready to paste into an AI chat — no explanations, no surrounding quotes, no preamble.',
};
const base = prompts[normalizedAction] ?? prompts.grammar;
// For the prompt-engineer action, the style describes the output the
// engineered prompt should ask for — not the wording of our response.
const styleModifier = style && style !== 'Default'
? ` Write in a ${style.toLowerCase()} style.`
? normalizedAction === 'prompt'
? ` The engineered prompt should instruct the model to respond in a ${style.toLowerCase()} style.`
: ` Write in a ${style.toLowerCase()} style.`
: '';
return base + styleModifier;
const paramModifiers = normalizedAction === 'prompt' ? promptParamModifiers(promptParams) : '';
return base + styleModifier + paramModifiers;
}
// ─── Adapter table ────────────────────────────────────────────────────────────
@@ -86,6 +128,26 @@ function openAiStyleBody(temperature?: number) {
const extractOpenAiStyle = (data: any): string | undefined => data?.choices?.[0]?.message?.content;
// OpenAI proper rejects the legacy `max_tokens` on newer models ("Use
// 'max_completion_tokens' instead") and reasoning models (o-series, gpt-5
// family) additionally reject any temperature other than the default.
// `max_completion_tokens` is accepted by all current OpenAI chat models, so
// send it unconditionally; temperature stays only for non-reasoning models.
// Groq/OpenRouter still expect `max_tokens` — don't apply this to them.
const OPENAI_REASONING_MODEL_RE = /^(o\d|gpt-5)/;
function openAiBody(model: string, systemPrompt: string, text: string, maxTokens: number) {
return {
model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: text },
],
max_completion_tokens: maxTokens,
...(OPENAI_REASONING_MODEL_RE.test(model) ? {} : { temperature: 0.7 }),
};
}
export const PROVIDER_SPECS: Record<string, ProviderSpec> = {
openai: {
label: 'OpenAI',
@@ -93,7 +155,7 @@ export const PROVIDER_SPECS: Record<string, ProviderSpec> = {
modelsUrl: 'https://api.openai.com/v1/models',
defaultModel: 'gpt-4o-mini',
headers: bearerHeaders,
body: openAiStyleBody(0.7),
body: openAiBody,
extract: extractOpenAiStyle,
},
anthropic: {
@@ -177,7 +239,9 @@ export async function callProvider(
return { error: `Network error reaching ${spec.label}: ${String(err)}` };
}
const data = await res.json();
// Guard the parse — gateways/proxies return HTML error pages (502 etc.)
// which would otherwise surface as a raw SyntaxError to the user.
const data = await res.json().catch(() => null);
if (!res.ok) {
const msg = data?.error?.message ?? `HTTP ${res.status}`;
return { error: `${spec.label} error: ${msg}` };