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:
@@ -1,7 +1,7 @@
|
||||
import { defineBackground } from 'wxt/utils/define-background';
|
||||
import type { AnalyzePayload, LexAIConfig, LexAIResponse } from '@lib/types';
|
||||
import { CONTEXT_MENU_ENTRIES, findContextMenuEntry } from '@lib/actions';
|
||||
import { decryptApiKey } from '@lib/crypto';
|
||||
import { CONTEXT_MENU_ENTRIES, findContextMenuEntry, resolvePromptPersona } from '@lib/actions';
|
||||
import { decryptApiKey, migratePlaintextApiKey } from '@lib/crypto';
|
||||
import { callProvider, getSystemPrompt, listModels } from '@lib/providers';
|
||||
|
||||
// Resolve the usable API key from stored config: prefer the encrypted path,
|
||||
@@ -22,13 +22,35 @@ async function handleAnalyzeText(payload: AnalyzePayload): Promise<LexAIResponse
|
||||
const stored = await chrome.storage.local.get(['provider', 'apiKey', 'apiKeyEnc', 'encKey', 'model']);
|
||||
const config = stored as LexAIConfig;
|
||||
|
||||
// Prompt requests from the toolbar/context menu carry no explicit params —
|
||||
// apply the Prompt Builder settings saved from the popup so all entry
|
||||
// points behave the same. The popup still overrides by sending its own.
|
||||
if (payload.action === 'prompt' && !payload.promptParams) {
|
||||
const saved = (await chrome.storage.local.get([
|
||||
'promptStyle', 'promptPersona', 'customPersona', 'promptFormat', 'promptModel',
|
||||
])) as Record<string, string | undefined>;
|
||||
payload = {
|
||||
...payload,
|
||||
promptParams: {
|
||||
promptStyle: saved.promptStyle,
|
||||
persona: resolvePromptPersona(saved.promptPersona, saved.customPersona),
|
||||
format: saved.promptFormat,
|
||||
},
|
||||
model: payload.model ?? (saved.promptModel || undefined),
|
||||
};
|
||||
}
|
||||
|
||||
const apiKey = await resolveApiKey(config);
|
||||
if (!apiKey) {
|
||||
return { error: 'No API key configured. Please open LexAI settings (click the extension icon).' };
|
||||
}
|
||||
|
||||
const resolvedConfig: LexAIConfig = { ...config, apiKey };
|
||||
const systemPrompt = getSystemPrompt(payload.action, payload.style);
|
||||
const resolvedConfig: LexAIConfig = {
|
||||
...config,
|
||||
apiKey,
|
||||
...(payload.model ? { model: payload.model } : {}),
|
||||
};
|
||||
const systemPrompt = getSystemPrompt(payload.action, payload.style, payload.promptParams);
|
||||
return callProvider(resolvedConfig, payload.text, systemPrompt);
|
||||
}
|
||||
|
||||
@@ -37,6 +59,10 @@ async function handleAnalyzeText(payload: AnalyzePayload): Promise<LexAIResponse
|
||||
export default defineBackground(() => {
|
||||
console.log('LexAI background service worker started');
|
||||
|
||||
// Encrypt any legacy plaintext apiKey left by older builds (no-op otherwise).
|
||||
// The read path keeps its plaintext fallback, so a failed migration is safe.
|
||||
migratePlaintextApiKey().catch(() => {});
|
||||
|
||||
// ─── Context menus ───────────────────────────────────────────────────────
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.removeAll(() => {
|
||||
@@ -64,7 +90,11 @@ export default defineBackground(() => {
|
||||
});
|
||||
|
||||
// ─── Message handler ─────────────────────────────────────────────────────
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
// Only our own contexts (content scripts, popup, options) may drive the
|
||||
// key-bearing call path — ignore anything from another extension.
|
||||
if (sender.id !== chrome.runtime.id) return;
|
||||
|
||||
if (message.type === 'ANALYZE_TEXT') {
|
||||
// Support both { payload: { text, action, style } } (content.ts) and
|
||||
// { text, action, style } (popup) formats
|
||||
@@ -72,6 +102,8 @@ export default defineBackground(() => {
|
||||
text: message.text as string,
|
||||
action: message.action as string,
|
||||
style: message.style as string | undefined,
|
||||
promptParams: message.promptParams,
|
||||
model: message.model as string | undefined,
|
||||
};
|
||||
handleAnalyzeText(payload)
|
||||
.then(sendResponse)
|
||||
@@ -99,10 +131,16 @@ export default defineBackground(() => {
|
||||
}
|
||||
|
||||
if (message.type === 'LIST_MODELS') {
|
||||
const provider = (message.provider as string) || 'openai';
|
||||
// Prefer an inline key (freshly typed, not yet saved); else use the stored key.
|
||||
const inlineKey = (message.apiKey as string | undefined)?.trim() || undefined;
|
||||
(async () => {
|
||||
// Callers that don't know the provider (content script) omit it —
|
||||
// fall back to the configured one.
|
||||
let provider = message.provider as string | undefined;
|
||||
if (!provider) {
|
||||
const stored = await chrome.storage.local.get('provider');
|
||||
provider = (stored.provider as string) || 'openai';
|
||||
}
|
||||
let apiKey = inlineKey;
|
||||
if (!apiKey) {
|
||||
const stored = await chrome.storage.local.get(['apiKey', 'apiKeyEnc', 'encKey']);
|
||||
|
||||
Reference in New Issue
Block a user