Use when working on context menus, prompt injection, token bridge, popup behavior, or installationManager. Load alongside extension-mv3.
onInstalled — не при всяко SW събитиеwaitForAuth() задължително преди всяко context menu действиеinstallationManager отваря login page само при нова инсталация — не при updateprompt-inject.ts се зарежда на ВСИЧКИ платформи (вж. manifest)1. Клик в чат (не textarea, не selection)
└── "💾 Save Chat to BrainBox"
2. Клик в textarea (editable field)
└── "⚡ Inject Prompt"
├── Last used → prompt 1..7
├── ─────────
├── 📁 Folder 1 (от Settings/cache)
├── 📁 Folder 2
└── 📁 Folder 3
3. Клик на маркиран текст (selection)
└── "✨ BrainBox"
├── 💾 Save as Prompt
├── 📌 Save as Chunk
└── 🔮 Enhance →
├── Структурирай
├── Направи по-ясен
├── Разшири
└── Съкрати
// src/background/modules/context-menu-manager.ts
// (Blueprint-ът го нарича dynamicMenus.ts — тук следваме конвенцията на текущия проект)
export async function setupContextMenus(): Promise<void> {
// 1. Save Chat — за AI платформи (не selection, не textarea)
chrome.contextMenus.create({
id: 'SAVE_CHAT',
title: '💾 Save Chat to BrainBox',
contexts: ['page'],
documentUrlPatterns: [
'https://chatgpt.com/*', 'https://chat.openai.com/*',
'https://claude.ai/*', 'https://gemini.google.com/*',
'https://chat.deepseek.com/*', 'https://www.perplexity.ai/*',
'https://grok.com/*', 'https://chat.qwen.ai/*',
],
})
// 2. Inject Prompt — в textarea
chrome.contextMenus.create({
id: 'INJECT_PROMPT_PARENT',
title: '⚡ Inject Prompt',
contexts: ['editable'],
})
// Зареди prompts от cache
const { brainbox_prompts_cache: prompts = [] } = await chrome.storage.local.get('brainbox_prompts_cache')
const contextPrompts = prompts.filter((p: { use_in_context_menu: boolean }) => p.use_in_context_menu).slice(0, 7)
for (const prompt of contextPrompts) {
chrome.contextMenus.create({
id: `INJECT_${prompt.id}`,
parentId: 'INJECT_PROMPT_PARENT',
title: prompt.title,
contexts: ['editable'],
})
}
// 3. Selection actions
chrome.contextMenus.create({
id: 'BB_SELECTION_PARENT',
title: '✨ BrainBox',
contexts: ['selection'],
})
chrome.contextMenus.create({ id: 'SAVE_AS_PROMPT', parentId: 'BB_SELECTION_PARENT', title: '💾 Save as Prompt', contexts: ['selection'] })
chrome.contextMenus.create({ id: 'SAVE_AS_CHUNK', parentId: 'BB_SELECTION_PARENT', title: '📌 Save as Chunk', contexts: ['selection'] })
const enhanceParent = chrome.contextMenus.create({ id: 'ENHANCE_PARENT', parentId: 'BB_SELECTION_PARENT', title: '🔮 Enhance →', contexts: ['selection'] })
for (const style of ['Структурирай', 'Направи по-ясен', 'Разшири', 'Съкрати']) {
chrome.contextMenus.create({ id: `ENHANCE_${style}`, parentId: 'ENHANCE_PARENT', title: style, contexts: ['selection'] })
}
}
export async function handleContextMenuClick(
info: chrome.contextMenus.OnClickData,
tab?: chrome.tabs.Tab
): Promise<void> {
if (!tab?.id) return
const token = await waitForAuth()
if (!token) {
// Уведоми потребителя да се логне
chrome.notifications.create({ type: 'basic', iconUrl: 'src/icons/icon48.png', title: 'BrainBox', message: 'Please login first.' })
return
}
if (info.menuItemId === 'SAVE_CHAT') {
chrome.tabs.sendMessage(tab.id, { type: 'EXTRACT_CONVERSATION' })
} else if (String(info.menuItemId).startsWith('INJECT_')) {
const promptId = String(info.menuItemId).replace('INJECT_', '')
chrome.tabs.sendMessage(tab.id, { type: 'INJECT_PROMPT', promptId })
} else if (info.menuItemId === 'SAVE_AS_PROMPT') {
await saveAsPrompt(info.selectionText ?? '', token)
} else if (info.menuItemId === 'SAVE_AS_CHUNK') {
await saveAsChunk(info.selectionText ?? '', info.pageUrl ?? '', token)
} else if (String(info.menuItemId).startsWith('ENHANCE_')) {
const style = String(info.menuItemId).replace('ENHANCE_', '')
await enhanceSelection(info.selectionText ?? '', style, tab.id, token)
}
}
// Проверява 3 пъти с 500ms интервал — не блокира SW безкрайно
async function waitForAuth(maxAttempts = 3, intervalMs = 500): Promise<string | null> {
for (let i = 0; i < maxAttempts; i++) {
const { accessToken } = await chrome.storage.local.get(['accessToken'])
if (accessToken) return accessToken
if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, intervalMs))
}
return null
}
// src/background/modules/installation-manager.ts
// (snake-case следва конвенцията на останалите модули: auth-manager.ts, sync-manager.ts)
export function setupInstallation(): void {
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'install') {
const { accessToken } = await chrome.storage.local.get(['accessToken'])
if (accessToken) return // вече логнат → нищо
// Нова инсталация → отвори login ВЕДНЪЖ
chrome.tabs.create({ url: `${config.web-appUrl}/auth/signin?redirect=/extension-auth` })
}
// 'update' → нищо — не redirect при update
})
}
Импортира се в service-worker.ts заедно с другите модули:
import { setupInstallation } from './modules/installation-manager'
// ...
chrome.runtime.onInstalled.addListener(() => {
setupContextMenus() // от context-menu-manager.ts
setupInstallation() // installation redirect логика
syncManager.init()
networkObserver.start()
})
// src/content/prompt-inject.ts
// (Зарежда се на всички платформи — вж. manifest content_scripts)
// Blueprint-ът го слага в src/prompt-inject/ — тук следваме src/content/ конвенцията
// Textarea CSS selectors по платформа
const TEXTAREA_SELECTORS: Record<string, string> = {
'chatgpt.com': '#prompt-textarea',
'claude.ai': '[contenteditable="true"].ProseMirror',
'gemini.google.com': 'rich-textarea .ql-editor',
'chat.deepseek.com': 'textarea#chat-input',
'perplexity.ai': 'textarea[placeholder]',
'grok.com': 'textarea',
'x.com': 'textarea',
'chat.qwen.ai': 'textarea',
'chat.lmsys.org': 'textarea',
}
function getTextarea(): HTMLElement | null {
const host = window.location.hostname
const selector = Object.entries(TEXTAREA_SELECTORS).find(([domain]) =>
host.includes(domain)
)?.[1]
if (!selector) return null
return document.querySelector<HTMLElement>(selector)
}
function injectText(element: HTMLElement, text: string): void {
element.focus()
// Native setter за React/Preact — dispatchEvent не работи без него
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype, 'value'
)?.set
if (nativeInputValueSetter) {
nativeInputValueSetter.call(element, text)
} else {
// Fallback за contenteditable (Claude, Gemini)
;(element as HTMLElement).innerText = text
}
element.dispatchEvent(new Event('input', { bubbles: true }))
element.dispatchEvent(new Event('change', { bubbles: true }))
}
// Слуша команди от service worker
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'INJECT_PROMPT') {
const textarea = getTextarea()
if (textarea && msg.text) injectText(textarea, msg.text)
}
if (msg.type === 'REPLACE_SELECTION') {
const textarea = getTextarea()
if (textarea && msg.text) injectText(textarea, msg.text)
}
})
1. Маркиран текст → десен бутон → Enhance → избор на стил
2. handleContextMenuClick → enhanceSelection(text, style, tabId, token)
3. SW изпраща към /api/ai/enhance (Web App API)
4. Резултатът се записва в chrome.storage.local['bb_enhance_result']
5. chrome.action.openPopup() → popup се отваря
6. Popup показва само текстово поле с резултата
7. Потребителят копира или инжектира директно в textarea
async function enhanceSelection(text: string, style: string, tabId: number, token: string): Promise<void> {
try {
const res = await fetch(`${config.apiBaseUrl}/api/ai/enhance`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ content: text, style }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const { enhanced } = await res.json() as { enhanced: string }
await chrome.storage.local.set({ bb_enhance_result: enhanced })
await chrome.action.openPopup()
} catch (error) {
logger.error('extensionBridge', 'Enhance failed', error)
}
}
interface Chunk {
id: string
text: string
source_url: string
platform: Platform
chat_id?: string
created_at: string
user_id: string
}
// POST /api/chunks — нов API endpoint (трябва да се добави в Phase 5)
// popup/components/QuickAccess.tsx
async function handlePlatformClick(platform: Platform): Promise<void> {
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true })
const isOnPlatform = PLATFORM_URLS[platform].some((url: string) =>
activeTab.url?.includes(url.replace('/*', ''))
)
if (isOnPlatform) {
// Потребителят е на тази платформа → Save Chat
chrome.runtime.sendMessage({ action: 'getConversation', platform, url: activeTab.url })
} else {
// Отвори платформата в нов таб
chrome.tabs.create({ url: PLATFORM_HOME_URLS[platform] })
}
}
┌─────────────────────────────────┐
│ 🧠 BrainBox ⚙️ 🌙 │ Header
├─────────────────────────────────┤
│ ● Connected 🔄 │ StatusBar
├─────────────────────────────────┤
│ Modules: Chats [●] Prompts [●] │ ModulesPanel
├─────────────────────────────────┤
│ Quick Access │
│ [GPT][GEM][CLA][GROK] │ QuickAccess (официални SVG)
│ [PER][LMA][DEP][QWE] │
├─────────────────────────────────┤
│ [🧠 OPEN DASHBOARD] │ CTA
├─────────────────────────────────┤
│ ● System Ready v3.0.0 │ Footer
└─────────────────────────────────┘
Компоненти: Header, StatusBar, ModulesPanel, QuickAccess, Footer
Hooks: useAuth, useStatus
❌ chrome.contextMenus.create() извън onInstalled → duplicate items при SW restart
❌ Директна заявка към API от popup → само чете от store/storage
❌ Бутони в DOM на платформи → само prompt-inject в textarea
❌ waitForAuth() пропуснат → действие без auth token
❌ chrome.tabs.create() при update в onInstalled → само при 'install'
onInstalled?waitForAuth() използван преди всяко context menu действие?prompt-inject.ts ползва native setter за React compatibility?installationManager не отваря tab при update?openPopup()?/api/chunks endpoint планиран в Phase 5?