bloby-bot 0.22.5 → 0.22.7
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-bloby/assets/{bloby-DvSkie1b.js → bloby-DRKZrgq5.js} +76 -76
- package/dist-bloby/assets/globals-D723zY1K.css +2 -0
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-BbEkUgTM.js → highlighted-body-OFNGDK62-2gSfFQnP.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-vCq54mVZ.js +1 -0
- package/dist-bloby/assets/{onboard-8MWxCQSm.js → onboard-UT8t4BUn.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +1 -1
- package/supervisor/chat/src/components/Chat/BlobyImageCard.tsx +63 -0
- package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +33 -8
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +48 -30
- package/dist-bloby/assets/globals-b7xkhPEo.css +0 -2
- package/dist-bloby/assets/mermaid-GHXKKRXX-C0VBooMz.js +0 -1
- package/workspace/skills/chrome-extension/.claude-plugin/plugin.json +0 -6
- package/workspace/skills/chrome-extension/SKILL.md +0 -181
- package/workspace/skills/chrome-extension/background.js +0 -182
- package/workspace/skills/chrome-extension/content-script.js +0 -309
- package/workspace/skills/chrome-extension/content-style.css +0 -86
- package/workspace/skills/chrome-extension/icons/icon-128.png +0 -0
- package/workspace/skills/chrome-extension/icons/icon-16.png +0 -0
- package/workspace/skills/chrome-extension/icons/icon-48.png +0 -0
- package/workspace/skills/chrome-extension/manifest.json +0 -47
- package/workspace/skills/chrome-extension/panel/panel.html +0 -36
- package/workspace/skills/chrome-extension/panel/panel.js +0 -123
- package/workspace/skills/chrome-extension/popup/popup.css +0 -124
- package/workspace/skills/chrome-extension/popup/popup.html +0 -47
- package/workspace/skills/chrome-extension/popup/popup.js +0 -115
- package/workspace/skills/chrome-extension/skill.json +0 -15
- /package/dist-bloby/assets/{globals-VdwDxdso.js → globals-BFNdjQrL.js} +0 -0
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
# Chrome Extension
|
|
2
|
-
|
|
3
|
-
## What This Is
|
|
4
|
-
|
|
5
|
-
A Chrome extension that puts you (the bloby) on every webpage your human visits. A floating bubble appears in the bottom-right corner of every tab. Your human clicks it, and a chat panel slides out — same chat, same conversation, fully mirrored with the dashboard and WhatsApp.
|
|
6
|
-
|
|
7
|
-
The extension also gives your human the ability to share page context with you — URL, title, selected text, and structured data (product info, prices). This lets you help with what they're actively looking at.
|
|
8
|
-
|
|
9
|
-
## Dependencies
|
|
10
|
-
|
|
11
|
-
None.
|
|
12
|
-
|
|
13
|
-
## Setup
|
|
14
|
-
|
|
15
|
-
### Step 1: Generate a pairing code
|
|
16
|
-
|
|
17
|
-
When your human asks to set up the Chrome extension, generate a pairing code:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
curl -s -X POST https://api.bloby.bot/api/extension/pair/create \
|
|
21
|
-
-H "Content-Type: application/json" \
|
|
22
|
-
-H "Authorization: Bearer $(cat ~/.bloby/config.json | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4)"
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
This returns a 6-digit code that expires in 5 minutes.
|
|
26
|
-
|
|
27
|
-
### Step 2: Walk your human through installation
|
|
28
|
-
|
|
29
|
-
Tell your human:
|
|
30
|
-
|
|
31
|
-
1. Open `chrome://extensions` in Chrome
|
|
32
|
-
2. Turn on **Developer Mode** (toggle in the top-right corner)
|
|
33
|
-
3. Click **Load unpacked**
|
|
34
|
-
4. Select the folder: `~/.bloby/workspace/skills/chrome-extension/`
|
|
35
|
-
5. Click the puzzle piece icon in Chrome's toolbar and pin Bloby
|
|
36
|
-
6. Click the Bloby icon and enter the 6-digit pairing code
|
|
37
|
-
|
|
38
|
-
### Step 3: Verify it works
|
|
39
|
-
|
|
40
|
-
After your human enters the code, the extension connects to your server. A gradient bubble should appear on every webpage. Ask your human to confirm they see it.
|
|
41
|
-
|
|
42
|
-
If the code expired, generate a new one (repeat Step 1).
|
|
43
|
-
|
|
44
|
-
### Re-pairing
|
|
45
|
-
|
|
46
|
-
If your human needs to reconnect (new device, token expired):
|
|
47
|
-
1. Generate a new pairing code (Step 1)
|
|
48
|
-
2. They click the Bloby icon in Chrome toolbar → enter the new code
|
|
49
|
-
|
|
50
|
-
## Usage
|
|
51
|
-
|
|
52
|
-
### How messages from the extension arrive
|
|
53
|
-
|
|
54
|
-
Messages from the extension come through the same WebSocket as dashboard chat messages. There is no difference — the extension is just another connected client. Your responses appear in both the extension and the dashboard simultaneously.
|
|
55
|
-
|
|
56
|
-
### Page context
|
|
57
|
-
|
|
58
|
-
When your human sends a message from the extension, the message may include page context. This appears as metadata at the start of the message:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
[Page: https://amazon.com/dp/B0XXXXX | iPhone 16 Pro - 256GB | $999.00]
|
|
62
|
-
Can you find this cheaper?
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The context includes:
|
|
66
|
-
- **URL** — the page they're on
|
|
67
|
-
- **Title** — the page title
|
|
68
|
-
- **Selection** — any text they've highlighted (up to 2000 chars)
|
|
69
|
-
- **Product info** — name, price, currency (extracted from JSON-LD structured data if available)
|
|
70
|
-
- **Meta description** — page summary
|
|
71
|
-
|
|
72
|
-
You can use this context directly, or fetch the URL yourself with curl/fetch tools for more detail.
|
|
73
|
-
|
|
74
|
-
### Common use cases
|
|
75
|
-
|
|
76
|
-
**Price comparison:**
|
|
77
|
-
Human is on a product page → asks "find this cheaper" → you have the URL, product name, and price → search the web for alternatives.
|
|
78
|
-
|
|
79
|
-
**Page summarization:**
|
|
80
|
-
Human is on a long article → asks "summarize this" → you fetch the URL and produce a summary.
|
|
81
|
-
|
|
82
|
-
**Video transcription:**
|
|
83
|
-
Human is on YouTube → asks "transcribe this" → you extract the video URL, use available tools to get the transcript.
|
|
84
|
-
|
|
85
|
-
**Save to workspace:**
|
|
86
|
-
Human finds something interesting → asks "save this to my research notes" → you write to a file in the workspace.
|
|
87
|
-
|
|
88
|
-
**Shopping assistant:**
|
|
89
|
-
Human is browsing → asks "add this to my wishlist" → you maintain a wishlist file in the workspace.
|
|
90
|
-
|
|
91
|
-
**Research assistant:**
|
|
92
|
-
Human is reading documentation → highlights text → asks "explain this" → you get the selected text and explain it in context.
|
|
93
|
-
|
|
94
|
-
### Working with the page URL
|
|
95
|
-
|
|
96
|
-
You have full tool access. If the context isn't enough, fetch the page yourself:
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
curl -s "https://the-url-from-context" | head -200
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Or use an MCP fetch tool if configured.
|
|
103
|
-
|
|
104
|
-
## Site-Specific Behaviors
|
|
105
|
-
|
|
106
|
-
When you detect the user is on a specific type of site (from the page context URL), adapt what you offer:
|
|
107
|
-
|
|
108
|
-
### YouTube (youtube.com, youtu.be)
|
|
109
|
-
- Offer to transcribe the video
|
|
110
|
-
- Offer to summarize, create bullet points, or extract key moments
|
|
111
|
-
- Offer to turn it into a blog post or audio summary
|
|
112
|
-
- The video URL is in the page context — you can fetch it or use tools to process it
|
|
113
|
-
|
|
114
|
-
### Shopping (amazon.com, ebay.com, walmart.com, etsy.com, etc.)
|
|
115
|
-
- Offer to find cheaper alternatives (web search)
|
|
116
|
-
- Offer to save to a wishlist (workspace file)
|
|
117
|
-
- Offer to compare prices across sites
|
|
118
|
-
- Product name and price are extracted from structured data when available
|
|
119
|
-
|
|
120
|
-
### Reddit / X / Social (reddit.com, x.com, twitter.com)
|
|
121
|
-
- Offer to summarize long threads
|
|
122
|
-
- Offer to draft a reply (the extension can type into input fields)
|
|
123
|
-
- Offer to save interesting posts to workspace notes
|
|
124
|
-
|
|
125
|
-
### Documentation / Articles (docs.*, medium.com, dev.to, any long-form text)
|
|
126
|
-
- Offer to summarize the article
|
|
127
|
-
- If text is selected, explain or elaborate on the selection
|
|
128
|
-
- Offer to save key points to workspace notes
|
|
129
|
-
|
|
130
|
-
### Forms (any page with detected form fields)
|
|
131
|
-
- When `hasForms` is true in context, offer to help fill the form
|
|
132
|
-
- Ask what information the user wants to fill
|
|
133
|
-
- Use the form field names from context to understand what's needed
|
|
134
|
-
|
|
135
|
-
### Email (gmail.com, outlook.com)
|
|
136
|
-
- Offer to draft replies
|
|
137
|
-
- Offer to summarize email threads
|
|
138
|
-
|
|
139
|
-
### Blocked Sites — NEVER interact with page content
|
|
140
|
-
- Banking: chase.com, wellsfargo.com, bankofamerica.com, any URL with /banking
|
|
141
|
-
- Auth pages: any URL with /login, /signin, /auth, /password
|
|
142
|
-
- Password managers: 1password.com, bitwarden.com, lastpass.com
|
|
143
|
-
- Medical records: mychart.com, any URL with /health-records
|
|
144
|
-
|
|
145
|
-
On blocked sites, you can still chat normally — just don't read or reference page content. If the user asks about the page, tell them: "I don't interact with sensitive pages like banking or login screens for your security."
|
|
146
|
-
|
|
147
|
-
### Default (any other site)
|
|
148
|
-
- Always share the URL and title (they're in the context)
|
|
149
|
-
- If the user asks about "this page" or "this", fetch the URL for details
|
|
150
|
-
- Offer to save interesting content to workspace
|
|
151
|
-
|
|
152
|
-
## Human Interaction
|
|
153
|
-
|
|
154
|
-
### What to tell your human
|
|
155
|
-
|
|
156
|
-
When they ask about the extension:
|
|
157
|
-
- "I can see whatever page you're on and help you with it"
|
|
158
|
-
- "Just click my bubble on any page and ask me anything"
|
|
159
|
-
- "If you highlight text on a page, I can see what you selected"
|
|
160
|
-
- "I can find better prices, summarize articles, save things to your workspace — whatever you need"
|
|
161
|
-
|
|
162
|
-
### What NOT to say
|
|
163
|
-
|
|
164
|
-
Never mention "extension", "content script", "background worker", or any technical implementation details. From your human's perspective, you're just... everywhere. Available on every page. That's it.
|
|
165
|
-
|
|
166
|
-
### Permissions
|
|
167
|
-
|
|
168
|
-
The extension only reads page content when your human actively opens the chat panel. It does NOT:
|
|
169
|
-
- Track browsing history
|
|
170
|
-
- Read pages in the background
|
|
171
|
-
- Send data anywhere except to your server
|
|
172
|
-
- Access passwords, form data, or cookies
|
|
173
|
-
|
|
174
|
-
If your human asks about privacy, be transparent about this.
|
|
175
|
-
|
|
176
|
-
## Notes
|
|
177
|
-
|
|
178
|
-
- The extension connects via the relay URL (e.g., `bloby.bloby.bot`). If the relay is down or the tunnel is offline, the extension chat won't connect. The bubble still appears but messages won't send.
|
|
179
|
-
- The pairing code is single-use and expires in 5 minutes. Generate a new one if it expires.
|
|
180
|
-
- The extension works on all pages except Chrome internal pages (`chrome://`, `chrome-extension://`) and your own Bloby dashboard domain (to avoid double-bubble).
|
|
181
|
-
- Multiple browsers/devices can be paired — each gets its own pairing code. They all share the same conversation.
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bloby Chrome Extension — Background Service Worker
|
|
3
|
-
*
|
|
4
|
-
* Responsibilities:
|
|
5
|
-
* - Manages pairing state (serverUrl stored in chrome.storage.local)
|
|
6
|
-
* - Handles pairing flow (verify code → save config)
|
|
7
|
-
* - Responds to state queries from content scripts and popup
|
|
8
|
-
*
|
|
9
|
-
* Note: WebSocket connection is handled by the chat iframe inside the panel,
|
|
10
|
-
* not by the background worker. The iframe loads the Bloby chat app which
|
|
11
|
-
* manages its own WS connection and auth (portal login).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
let config = null; // { serverUrl, username, tier }
|
|
15
|
-
|
|
16
|
-
// ── Config Management ──────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
async function loadConfig() {
|
|
19
|
-
const data = await chrome.storage.local.get(['serverUrl', 'username', 'tier']);
|
|
20
|
-
if (data.serverUrl) {
|
|
21
|
-
config = data;
|
|
22
|
-
console.log(`[bloby-bg] Config loaded: ${config.serverUrl} (${config.username})`);
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
console.log('[bloby-bg] No config found — not paired');
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function saveConfig(newConfig) {
|
|
30
|
-
config = newConfig;
|
|
31
|
-
await chrome.storage.local.set(newConfig);
|
|
32
|
-
console.log(`[bloby-bg] Config saved: ${config.serverUrl}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function clearConfig() {
|
|
36
|
-
config = null;
|
|
37
|
-
await chrome.storage.local.clear();
|
|
38
|
-
console.log('[bloby-bg] Config cleared — unpaired');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── Broadcast to All Tabs ──────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
async function broadcastToTabs(msg) {
|
|
44
|
-
try {
|
|
45
|
-
const tabs = await chrome.tabs.query({});
|
|
46
|
-
for (const tab of tabs) {
|
|
47
|
-
if (tab.id) {
|
|
48
|
-
chrome.tabs.sendMessage(tab.id, msg).catch(() => {});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
} catch { /* ignore */ }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Message Handler (from content scripts + popup) ─────────────────────────
|
|
55
|
-
|
|
56
|
-
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
57
|
-
if (msg.type === 'bloby:get-state') {
|
|
58
|
-
sendResponse({
|
|
59
|
-
paired: !!config?.serverUrl,
|
|
60
|
-
connected: true, // iframe manages its own connection
|
|
61
|
-
config: config ? { serverUrl: config.serverUrl, username: config.username } : null,
|
|
62
|
-
});
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (msg.type === 'bloby:pair') {
|
|
67
|
-
handlePair(msg.code).then(sendResponse);
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Content script: take screenshot
|
|
72
|
-
if (msg.type === 'bloby:screenshot') {
|
|
73
|
-
chrome.tabs.captureVisibleTab(null, { format: 'png' }, (dataUrl) => {
|
|
74
|
-
sendResponse({ dataUrl: dataUrl || null });
|
|
75
|
-
});
|
|
76
|
-
return true; // async
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (msg.type === 'bloby:unpair') {
|
|
80
|
-
(async () => {
|
|
81
|
-
// Clear frame rules
|
|
82
|
-
try {
|
|
83
|
-
const existing = await chrome.declarativeNetRequest.getDynamicRules();
|
|
84
|
-
await chrome.declarativeNetRequest.updateDynamicRules({
|
|
85
|
-
removeRuleIds: existing.map((r) => r.id),
|
|
86
|
-
});
|
|
87
|
-
} catch {}
|
|
88
|
-
await clearConfig();
|
|
89
|
-
broadcastToTabs({ type: 'bloby:unpaired' });
|
|
90
|
-
sendResponse({ success: true });
|
|
91
|
-
})();
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// ── Pairing Flow ───────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
async function handlePair(code) {
|
|
99
|
-
try {
|
|
100
|
-
console.log(`[bloby-bg] Verifying pairing code: ${code}`);
|
|
101
|
-
|
|
102
|
-
const res = await fetch('https://api.bloby.bot/api/extension/pair/verify', {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
headers: { 'Content-Type': 'application/json' },
|
|
105
|
-
body: JSON.stringify({ code }),
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (!res.ok) {
|
|
109
|
-
const data = await res.json().catch(() => ({}));
|
|
110
|
-
console.log(`[bloby-bg] Pairing failed: ${data.error || res.status}`);
|
|
111
|
-
return { success: false, error: data.error || 'Invalid code' };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const data = await res.json();
|
|
115
|
-
console.log(`[bloby-bg] Pairing verified: ${data.serverUrl}`);
|
|
116
|
-
|
|
117
|
-
await saveConfig({
|
|
118
|
-
serverUrl: data.serverUrl,
|
|
119
|
-
username: data.username,
|
|
120
|
-
tier: data.tier,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Set up header rules to allow iframing the Bloby server
|
|
124
|
-
await updateFrameRules(data.serverUrl);
|
|
125
|
-
|
|
126
|
-
// Notify all tabs to show the bubble
|
|
127
|
-
broadcastToTabs({ type: 'bloby:paired', serverUrl: data.serverUrl });
|
|
128
|
-
|
|
129
|
-
return { success: true, username: data.username, serverUrl: data.serverUrl };
|
|
130
|
-
} catch (err) {
|
|
131
|
-
console.error('[bloby-bg] Pairing error:', err);
|
|
132
|
-
return { success: false, error: 'Network error — check your connection' };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ── Frame Header Rules ─────────────────────────────────────────────────────
|
|
137
|
-
// Strip X-Frame-Options and restrictive frame-ancestors from the Bloby server
|
|
138
|
-
// so the chat can be loaded in the extension's panel iframe.
|
|
139
|
-
|
|
140
|
-
async function updateFrameRules(serverUrl) {
|
|
141
|
-
const host = serverUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
142
|
-
console.log(`[bloby-bg] Setting frame rules for: *://${host}/*`);
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
// Remove old rules
|
|
146
|
-
const existing = await chrome.declarativeNetRequest.getDynamicRules();
|
|
147
|
-
const removeIds = existing.map((r) => r.id);
|
|
148
|
-
|
|
149
|
-
await chrome.declarativeNetRequest.updateDynamicRules({
|
|
150
|
-
removeRuleIds: removeIds,
|
|
151
|
-
addRules: [
|
|
152
|
-
{
|
|
153
|
-
id: 1,
|
|
154
|
-
priority: 1,
|
|
155
|
-
action: {
|
|
156
|
-
type: 'modifyHeaders',
|
|
157
|
-
responseHeaders: [
|
|
158
|
-
{ header: 'X-Frame-Options', operation: 'remove' },
|
|
159
|
-
{ header: 'Content-Security-Policy', operation: 'remove' },
|
|
160
|
-
],
|
|
161
|
-
},
|
|
162
|
-
condition: {
|
|
163
|
-
urlFilter: `*://${host}/*`,
|
|
164
|
-
resourceTypes: ['sub_frame'],
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
});
|
|
169
|
-
console.log('[bloby-bg] Frame rules set successfully');
|
|
170
|
-
} catch (err) {
|
|
171
|
-
console.error('[bloby-bg] Failed to set frame rules:', err);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ── Startup ────────────────────────────────────────────────────────────────
|
|
176
|
-
|
|
177
|
-
(async () => {
|
|
178
|
-
const hasCfg = await loadConfig();
|
|
179
|
-
if (hasCfg && config.serverUrl) {
|
|
180
|
-
await updateFrameRules(config.serverUrl);
|
|
181
|
-
}
|
|
182
|
-
})();
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bloby Chrome Extension — Content Script
|
|
3
|
-
*
|
|
4
|
-
* Injected into every page. Responsibilities:
|
|
5
|
-
* - Renders the Bloby bubble (bottom-right corner)
|
|
6
|
-
* - Opens/closes the chat panel (iframe to extension's panel.html)
|
|
7
|
-
* - Extracts page context (URL, title, selection, structured data)
|
|
8
|
-
* - Handles ExtensionAction commands from the agent (screenshot, fill form, type)
|
|
9
|
-
* - Bridges messages between the panel iframe and the background worker
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
(function () {
|
|
13
|
-
// Guard: don't inject on extension pages or Bloby's own domain
|
|
14
|
-
if (document.getElementById('bloby-ext-bubble')) return;
|
|
15
|
-
if (location.hostname.endsWith('bloby.bot')) return;
|
|
16
|
-
|
|
17
|
-
let panelOpen = false;
|
|
18
|
-
let paired = false;
|
|
19
|
-
|
|
20
|
-
// ── Check if paired ────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
chrome.runtime.sendMessage({ type: 'bloby:get-state' }, (state) => {
|
|
23
|
-
if (chrome.runtime.lastError) return;
|
|
24
|
-
if (state?.paired) {
|
|
25
|
-
paired = true;
|
|
26
|
-
createBubble();
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
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
|
|
44
|
-
chrome.storage.onChanged.addListener((changes) => {
|
|
45
|
-
if (changes.serverUrl) {
|
|
46
|
-
if (changes.serverUrl.newValue) {
|
|
47
|
-
paired = true;
|
|
48
|
-
if (!document.getElementById('bloby-ext-bubble')) createBubble();
|
|
49
|
-
} else {
|
|
50
|
-
paired = false;
|
|
51
|
-
removeBubble();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// ── Bubble ─────────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
function createBubble() {
|
|
59
|
-
const bubble = document.createElement('div');
|
|
60
|
-
bubble.id = 'bloby-ext-bubble';
|
|
61
|
-
bubble.title = 'Chat with Bloby';
|
|
62
|
-
bubble.addEventListener('click', togglePanel);
|
|
63
|
-
document.body.appendChild(bubble);
|
|
64
|
-
|
|
65
|
-
const dot = document.createElement('div');
|
|
66
|
-
dot.className = 'bloby-ext-bubble-dot';
|
|
67
|
-
bubble.appendChild(dot);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function removeBubble() {
|
|
71
|
-
document.getElementById('bloby-ext-bubble')?.remove();
|
|
72
|
-
document.getElementById('bloby-ext-panel')?.remove();
|
|
73
|
-
document.getElementById('bloby-ext-backdrop')?.remove();
|
|
74
|
-
panelOpen = false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── Panel ──────────────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
function togglePanel() {
|
|
80
|
-
panelOpen ? closePanel() : openPanel();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function openPanel() {
|
|
84
|
-
if (document.getElementById('bloby-ext-panel')) {
|
|
85
|
-
document.getElementById('bloby-ext-panel').classList.add('bloby-ext-panel-open');
|
|
86
|
-
panelOpen = true;
|
|
87
|
-
showBackdrop();
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
showBackdrop();
|
|
92
|
-
|
|
93
|
-
const panel = document.createElement('div');
|
|
94
|
-
panel.id = 'bloby-ext-panel';
|
|
95
|
-
|
|
96
|
-
const iframe = document.createElement('iframe');
|
|
97
|
-
iframe.id = 'bloby-ext-iframe';
|
|
98
|
-
iframe.src = chrome.runtime.getURL('panel/panel.html');
|
|
99
|
-
iframe.allow = 'microphone';
|
|
100
|
-
panel.appendChild(iframe);
|
|
101
|
-
|
|
102
|
-
document.body.appendChild(panel);
|
|
103
|
-
|
|
104
|
-
requestAnimationFrame(() => {
|
|
105
|
-
panel.classList.add('bloby-ext-panel-open');
|
|
106
|
-
panelOpen = true;
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
window.addEventListener('message', handlePanelMessage);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function closePanel() {
|
|
113
|
-
const panel = document.getElementById('bloby-ext-panel');
|
|
114
|
-
if (panel) {
|
|
115
|
-
panel.classList.remove('bloby-ext-panel-open');
|
|
116
|
-
panelOpen = false;
|
|
117
|
-
hideBackdrop();
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function showBackdrop() {
|
|
122
|
-
let backdrop = document.getElementById('bloby-ext-backdrop');
|
|
123
|
-
if (!backdrop) {
|
|
124
|
-
backdrop = document.createElement('div');
|
|
125
|
-
backdrop.id = 'bloby-ext-backdrop';
|
|
126
|
-
backdrop.addEventListener('click', closePanel);
|
|
127
|
-
document.body.appendChild(backdrop);
|
|
128
|
-
}
|
|
129
|
-
requestAnimationFrame(() => backdrop.classList.add('bloby-ext-backdrop-visible'));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function hideBackdrop() {
|
|
133
|
-
const backdrop = document.getElementById('bloby-ext-backdrop');
|
|
134
|
-
if (backdrop) backdrop.classList.remove('bloby-ext-backdrop-visible');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── Message Bridge (panel ↔ content script) ────────────────────────────────
|
|
138
|
-
|
|
139
|
-
function handlePanelMessage(event) {
|
|
140
|
-
const iframe = document.getElementById('bloby-ext-iframe');
|
|
141
|
-
if (!iframe || event.source !== iframe.contentWindow) return;
|
|
142
|
-
|
|
143
|
-
const msg = event.data;
|
|
144
|
-
if (!msg?.type) return;
|
|
145
|
-
|
|
146
|
-
if (msg.type === 'bloby:close') {
|
|
147
|
-
closePanel();
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Panel requests page context
|
|
152
|
-
if (msg.type === 'bloby:get-page-context') {
|
|
153
|
-
const context = getPageContext();
|
|
154
|
-
iframe.contentWindow.postMessage({ type: 'bloby:page-context', context }, '*');
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Agent requests an action on the page
|
|
159
|
-
if (msg.type === 'bloby:extension-action') {
|
|
160
|
-
handleExtensionAction(msg.action, msg.data);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ── Page Context Extraction ────────────────────────────────────────────────
|
|
166
|
-
|
|
167
|
-
function getPageContext() {
|
|
168
|
-
const context = {
|
|
169
|
-
url: location.href,
|
|
170
|
-
title: document.title,
|
|
171
|
-
selection: window.getSelection()?.toString()?.slice(0, 2000) || '',
|
|
172
|
-
};
|
|
173
|
-
|
|
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 */ }
|
|
190
|
-
|
|
191
|
-
// Meta description fallback
|
|
192
|
-
if (!context.description) {
|
|
193
|
-
const meta = document.querySelector('meta[name="description"]');
|
|
194
|
-
if (meta) context.description = meta.content?.slice(0, 500);
|
|
195
|
-
}
|
|
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);
|
|
217
|
-
return context;
|
|
218
|
-
}
|
|
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
|
-
|
|
304
|
-
// ── Keyboard shortcut ──────────────────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
document.addEventListener('keydown', (e) => {
|
|
307
|
-
if (e.key === 'Escape' && panelOpen) closePanel();
|
|
308
|
-
});
|
|
309
|
-
})();
|