@symbo.ls/connect 3.4.9 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/panel.js CHANGED
@@ -4624,15 +4624,7 @@ Do NOT include any explanation, only valid JSON.`
4624
4624
 
4625
4625
  // ---- Claude API (direct or via platform) ----
4626
4626
  async function callClaudeAPI (prompt, context) {
4627
- const contextStr = 'Element path: ' + (context.elementPath || 'none') +
4628
- '\nElement key: ' + (context.elementKey || 'none') +
4629
- '\nCurrent props: ' + JSON.stringify(context.props) +
4630
- '\nCurrent state: ' + JSON.stringify(context.state) +
4631
- '\nTag: ' + (context.tag || 'unknown') +
4632
- (context.scope === 'section' && context.children
4633
- ? '\nChildren: ' + JSON.stringify(context.children)
4634
- : '') +
4635
- '\n\nUser request: ' + prompt
4627
+ const contextStr = buildContextStr(prompt, context)
4636
4628
 
4637
4629
  // Try 1: own API key
4638
4630
  const apiKey = await getApiKey('anthropic')
@@ -4693,15 +4685,155 @@ Do NOT include any explanation, only valid JSON.`
4693
4685
  throw new Error('No Claude access. Enter your API key or sign in to symbols.app.')
4694
4686
  }
4695
4687
 
4688
+ // ---- OpenAI API ----
4689
+ async function callOpenAI (prompt, context) {
4690
+ const contextStr = buildContextStr(prompt, context)
4691
+ const apiKey = await getApiKey('openai')
4692
+ if (!apiKey) throw new Error('No OpenAI API key. Add it in AI Settings.')
4693
+
4694
+ const resp = await swFetch('https://api.openai.com/v1/chat/completions', {
4695
+ method: 'POST',
4696
+ headers: {
4697
+ 'Content-Type': 'application/json',
4698
+ 'Authorization': 'Bearer ' + apiKey
4699
+ },
4700
+ body: JSON.stringify({
4701
+ model: 'gpt-4o',
4702
+ messages: [
4703
+ { role: 'system', content: AI_SYSTEM_PROMPT },
4704
+ { role: 'user', content: contextStr }
4705
+ ],
4706
+ max_tokens: 1024
4707
+ })
4708
+ })
4709
+
4710
+ if (!resp.ok) {
4711
+ if (resp.status === 401) throw new Error('Invalid OpenAI API key.')
4712
+ throw new Error('OpenAI API error: HTTP ' + resp.status)
4713
+ }
4714
+ if (resp.data && resp.data.choices && resp.data.choices[0]) {
4715
+ return resp.data.choices[0].message.content
4716
+ }
4717
+ throw new Error('Unexpected OpenAI response')
4718
+ }
4719
+
4720
+ // ---- Google Gemini API ----
4721
+ async function callGemini (prompt, context) {
4722
+ const contextStr = buildContextStr(prompt, context)
4723
+ const apiKey = await getApiKey('gemini')
4724
+ if (!apiKey) throw new Error('No Gemini API key. Add it in AI Settings.')
4725
+
4726
+ const resp = await swFetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + apiKey, {
4727
+ method: 'POST',
4728
+ headers: { 'Content-Type': 'application/json' },
4729
+ body: JSON.stringify({
4730
+ systemInstruction: { parts: [{ text: AI_SYSTEM_PROMPT }] },
4731
+ contents: [{ parts: [{ text: contextStr }] }],
4732
+ generationConfig: { maxOutputTokens: 1024 }
4733
+ })
4734
+ })
4735
+
4736
+ if (!resp.ok) {
4737
+ if (resp.status === 400 || resp.status === 403) throw new Error('Invalid Gemini API key.')
4738
+ throw new Error('Gemini API error: HTTP ' + resp.status)
4739
+ }
4740
+ if (resp.data && resp.data.candidates && resp.data.candidates[0]) {
4741
+ return resp.data.candidates[0].content.parts[0].text
4742
+ }
4743
+ throw new Error('Unexpected Gemini response')
4744
+ }
4745
+
4746
+ // ---- DeepSeek API (OpenAI-compatible) ----
4747
+ async function callDeepSeek (prompt, context) {
4748
+ const contextStr = buildContextStr(prompt, context)
4749
+ const apiKey = await getApiKey('deepseek')
4750
+ if (!apiKey) throw new Error('No DeepSeek API key. Add it in AI Settings.')
4751
+
4752
+ const resp = await swFetch('https://api.deepseek.com/chat/completions', {
4753
+ method: 'POST',
4754
+ headers: {
4755
+ 'Content-Type': 'application/json',
4756
+ 'Authorization': 'Bearer ' + apiKey
4757
+ },
4758
+ body: JSON.stringify({
4759
+ model: 'deepseek-chat',
4760
+ messages: [
4761
+ { role: 'system', content: AI_SYSTEM_PROMPT },
4762
+ { role: 'user', content: contextStr }
4763
+ ],
4764
+ max_tokens: 1024
4765
+ })
4766
+ })
4767
+
4768
+ if (!resp.ok) {
4769
+ if (resp.status === 401) throw new Error('Invalid DeepSeek API key.')
4770
+ throw new Error('DeepSeek API error: HTTP ' + resp.status)
4771
+ }
4772
+ if (resp.data && resp.data.choices && resp.data.choices[0]) {
4773
+ return resp.data.choices[0].message.content
4774
+ }
4775
+ throw new Error('Unexpected DeepSeek response')
4776
+ }
4777
+
4778
+ // ---- Groq API (OpenAI-compatible) ----
4779
+ async function callGroq (prompt, context) {
4780
+ const contextStr = buildContextStr(prompt, context)
4781
+ const apiKey = await getApiKey('groq')
4782
+ if (!apiKey) throw new Error('No Groq API key. Add it in AI Settings.')
4783
+
4784
+ const resp = await swFetch('https://api.groq.com/openai/v1/chat/completions', {
4785
+ method: 'POST',
4786
+ headers: {
4787
+ 'Content-Type': 'application/json',
4788
+ 'Authorization': 'Bearer ' + apiKey
4789
+ },
4790
+ body: JSON.stringify({
4791
+ model: 'llama-3.3-70b-versatile',
4792
+ messages: [
4793
+ { role: 'system', content: AI_SYSTEM_PROMPT },
4794
+ { role: 'user', content: contextStr }
4795
+ ],
4796
+ max_tokens: 1024
4797
+ })
4798
+ })
4799
+
4800
+ if (!resp.ok) {
4801
+ if (resp.status === 401) throw new Error('Invalid Groq API key.')
4802
+ throw new Error('Groq API error: HTTP ' + resp.status)
4803
+ }
4804
+ if (resp.data && resp.data.choices && resp.data.choices[0]) {
4805
+ return resp.data.choices[0].message.content
4806
+ }
4807
+ throw new Error('Unexpected Groq response')
4808
+ }
4809
+
4810
+ // ---- Shared context builder ----
4811
+ function buildContextStr (prompt, context) {
4812
+ return 'Element path: ' + (context.elementPath || 'none') +
4813
+ '\nElement key: ' + (context.elementKey || 'none') +
4814
+ '\nCurrent props: ' + JSON.stringify(context.props) +
4815
+ '\nCurrent state: ' + JSON.stringify(context.state) +
4816
+ '\nTag: ' + (context.tag || 'unknown') +
4817
+ (context.scope === 'section' && context.children
4818
+ ? '\nChildren: ' + JSON.stringify(context.children)
4819
+ : '') +
4820
+ '\n\nUser request: ' + prompt
4821
+ }
4822
+
4696
4823
  // ---- AI settings dialog ----
4824
+ const AI_PROVIDERS = ['anthropic', 'openai', 'gemini', 'deepseek', 'groq']
4825
+
4697
4826
  function openAISettings () {
4698
4827
  const dialog = document.getElementById('ai-settings-dialog')
4699
4828
  dialog.style.display = ''
4700
4829
 
4701
- // Load saved key
4702
- getApiKey('anthropic').then(key => {
4703
- document.getElementById('ai-key-anthropic').value = key || ''
4704
- })
4830
+ // Load saved keys for all providers
4831
+ for (const provider of AI_PROVIDERS) {
4832
+ getApiKey(provider).then(key => {
4833
+ const el = document.getElementById('ai-key-' + provider)
4834
+ if (el) el.value = key || ''
4835
+ })
4836
+ }
4705
4837
 
4706
4838
  // Show auth status
4707
4839
  const statusEl = document.getElementById('ai-dialog-auth-status')
@@ -4712,12 +4844,12 @@ Do NOT include any explanation, only valid JSON.`
4712
4844
  statusEl.textContent = 'Signed in to symbols.app — Claude available via platform'
4713
4845
  } else {
4714
4846
  statusEl.className = 'ai-dialog-auth-status signed-out'
4715
- statusEl.textContent = 'Not signed in — sign in to symbols.app for free Claude access, or use your own API key below'
4847
+ statusEl.textContent = 'Not signed in to symbols.app'
4716
4848
  }
4717
4849
  })
4718
4850
  } else {
4719
4851
  statusEl.className = 'ai-dialog-auth-status signed-out'
4720
- statusEl.textContent = 'Sign in to symbols.app or enter your Anthropic API key below'
4852
+ statusEl.textContent = ''
4721
4853
  }
4722
4854
  }
4723
4855
 
@@ -4725,11 +4857,15 @@ Do NOT include any explanation, only valid JSON.`
4725
4857
  document.getElementById('ai-settings-dialog').style.display = 'none'
4726
4858
  }
4727
4859
 
4728
- function saveAISettings () {
4729
- const key = document.getElementById('ai-key-anthropic').value.trim()
4730
- setApiKey('anthropic', key || null).then(() => {
4731
- closeAISettings()
4732
- })
4860
+ async function saveAISettings () {
4861
+ for (const provider of AI_PROVIDERS) {
4862
+ const el = document.getElementById('ai-key-' + provider)
4863
+ if (el) {
4864
+ const key = el.value.trim()
4865
+ await setApiKey(provider, key || null)
4866
+ }
4867
+ }
4868
+ closeAISettings()
4733
4869
  }
4734
4870
 
4735
4871
  function getActiveScope (containerId) {
@@ -4854,6 +4990,14 @@ Do NOT include any explanation, only valid JSON.`
4854
4990
  response = starterAI(prompt, context)
4855
4991
  } else if (model === 'claude') {
4856
4992
  response = await callClaudeAPI(prompt, context)
4993
+ } else if (model === 'openai') {
4994
+ response = await callOpenAI(prompt, context)
4995
+ } else if (model === 'gemini') {
4996
+ response = await callGemini(prompt, context)
4997
+ } else if (model === 'deepseek') {
4998
+ response = await callDeepSeek(prompt, context)
4999
+ } else if (model === 'groq') {
5000
+ response = await callGroq(prompt, context)
4857
5001
  } else if (model === 'chrome') {
4858
5002
  response = await runChromeAI(prompt, context)
4859
5003
  }
@@ -5322,6 +5466,8 @@ Do NOT include any explanation, only valid JSON.`
5322
5466
  if (mode === 'chat') updateChatContextLabel()
5323
5467
  if (mode === 'gallery') renderGallery()
5324
5468
  if (mode === 'content') renderContent()
5469
+ if (mode === 'network') renderNetwork()
5470
+ if (mode === 'integrations') renderIntegrations()
5325
5471
  }
5326
5472
 
5327
5473
  // ============================================================
@@ -5436,6 +5582,14 @@ Do NOT include any explanation, only valid JSON.`
5436
5582
  response = starterAI(prompt, context)
5437
5583
  } else if (model === 'claude') {
5438
5584
  response = await callClaudeAPI(prompt, context)
5585
+ } else if (model === 'openai') {
5586
+ response = await callOpenAI(prompt, context)
5587
+ } else if (model === 'gemini') {
5588
+ response = await callGemini(prompt, context)
5589
+ } else if (model === 'deepseek') {
5590
+ response = await callDeepSeek(prompt, context)
5591
+ } else if (model === 'groq') {
5592
+ response = await callGroq(prompt, context)
5439
5593
  } else if (model === 'chrome') {
5440
5594
  response = await runChromeAI(prompt, context)
5441
5595
  }
@@ -5579,6 +5733,20 @@ Do NOT include any explanation, only valid JSON.`
5579
5733
  }
5580
5734
  document.getElementById('btn-app-disconnect').addEventListener('click', disconnect)
5581
5735
 
5736
+ // Network controls
5737
+ const networkTestBtn = document.getElementById('network-test')
5738
+ if (networkTestBtn) networkTestBtn.addEventListener('click', testNetworkConnection)
5739
+ const networkSaveBtn = document.getElementById('network-save')
5740
+ if (networkSaveBtn) networkSaveBtn.addEventListener('click', saveNetworkConfig)
5741
+ const networkDisconnectBtn = document.getElementById('network-disconnect')
5742
+ if (networkDisconnectBtn) networkDisconnectBtn.addEventListener('click', disconnectNetwork)
5743
+
5744
+ // Integrations search
5745
+ const intgSearch = document.getElementById('integrations-search')
5746
+ if (intgSearch) {
5747
+ intgSearch.addEventListener('input', () => renderMarketplace(intgSearch.value))
5748
+ }
5749
+
5582
5750
  // Tree pane tabs (Active Nodes / State Tree / Design System)
5583
5751
  document.querySelectorAll('.tree-pane-tab').forEach(tab => {
5584
5752
  tab.addEventListener('click', () => {
@@ -6230,6 +6398,303 @@ Do NOT include any explanation, only valid JSON.`
6230
6398
  }
6231
6399
  }
6232
6400
 
6401
+ // ============================================================
6402
+ // Network mode — backend connection & server info
6403
+ // ============================================================
6404
+ let networkConfig = { url: '', token: '' }
6405
+
6406
+ async function renderNetwork () {
6407
+ // Load saved config
6408
+ const data = await chrome.storage.local.get('network_config')
6409
+ if (data.network_config) networkConfig = data.network_config
6410
+
6411
+ const urlInput = document.getElementById('network-url')
6412
+ const tokenInput = document.getElementById('network-token')
6413
+ if (urlInput) urlInput.value = networkConfig.url || ''
6414
+ if (tokenInput) tokenInput.value = networkConfig.token || ''
6415
+
6416
+ // Try to load config from symbols.json or runtime
6417
+ loadNetworkInfo()
6418
+ }
6419
+
6420
+ async function loadNetworkInfo () {
6421
+ // Try reading from fileCache (symbols.json)
6422
+ let config = null
6423
+ if (typeof fileCache !== 'undefined') {
6424
+ for (const [path, content] of Object.entries(fileCache)) {
6425
+ if (path.endsWith('symbols.json') || path.endsWith('symbols.config.js')) {
6426
+ try {
6427
+ const parsed = JSON.parse(content)
6428
+ if (parsed.server || parsed.api || parsed.backend || parsed.network) {
6429
+ config = parsed.server || parsed.api || parsed.backend || parsed.network
6430
+ break
6431
+ }
6432
+ } catch (e) {}
6433
+ }
6434
+ }
6435
+ }
6436
+
6437
+ // Try reading from runtime context
6438
+ if (!config) {
6439
+ try {
6440
+ const raw = await pageEval(`(function(){
6441
+ var I = window.__DOMQL_INSPECTOR__;
6442
+ if (!I) return 'null';
6443
+ var el = I.findRoot();
6444
+ if (!el) return 'null';
6445
+ var ctx = el.context || {};
6446
+ var cfg = ctx.server || ctx.api || ctx.backend || ctx.network || {};
6447
+ if (!cfg || typeof cfg !== 'object') return 'null';
6448
+ try { return JSON.stringify(cfg); } catch(e) { return 'null'; }
6449
+ })()`)
6450
+ if (raw && raw !== 'null') config = JSON.parse(raw)
6451
+ } catch (e) {}
6452
+ }
6453
+
6454
+ // Populate server info from config
6455
+ if (config) {
6456
+ setNetworkInfoField('host', config.host || config.url || config.hostname || '—')
6457
+ setNetworkInfoField('ip', config.ip || config.address || '—')
6458
+ setNetworkInfoField('platform', config.platform || config.provider || config.hosting || '—')
6459
+ setNetworkInfoField('region', config.region || config.location || '—')
6460
+
6461
+ // Auto-fill URL if empty
6462
+ const urlInput = document.getElementById('network-url')
6463
+ if (urlInput && !urlInput.value && (config.url || config.host)) {
6464
+ urlInput.value = config.url || config.host
6465
+ }
6466
+ }
6467
+
6468
+ // Try to get deployment info
6469
+ const deployEl = document.getElementById('network-deployment')
6470
+ if (config && (config.deploy || config.deployment || config.ci)) {
6471
+ const deploy = config.deploy || config.deployment || config.ci
6472
+ deployEl.innerHTML = ''
6473
+ for (const [k, v] of Object.entries(deploy)) {
6474
+ const row = document.createElement('div')
6475
+ row.className = 'network-info-row'
6476
+ row.innerHTML = '<span class="network-info-label">' + escapeHtml(k) + '</span><span class="network-info-value">' + escapeHtml(String(v)) + '</span>'
6477
+ deployEl.appendChild(row)
6478
+ }
6479
+ }
6480
+ }
6481
+
6482
+ function setNetworkInfoField (field, value) {
6483
+ const el = document.getElementById('network-info-' + field)
6484
+ if (el) el.textContent = value || '—'
6485
+ }
6486
+
6487
+ async function testNetworkConnection () {
6488
+ const url = document.getElementById('network-url').value.trim()
6489
+ if (!url) return
6490
+ const statusEl = document.getElementById('network-status')
6491
+ const statusText = document.getElementById('network-status-text')
6492
+ statusText.textContent = 'Testing...'
6493
+ statusEl.className = 'network-status'
6494
+
6495
+ const start = Date.now()
6496
+ try {
6497
+ const resp = await swFetch(url + (url.includes('?') ? '&' : '?') + '_t=' + Date.now(), {
6498
+ method: 'GET',
6499
+ headers: networkConfig.token ? { 'Authorization': 'Bearer ' + networkConfig.token } : {}
6500
+ })
6501
+ const latency = Date.now() - start
6502
+ setNetworkInfoField('latency', latency + 'ms')
6503
+
6504
+ if (resp.ok) {
6505
+ statusEl.className = 'network-status connected'
6506
+ statusText.textContent = 'Connected (' + latency + 'ms)'
6507
+ setNetworkInfoField('status', 'OK (' + (resp.status || 200) + ')')
6508
+
6509
+ // Try to extract host from URL
6510
+ try {
6511
+ const u = new URL(url)
6512
+ setNetworkInfoField('host', u.hostname)
6513
+ } catch (e) {}
6514
+ } else {
6515
+ statusEl.className = 'network-status error'
6516
+ statusText.textContent = 'Error: HTTP ' + (resp.status || 'unknown')
6517
+ setNetworkInfoField('status', 'Error ' + (resp.status || ''))
6518
+ }
6519
+ } catch (e) {
6520
+ statusEl.className = 'network-status error'
6521
+ statusText.textContent = 'Failed: ' + (e.message || 'connection error')
6522
+ setNetworkInfoField('status', 'Unreachable')
6523
+ setNetworkInfoField('latency', '—')
6524
+ }
6525
+ }
6526
+
6527
+ async function saveNetworkConfig () {
6528
+ networkConfig.url = document.getElementById('network-url').value.trim()
6529
+ networkConfig.token = document.getElementById('network-token').value.trim()
6530
+ await chrome.storage.local.set({ network_config: networkConfig })
6531
+ setStatus('Network config saved')
6532
+ testNetworkConnection()
6533
+ }
6534
+
6535
+ async function disconnectNetwork () {
6536
+ networkConfig = { url: '', token: '' }
6537
+ await chrome.storage.local.set({ network_config: networkConfig })
6538
+ document.getElementById('network-url').value = ''
6539
+ document.getElementById('network-token').value = ''
6540
+ document.getElementById('network-status').className = 'network-status'
6541
+ document.getElementById('network-status-text').textContent = 'Not connected'
6542
+ ;['host', 'ip', 'status', 'latency', 'platform', 'region'].forEach(f => setNetworkInfoField(f, '—'))
6543
+ document.getElementById('network-deployment').innerHTML = '<div class="empty-message">Connect to view deployment configuration</div>'
6544
+ setStatus('Disconnected')
6545
+ }
6546
+
6547
+ // ============================================================
6548
+ // Integrations mode — marketplace & active integrations
6549
+ // ============================================================
6550
+ const MARKETPLACE_ITEMS = [
6551
+ { id: 'google-analytics', name: 'Google Analytics', icon: '\uD83D\uDCCA', desc: 'Track page views, events, and user behavior', category: 'Analytics' },
6552
+ { id: 'mixpanel', name: 'Mixpanel', icon: '\uD83D\uDD0D', desc: 'Product analytics and user tracking', category: 'Analytics' },
6553
+ { id: 'hotjar', name: 'Hotjar', icon: '\uD83D\uDD25', desc: 'Heatmaps, recordings, and user feedback', category: 'Analytics' },
6554
+ { id: 'sentry', name: 'Sentry', icon: '\uD83D\uDEE1', desc: 'Error tracking and performance monitoring', category: 'Monitoring' },
6555
+ { id: 'intercom', name: 'Intercom', icon: '\uD83D\uDCAC', desc: 'Customer messaging and live chat', category: 'Chatbot' },
6556
+ { id: 'crisp', name: 'Crisp', icon: '\uD83D\uDDE8', desc: 'Live chat and helpdesk widget', category: 'Chatbot' },
6557
+ { id: 'stripe', name: 'Stripe', icon: '\uD83D\uDCB3', desc: 'Payment processing and subscriptions', category: 'Payments' },
6558
+ { id: 'auth0', name: 'Auth0', icon: '\uD83D\uDD10', desc: 'Authentication and authorization', category: 'Auth' },
6559
+ { id: 'firebase', name: 'Firebase', icon: '\uD83D\uDD25', desc: 'Backend services, auth, and real-time database', category: 'Backend' },
6560
+ { id: 'supabase', name: 'Supabase', icon: '\u26A1', desc: 'Open-source Firebase alternative with Postgres', category: 'Backend' },
6561
+ { id: 'cloudflare', name: 'Cloudflare', icon: '\u2601', desc: 'CDN, DNS, and edge computing', category: 'Infrastructure' },
6562
+ { id: 'vercel', name: 'Vercel', icon: '\u25B2', desc: 'Deployment and serverless functions', category: 'Infrastructure' },
6563
+ { id: 'mailchimp', name: 'Mailchimp', icon: '\uD83D\uDCE7', desc: 'Email marketing and automation', category: 'Marketing' },
6564
+ { id: 'segment', name: 'Segment', icon: '\uD83D\uDD00', desc: 'Customer data platform and event routing', category: 'Analytics' },
6565
+ { id: 'posthog', name: 'PostHog', icon: '\uD83E\uDDA4', desc: 'Open-source product analytics and feature flags', category: 'Analytics' },
6566
+ { id: 'recaptcha', name: 'reCAPTCHA', icon: '\uD83E\uDD16', desc: 'Bot protection and spam prevention', category: 'Security' },
6567
+ ]
6568
+
6569
+ let enabledIntegrations = {}
6570
+
6571
+ async function renderIntegrations () {
6572
+ const data = await chrome.storage.local.get('integrations')
6573
+ enabledIntegrations = (data.integrations) || {}
6574
+
6575
+ renderActiveIntegrations()
6576
+ renderMarketplace()
6577
+ }
6578
+
6579
+ function renderActiveIntegrations () {
6580
+ const container = document.getElementById('integrations-active')
6581
+ container.innerHTML = ''
6582
+ const activeIds = Object.keys(enabledIntegrations)
6583
+
6584
+ if (activeIds.length === 0) {
6585
+ container.innerHTML = '<div class="empty-message">No integrations enabled</div>'
6586
+ return
6587
+ }
6588
+
6589
+ for (const id of activeIds) {
6590
+ const item = MARKETPLACE_ITEMS.find(m => m.id === id) || { name: id, icon: '\uD83D\uDD0C', desc: '' }
6591
+ const config = enabledIntegrations[id] || {}
6592
+
6593
+ const el = document.createElement('div')
6594
+ el.className = 'integration-item'
6595
+
6596
+ el.innerHTML =
6597
+ '<div class="integration-icon">' + item.icon + '</div>' +
6598
+ '<div class="integration-info">' +
6599
+ '<div class="integration-name">' + escapeHtml(item.name) + '</div>' +
6600
+ '<div class="integration-desc">' + escapeHtml(config.key ? 'Configured' : 'Not configured') + '</div>' +
6601
+ '</div>'
6602
+
6603
+ const actions = document.createElement('div')
6604
+ actions.className = 'integration-actions'
6605
+
6606
+ const configBtn = document.createElement('button')
6607
+ configBtn.className = 'integration-btn'
6608
+ configBtn.textContent = 'Configure'
6609
+ configBtn.addEventListener('click', () => openIntegrationConfig(id, item))
6610
+ actions.appendChild(configBtn)
6611
+
6612
+ const removeBtn = document.createElement('button')
6613
+ removeBtn.className = 'integration-btn integration-btn-danger'
6614
+ removeBtn.textContent = 'Remove'
6615
+ removeBtn.addEventListener('click', async () => {
6616
+ delete enabledIntegrations[id]
6617
+ await chrome.storage.local.set({ integrations: enabledIntegrations })
6618
+ renderActiveIntegrations()
6619
+ renderMarketplace()
6620
+ })
6621
+ actions.appendChild(removeBtn)
6622
+
6623
+ el.appendChild(actions)
6624
+ container.appendChild(el)
6625
+ }
6626
+ }
6627
+
6628
+ function renderMarketplace (filter) {
6629
+ const container = document.getElementById('integrations-marketplace')
6630
+ container.innerHTML = ''
6631
+ const query = (filter || '').toLowerCase()
6632
+
6633
+ for (const item of MARKETPLACE_ITEMS) {
6634
+ if (enabledIntegrations[item.id]) continue
6635
+ if (query && !item.name.toLowerCase().includes(query) && !item.category.toLowerCase().includes(query) && !item.desc.toLowerCase().includes(query)) continue
6636
+
6637
+ const card = document.createElement('div')
6638
+ card.className = 'integration-card'
6639
+ card.innerHTML =
6640
+ '<div class="integration-card-header">' +
6641
+ '<div class="integration-card-icon">' + item.icon + '</div>' +
6642
+ '<div class="integration-card-name">' + escapeHtml(item.name) + '</div>' +
6643
+ '</div>' +
6644
+ '<div class="integration-card-desc">' + escapeHtml(item.desc) + '</div>' +
6645
+ '<span class="integration-card-tag">' + escapeHtml(item.category) + '</span>'
6646
+
6647
+ card.addEventListener('click', () => openIntegrationConfig(item.id, item))
6648
+ container.appendChild(card)
6649
+ }
6650
+
6651
+ if (container.children.length === 0) {
6652
+ container.innerHTML = '<div class="empty-message">' + (query ? 'No matching integrations' : 'All integrations enabled') + '</div>'
6653
+ }
6654
+ }
6655
+
6656
+ function openIntegrationConfig (id, item) {
6657
+ const config = enabledIntegrations[id] || {}
6658
+ const isNew = !enabledIntegrations[id]
6659
+
6660
+ // Simple config dialog inline
6661
+ const overlay = document.createElement('div')
6662
+ overlay.className = 'ai-dialog-overlay'
6663
+ const dialog = document.createElement('div')
6664
+ dialog.className = 'ai-dialog'
6665
+ dialog.innerHTML =
6666
+ '<div class="ai-dialog-header"><span>' + escapeHtml(item.name) + '</span><button class="dialog-close">&times;</button></div>' +
6667
+ '<div class="ai-dialog-body">' +
6668
+ '<div class="ai-dialog-section">' +
6669
+ '<div class="ai-dialog-field"><label>API Key / ID</label><input id="intg-key" type="text" placeholder="Enter API key or tracking ID" class="ai-dialog-input" value="' + escapeHtml(config.key || '') + '" /></div>' +
6670
+ '<div class="ai-dialog-field"><label>Config (JSON, optional)</label><textarea id="intg-config" class="method-args-textarea" rows="4" spellcheck="false" placeholder=\'{"option": "value"}\'>' + escapeHtml(config.extra ? JSON.stringify(config.extra, null, 2) : '') + '</textarea></div>' +
6671
+ '</div>' +
6672
+ '<div class="ai-dialog-actions">' +
6673
+ '<button id="intg-save" class="ai-dialog-save">' + (isNew ? 'Enable' : 'Save') + '</button>' +
6674
+ '</div>' +
6675
+ '</div>'
6676
+
6677
+ overlay.appendChild(dialog)
6678
+ document.body.appendChild(overlay)
6679
+
6680
+ dialog.querySelector('.dialog-close').addEventListener('click', () => overlay.remove())
6681
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove() })
6682
+
6683
+ dialog.querySelector('#intg-save').addEventListener('click', async () => {
6684
+ const key = dialog.querySelector('#intg-key').value.trim()
6685
+ let extra = null
6686
+ const extraRaw = dialog.querySelector('#intg-config').value.trim()
6687
+ if (extraRaw) {
6688
+ try { extra = JSON.parse(extraRaw) } catch (e) { extra = null }
6689
+ }
6690
+ enabledIntegrations[id] = { key, extra, enabledAt: Date.now() }
6691
+ await chrome.storage.local.set({ integrations: enabledIntegrations })
6692
+ overlay.remove()
6693
+ renderActiveIntegrations()
6694
+ renderMarketplace()
6695
+ })
6696
+ }
6697
+
6233
6698
  // Auto-refresh: listen for page navigation and poll for DOM changes
6234
6699
  function initAutoRefresh () {
6235
6700
  // Refresh tree when user navigates to a new page
@@ -73,7 +73,7 @@ chrome.runtime.onInstalled.addListener(({ previousVersion, reason }) => {
73
73
  if (["install", "update"].includes(reason)) {
74
74
  initSettings();
75
75
  chrome.declarativeNetRequest.updateDynamicRules({
76
- removeRuleIds: [1, 2, 3],
76
+ removeRuleIds: [1, 2, 3, 4, 5, 6, 7],
77
77
  addRules: [
78
78
  {
79
79
  id: 1,
@@ -102,6 +102,62 @@ chrome.runtime.onInstalled.addListener(({ previousVersion, reason }) => {
102
102
  urlFilter: "https://api.anthropic.com/*",
103
103
  resourceTypes: ["xmlhttprequest", "other"]
104
104
  }
105
+ },
106
+ {
107
+ id: 3,
108
+ priority: 1,
109
+ action: {
110
+ type: "modifyHeaders",
111
+ requestHeaders: [
112
+ { header: "Origin", operation: "remove" }
113
+ ]
114
+ },
115
+ condition: {
116
+ urlFilter: "https://api.openai.com/*",
117
+ resourceTypes: ["xmlhttprequest", "other"]
118
+ }
119
+ },
120
+ {
121
+ id: 4,
122
+ priority: 1,
123
+ action: {
124
+ type: "modifyHeaders",
125
+ requestHeaders: [
126
+ { header: "Origin", operation: "remove" }
127
+ ]
128
+ },
129
+ condition: {
130
+ urlFilter: "https://generativelanguage.googleapis.com/*",
131
+ resourceTypes: ["xmlhttprequest", "other"]
132
+ }
133
+ },
134
+ {
135
+ id: 5,
136
+ priority: 1,
137
+ action: {
138
+ type: "modifyHeaders",
139
+ requestHeaders: [
140
+ { header: "Origin", operation: "remove" }
141
+ ]
142
+ },
143
+ condition: {
144
+ urlFilter: "https://api.deepseek.com/*",
145
+ resourceTypes: ["xmlhttprequest", "other"]
146
+ }
147
+ },
148
+ {
149
+ id: 6,
150
+ priority: 1,
151
+ action: {
152
+ type: "modifyHeaders",
153
+ requestHeaders: [
154
+ { header: "Origin", operation: "remove" }
155
+ ]
156
+ },
157
+ condition: {
158
+ urlFilter: "https://api.groq.com/*",
159
+ resourceTypes: ["xmlhttprequest", "other"]
160
+ }
105
161
  }
106
162
  ]
107
163
  });