bloby-bot 0.22.1 → 0.22.5

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.
@@ -4,7 +4,8 @@
4
4
  * Injected into every page. Responsibilities:
5
5
  * - Renders the Bloby bubble (bottom-right corner)
6
6
  * - Opens/closes the chat panel (iframe to extension's panel.html)
7
- * - Extracts page context (URL, title, selection) for the agent
7
+ * - Extracts page context (URL, title, selection, structured data)
8
+ * - Handles ExtensionAction commands from the agent (screenshot, fill form, type)
8
9
  * - Bridges messages between the panel iframe and the background worker
9
10
  */
10
11
 
@@ -13,10 +14,6 @@
13
14
  if (document.getElementById('bloby-ext-bubble')) return;
14
15
  if (location.hostname.endsWith('bloby.bot')) return;
15
16
 
16
- const BUBBLE_SIZE = 60;
17
- const BUBBLE_MARGIN = 24;
18
- const PANEL_WIDTH = 420;
19
-
20
17
  let panelOpen = false;
21
18
  let paired = false;
22
19
 
@@ -30,7 +27,20 @@
30
27
  }
31
28
  });
32
29
 
33
- // Listen for pairing changes
30
+ // Listen for pairing from background worker
31
+ chrome.runtime.onMessage.addListener((msg) => {
32
+ if (msg.type === 'bloby:paired') {
33
+ console.log('[bloby-ext] Paired! Showing bubble.');
34
+ paired = true;
35
+ if (!document.getElementById('bloby-ext-bubble')) createBubble();
36
+ }
37
+ if (msg.type === 'bloby:unpaired') {
38
+ paired = false;
39
+ removeBubble();
40
+ }
41
+ });
42
+
43
+ // Listen for pairing changes via storage
34
44
  chrome.storage.onChanged.addListener((changes) => {
35
45
  if (changes.serverUrl) {
36
46
  if (changes.serverUrl.newValue) {
@@ -52,48 +62,37 @@
52
62
  bubble.addEventListener('click', togglePanel);
53
63
  document.body.appendChild(bubble);
54
64
 
55
- // Inner dot (gradient)
56
65
  const dot = document.createElement('div');
57
66
  dot.className = 'bloby-ext-bubble-dot';
58
67
  bubble.appendChild(dot);
59
68
  }
60
69
 
61
70
  function removeBubble() {
62
- const bubble = document.getElementById('bloby-ext-bubble');
63
- if (bubble) bubble.remove();
64
- const panel = document.getElementById('bloby-ext-panel');
65
- if (panel) panel.remove();
66
- const backdrop = document.getElementById('bloby-ext-backdrop');
67
- if (backdrop) backdrop.remove();
71
+ document.getElementById('bloby-ext-bubble')?.remove();
72
+ document.getElementById('bloby-ext-panel')?.remove();
73
+ document.getElementById('bloby-ext-backdrop')?.remove();
74
+ panelOpen = false;
68
75
  }
69
76
 
70
77
  // ── Panel ──────────────────────────────────────────────────────────────────
71
78
 
72
79
  function togglePanel() {
73
- if (panelOpen) {
74
- closePanel();
75
- } else {
76
- openPanel();
77
- }
80
+ panelOpen ? closePanel() : openPanel();
78
81
  }
79
82
 
80
83
  function openPanel() {
81
84
  if (document.getElementById('bloby-ext-panel')) {
82
- const panel = document.getElementById('bloby-ext-panel');
83
- panel.classList.add('bloby-ext-panel-open');
85
+ document.getElementById('bloby-ext-panel').classList.add('bloby-ext-panel-open');
84
86
  panelOpen = true;
85
87
  showBackdrop();
86
88
  return;
87
89
  }
88
90
 
89
- // Create backdrop
90
91
  showBackdrop();
91
92
 
92
- // Create panel
93
93
  const panel = document.createElement('div');
94
94
  panel.id = 'bloby-ext-panel';
95
95
 
96
- // Create iframe — loads the extension's own panel page
97
96
  const iframe = document.createElement('iframe');
98
97
  iframe.id = 'bloby-ext-iframe';
99
98
  iframe.src = chrome.runtime.getURL('panel/panel.html');
@@ -102,13 +101,11 @@
102
101
 
103
102
  document.body.appendChild(panel);
104
103
 
105
- // Trigger slide-in animation on next frame
106
104
  requestAnimationFrame(() => {
107
105
  panel.classList.add('bloby-ext-panel-open');
108
106
  panelOpen = true;
109
107
  });
110
108
 
111
- // Listen for messages from the panel iframe
112
109
  window.addEventListener('message', handlePanelMessage);
113
110
  }
114
111
 
@@ -134,49 +131,36 @@
134
131
 
135
132
  function hideBackdrop() {
136
133
  const backdrop = document.getElementById('bloby-ext-backdrop');
137
- if (backdrop) {
138
- backdrop.classList.remove('bloby-ext-backdrop-visible');
139
- }
134
+ if (backdrop) backdrop.classList.remove('bloby-ext-backdrop-visible');
140
135
  }
141
136
 
142
- // ── PanelBackground Message Bridge ──────────────────────────────────────
137
+ // ── Message Bridge (panel content script) ────────────────────────────────
143
138
 
144
139
  function handlePanelMessage(event) {
145
- if (event.source !== document.getElementById('bloby-ext-iframe')?.contentWindow) return;
140
+ const iframe = document.getElementById('bloby-ext-iframe');
141
+ if (!iframe || event.source !== iframe.contentWindow) return;
146
142
 
147
143
  const msg = event.data;
148
144
  if (!msg?.type) return;
149
145
 
150
- // Close panel request
151
146
  if (msg.type === 'bloby:close') {
152
147
  closePanel();
153
148
  return;
154
149
  }
155
150
 
156
- // Forward chat messages to background worker → server
157
- if (msg.type === 'bloby:send') {
158
- chrome.runtime.sendMessage({ type: 'bloby:send', payload: msg.payload });
159
- return;
160
- }
161
-
162
151
  // Panel requests page context
163
152
  if (msg.type === 'bloby:get-page-context') {
164
153
  const context = getPageContext();
165
- const iframe = document.getElementById('bloby-ext-iframe');
166
- if (iframe?.contentWindow) {
167
- iframe.contentWindow.postMessage({ type: 'bloby:page-context', context }, '*');
168
- }
154
+ iframe.contentWindow.postMessage({ type: 'bloby:page-context', context }, '*');
169
155
  return;
170
156
  }
171
- }
172
157
 
173
- // Forward server messages from background to panel iframe
174
- chrome.runtime.onMessage.addListener((msg) => {
175
- const iframe = document.getElementById('bloby-ext-iframe');
176
- if (iframe?.contentWindow && panelOpen) {
177
- iframe.contentWindow.postMessage(msg, '*');
158
+ // Agent requests an action on the page
159
+ if (msg.type === 'bloby:extension-action') {
160
+ handleExtensionAction(msg.action, msg.data);
161
+ return;
178
162
  }
179
- });
163
+ }
180
164
 
181
165
  // ── Page Context Extraction ────────────────────────────────────────────────
182
166
 
@@ -187,32 +171,139 @@
187
171
  selection: window.getSelection()?.toString()?.slice(0, 2000) || '',
188
172
  };
189
173
 
190
- // Try to extract structured data (JSON-LD)
191
- const jsonLd = document.querySelector('script[type="application/ld+json"]');
192
- if (jsonLd) {
193
- try {
194
- const data = JSON.parse(jsonLd.textContent);
195
- if (data.name) context.productName = data.name;
196
- if (data.offers?.price) context.price = data.offers.price;
197
- if (data.offers?.priceCurrency) context.currency = data.offers.priceCurrency;
198
- if (data.description) context.description = data.description?.slice(0, 500);
199
- } catch { /* ignore malformed JSON-LD */ }
200
- }
174
+ // Extract structured data (JSON-LD) — product info, articles, etc.
175
+ try {
176
+ const scripts = document.querySelectorAll('script[type="application/ld+json"]');
177
+ for (const script of scripts) {
178
+ const data = JSON.parse(script.textContent);
179
+ // Handle arrays (some sites wrap in array)
180
+ const items = Array.isArray(data) ? data : [data];
181
+ for (const item of items) {
182
+ if (item.name && !context.productName) context.productName = item.name;
183
+ if (item.offers?.price) context.price = item.offers.price;
184
+ if (item.offers?.priceCurrency) context.currency = item.offers.priceCurrency;
185
+ if (item.description && !context.description) context.description = item.description?.slice(0, 500);
186
+ if (item['@type'] === 'VideoObject' && item.name) context.videoTitle = item.name;
187
+ }
188
+ }
189
+ } catch { /* ignore */ }
201
190
 
202
191
  // Meta description fallback
203
- const metaDesc = document.querySelector('meta[name="description"]');
204
- if (metaDesc && !context.description) {
205
- context.description = metaDesc.content?.slice(0, 500);
192
+ if (!context.description) {
193
+ const meta = document.querySelector('meta[name="description"]');
194
+ if (meta) context.description = meta.content?.slice(0, 500);
206
195
  }
207
196
 
197
+ // Detect forms on the page
198
+ const forms = document.querySelectorAll('form');
199
+ if (forms.length > 0) {
200
+ context.hasForms = true;
201
+ context.formCount = forms.length;
202
+ // Extract visible form field labels/names
203
+ const fields = [];
204
+ forms.forEach((form) => {
205
+ form.querySelectorAll('input, select, textarea').forEach((el) => {
206
+ const name = el.getAttribute('name') || el.getAttribute('id') || el.getAttribute('placeholder') || '';
207
+ const type = el.getAttribute('type') || el.tagName.toLowerCase();
208
+ if (name && type !== 'hidden' && type !== 'submit') {
209
+ fields.push({ name, type });
210
+ }
211
+ });
212
+ });
213
+ if (fields.length > 0) context.formFields = fields.slice(0, 20); // cap at 20
214
+ }
215
+
216
+ console.log('[bloby-ext] Page context:', context);
208
217
  return context;
209
218
  }
210
219
 
220
+ // ── Extension Actions (agent → page) ───────────────────────────────────────
221
+
222
+ function handleExtensionAction(action, data) {
223
+ console.log(`[bloby-ext] Action: ${action}`, data);
224
+ const iframe = document.getElementById('bloby-ext-iframe');
225
+
226
+ switch (action) {
227
+ case 'screenshot': {
228
+ // Request screenshot via background worker (needs chrome.tabs API)
229
+ chrome.runtime.sendMessage({ type: 'bloby:screenshot' }, (response) => {
230
+ if (response?.dataUrl && iframe?.contentWindow) {
231
+ iframe.contentWindow.postMessage({
232
+ type: 'bloby:action-result',
233
+ action: 'screenshot',
234
+ result: response.dataUrl,
235
+ }, '*');
236
+ }
237
+ });
238
+ break;
239
+ }
240
+
241
+ case 'read-page': {
242
+ // Extract main text content from the page
243
+ const text = document.body.innerText?.slice(0, 10000) || '';
244
+ if (iframe?.contentWindow) {
245
+ iframe.contentWindow.postMessage({
246
+ type: 'bloby:action-result',
247
+ action: 'read-page',
248
+ result: text,
249
+ }, '*');
250
+ }
251
+ break;
252
+ }
253
+
254
+ case 'fill-form': {
255
+ // Fill form fields from agent-provided data
256
+ if (data?.fields && typeof data.fields === 'object') {
257
+ for (const [selector, value] of Object.entries(data.fields)) {
258
+ const el = document.querySelector(selector) ||
259
+ document.querySelector(`[name="${selector}"]`) ||
260
+ document.querySelector(`[id="${selector}"]`);
261
+ if (el) {
262
+ el.value = value;
263
+ el.dispatchEvent(new Event('input', { bubbles: true }));
264
+ el.dispatchEvent(new Event('change', { bubbles: true }));
265
+ }
266
+ }
267
+ if (iframe?.contentWindow) {
268
+ iframe.contentWindow.postMessage({
269
+ type: 'bloby:action-result',
270
+ action: 'fill-form',
271
+ result: 'done',
272
+ }, '*');
273
+ }
274
+ }
275
+ break;
276
+ }
277
+
278
+ case 'click': {
279
+ // Click an element by selector
280
+ if (data?.selector) {
281
+ const el = document.querySelector(data.selector);
282
+ if (el) el.click();
283
+ }
284
+ break;
285
+ }
286
+
287
+ case 'type': {
288
+ // Type into the currently focused input or a targeted selector
289
+ const target = data?.selector
290
+ ? document.querySelector(data.selector)
291
+ : document.activeElement;
292
+ if (target && data?.text) {
293
+ target.value = data.text;
294
+ target.dispatchEvent(new Event('input', { bubbles: true }));
295
+ }
296
+ break;
297
+ }
298
+
299
+ default:
300
+ console.warn(`[bloby-ext] Unknown action: ${action}`);
301
+ }
302
+ }
303
+
211
304
  // ── Keyboard shortcut ──────────────────────────────────────────────────────
212
305
 
213
306
  document.addEventListener('keydown', (e) => {
214
- if (e.key === 'Escape' && panelOpen) {
215
- closePanel();
216
- }
307
+ if (e.key === 'Escape' && panelOpen) closePanel();
217
308
  });
218
309
  })();
@@ -5,7 +5,8 @@
5
5
  "description": "Your AI agent, on every page. Chat with Bloby from anywhere.",
6
6
  "permissions": [
7
7
  "storage",
8
- "activeTab"
8
+ "activeTab",
9
+ "declarativeNetRequest"
9
10
  ],
10
11
  "host_permissions": [
11
12
  "https://*.bloby.bot/*",
@@ -34,7 +35,7 @@
34
35
  ],
35
36
  "web_accessible_resources": [
36
37
  {
37
- "resources": ["panel/panel.html", "assets/*", "icons/*"],
38
+ "resources": ["panel/panel.html", "panel/panel.js", "assets/*", "icons/*"],
38
39
  "matches": ["<all_urls>"]
39
40
  }
40
41
  ],
@@ -11,7 +11,7 @@
11
11
  .loading {
12
12
  display: flex; align-items: center; justify-content: center;
13
13
  height: 100vh; color: #a1a1aa; font-family: system-ui, sans-serif;
14
- font-size: 14px;
14
+ font-size: 14px; flex-direction: column; gap: 8px;
15
15
  }
16
16
  .loading .dots span {
17
17
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
@@ -23,52 +23,14 @@
23
23
  0%, 80%, 100% { transform: scale(0); }
24
24
  40% { transform: scale(1); }
25
25
  }
26
+ .debug { font-size: 11px; color: #52525b; max-width: 90%; word-break: break-all; }
26
27
  </style>
27
28
  </head>
28
29
  <body>
29
30
  <div class="loading" id="loading">
30
31
  <div class="dots"><span></span><span></span><span></span></div>
32
+ <div class="debug" id="debug"></div>
31
33
  </div>
32
-
33
- <script>
34
- /**
35
- * Panel page — loads the user's Bloby chat in an iframe.
36
- * Reads serverUrl from chrome.storage.local.
37
- */
38
- (async () => {
39
- const data = await chrome.storage.local.get(['serverUrl', 'authToken']);
40
-
41
- if (!data.serverUrl) {
42
- document.getElementById('loading').innerHTML =
43
- '<span style="color:#ef4444">Not paired. Click the Bloby icon in the toolbar to set up.</span>';
44
- return;
45
- }
46
-
47
- // Build the chat URL — same as the dashboard chat iframe
48
- let chatUrl = `${data.serverUrl}/bloby/`;
49
- if (data.authToken) {
50
- chatUrl += `?token=${data.authToken}`;
51
- }
52
-
53
- // Create iframe to the user's Bloby server
54
- const iframe = document.createElement('iframe');
55
- iframe.src = chatUrl;
56
- iframe.allow = 'microphone';
57
- iframe.style.cssText = 'width:100%;height:100%;border:none;';
58
-
59
- iframe.onload = () => {
60
- document.getElementById('loading').style.display = 'none';
61
- };
62
-
63
- document.body.appendChild(iframe);
64
-
65
- // Forward close events from chat iframe to content script (parent of this panel)
66
- window.addEventListener('message', (e) => {
67
- if (e.data?.type === 'bloby:close') {
68
- window.parent.postMessage({ type: 'bloby:close' }, '*');
69
- }
70
- });
71
- })();
72
- </script>
34
+ <script src="panel.js"></script>
73
35
  </body>
74
36
  </html>
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Panel page — loads the Bloby chat in an iframe and bridges page context.
3
+ *
4
+ * Architecture:
5
+ * Content script (host page) ↔ Panel (this page) ↔ Chat iframe (Bloby server)
6
+ *
7
+ * The panel:
8
+ * 1. Loads the Bloby chat in an iframe
9
+ * 2. Requests page context from the content script
10
+ * 3. Injects context into outgoing chat messages
11
+ * 4. Forwards ExtensionAction commands from chat to content script
12
+ */
13
+
14
+ const debug = document.getElementById('debug');
15
+ function log(msg) {
16
+ console.log('[bloby-panel] ' + msg);
17
+ if (debug) debug.textContent = msg;
18
+ }
19
+
20
+ let chatIframe = null;
21
+ let currentPageContext = null;
22
+
23
+ (async () => {
24
+ try {
25
+ log('Reading config...');
26
+ const data = await chrome.storage.local.get(['serverUrl', 'authToken']);
27
+
28
+ if (!data.serverUrl) {
29
+ document.getElementById('loading').innerHTML =
30
+ '<span style="color:#ef4444">Not paired. Click the Bloby icon in the toolbar to set up.</span>';
31
+ return;
32
+ }
33
+
34
+ let chatUrl = data.serverUrl + '/bloby/';
35
+ if (data.authToken) {
36
+ chatUrl += '?token=' + data.authToken;
37
+ }
38
+
39
+ // Add a marker so the chat app knows it's inside the extension
40
+ chatUrl += (chatUrl.includes('?') ? '&' : '?') + 'ext=1';
41
+
42
+ log('Loading: ' + chatUrl);
43
+
44
+ chatIframe = document.createElement('iframe');
45
+ chatIframe.id = 'bloby-chat-iframe';
46
+ chatIframe.src = chatUrl;
47
+ chatIframe.allow = 'microphone';
48
+ chatIframe.style.cssText = 'width:100%;height:100%;border:none;';
49
+
50
+ chatIframe.onload = () => {
51
+ log('Chat loaded!');
52
+ document.getElementById('loading').style.display = 'none';
53
+ // Request initial page context from content script
54
+ requestPageContext();
55
+ };
56
+
57
+ document.body.appendChild(chatIframe);
58
+
59
+ setTimeout(() => {
60
+ if (document.getElementById('loading').style.display !== 'none') {
61
+ log('Timeout loading chat');
62
+ }
63
+ }, 15000);
64
+
65
+ } catch (err) {
66
+ log('Error: ' + err.message);
67
+ }
68
+ })();
69
+
70
+ // ── Page Context ───────────────────────────────────────────────────────────
71
+
72
+ function requestPageContext() {
73
+ // Ask the content script (parent of this panel) for page context
74
+ window.parent.postMessage({ type: 'bloby:get-page-context' }, '*');
75
+ }
76
+
77
+ // ── Message Handling ───────────────────────────────────────────────────────
78
+
79
+ window.addEventListener('message', (event) => {
80
+ const msg = event.data;
81
+ if (!msg?.type) return;
82
+
83
+ // Context response from content script
84
+ if (msg.type === 'bloby:page-context') {
85
+ currentPageContext = msg.context;
86
+ log('Page context received: ' + (msg.context?.url || 'none'));
87
+
88
+ // Forward to chat iframe so it can use it
89
+ if (chatIframe?.contentWindow) {
90
+ chatIframe.contentWindow.postMessage({
91
+ type: 'bloby:page-context',
92
+ context: msg.context,
93
+ }, '*');
94
+ }
95
+ return;
96
+ }
97
+
98
+ // Action result from content script → forward to chat
99
+ if (msg.type === 'bloby:action-result') {
100
+ if (chatIframe?.contentWindow) {
101
+ chatIframe.contentWindow.postMessage(msg, '*');
102
+ }
103
+ return;
104
+ }
105
+
106
+ // Close request from chat iframe → forward to content script
107
+ if (msg.type === 'bloby:close') {
108
+ window.parent.postMessage({ type: 'bloby:close' }, '*');
109
+ return;
110
+ }
111
+
112
+ // Extension action from chat iframe → forward to content script
113
+ if (msg.type === 'bloby:extension-action') {
114
+ window.parent.postMessage(msg, '*');
115
+ return;
116
+ }
117
+
118
+ // Chat requests fresh page context
119
+ if (msg.type === 'bloby:request-context') {
120
+ requestPageContext();
121
+ return;
122
+ }
123
+ });
@@ -1 +0,0 @@
1
- import{i as e}from"./bloby-8izT4vax.js";export{e as Mermaid};