feat: Implement Prompt Builder functionality in Popup and Options
Some checks failed
CI — Test & Build / Test & Build (push) Failing after 39s
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:
@@ -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}` };
|
||||
|
||||
Reference in New Issue
Block a user