bloby-bot 0.22.0 → 0.22.1
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/package.json +1 -1
- package/workspace/skills/chrome-extension/.claude-plugin/plugin.json +6 -0
- package/workspace/skills/chrome-extension/SKILL.md +133 -0
- package/workspace/skills/chrome-extension/background.js +239 -0
- package/workspace/skills/chrome-extension/content-script.js +218 -0
- package/workspace/skills/chrome-extension/content-style.css +86 -0
- 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 +46 -0
- package/workspace/skills/chrome-extension/panel/panel.html +74 -0
- package/workspace/skills/chrome-extension/popup/popup.css +124 -0
- package/workspace/skills/chrome-extension/popup/popup.html +47 -0
- package/workspace/skills/chrome-extension/popup/popup.js +115 -0
- package/workspace/skills/chrome-extension/skill.json +15 -0
package/package.json
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
## Human Interaction
|
|
105
|
+
|
|
106
|
+
### What to tell your human
|
|
107
|
+
|
|
108
|
+
When they ask about the extension:
|
|
109
|
+
- "I can see whatever page you're on and help you with it"
|
|
110
|
+
- "Just click my bubble on any page and ask me anything"
|
|
111
|
+
- "If you highlight text on a page, I can see what you selected"
|
|
112
|
+
- "I can find better prices, summarize articles, save things to your workspace — whatever you need"
|
|
113
|
+
|
|
114
|
+
### What NOT to say
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
### Permissions
|
|
119
|
+
|
|
120
|
+
The extension only reads page content when your human actively opens the chat panel. It does NOT:
|
|
121
|
+
- Track browsing history
|
|
122
|
+
- Read pages in the background
|
|
123
|
+
- Send data anywhere except to your server
|
|
124
|
+
- Access passwords, form data, or cookies
|
|
125
|
+
|
|
126
|
+
If your human asks about privacy, be transparent about this.
|
|
127
|
+
|
|
128
|
+
## Notes
|
|
129
|
+
|
|
130
|
+
- 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.
|
|
131
|
+
- The pairing code is single-use and expires in 5 minutes. Generate a new one if it expires.
|
|
132
|
+
- The extension works on all pages except Chrome internal pages (`chrome://`, `chrome-extension://`) and your own Bloby dashboard domain (to avoid double-bubble).
|
|
133
|
+
- Multiple browsers/devices can be paired — each gets its own pairing code. They all share the same conversation.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloby Chrome Extension — Background Service Worker
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Manages the WebSocket connection to the user's Bloby server
|
|
6
|
+
* - Bridges messages between content scripts and the server
|
|
7
|
+
* - Handles pairing state (serverUrl stored in chrome.storage.local)
|
|
8
|
+
* - Keeps WS alive with heartbeat pings
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let ws = null;
|
|
12
|
+
let config = null; // { serverUrl, username, tier }
|
|
13
|
+
let reconnectTimer = null;
|
|
14
|
+
let reconnectDelay = 1000;
|
|
15
|
+
const MAX_RECONNECT_DELAY = 8000;
|
|
16
|
+
const HEARTBEAT_MS = 25000;
|
|
17
|
+
let heartbeatTimer = null;
|
|
18
|
+
|
|
19
|
+
// ── Config Management ──────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
async function loadConfig() {
|
|
22
|
+
const data = await chrome.storage.local.get(['serverUrl', 'username', 'tier', 'authToken']);
|
|
23
|
+
if (data.serverUrl) {
|
|
24
|
+
config = data;
|
|
25
|
+
console.log(`[bloby-bg] Config loaded: ${config.serverUrl} (${config.username})`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
console.log('[bloby-bg] No config found — not paired');
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function saveConfig(newConfig) {
|
|
33
|
+
config = newConfig;
|
|
34
|
+
await chrome.storage.local.set(newConfig);
|
|
35
|
+
console.log(`[bloby-bg] Config saved: ${config.serverUrl}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function clearConfig() {
|
|
39
|
+
config = null;
|
|
40
|
+
await chrome.storage.local.clear();
|
|
41
|
+
disconnectWs();
|
|
42
|
+
console.log('[bloby-bg] Config cleared — unpaired');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── WebSocket Connection ───────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function connectWs() {
|
|
48
|
+
if (!config?.serverUrl) return;
|
|
49
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
50
|
+
|
|
51
|
+
const proto = config.serverUrl.startsWith('https') ? 'wss' : 'ws';
|
|
52
|
+
const host = config.serverUrl.replace(/^https?:\/\//, '');
|
|
53
|
+
let url = `${proto}://${host}/bloby/ws`;
|
|
54
|
+
if (config.authToken) {
|
|
55
|
+
url += `?token=${config.authToken}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`[bloby-bg] Connecting WS: ${url.replace(/token=.*/, 'token=***')}`);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
ws = new WebSocket(url);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('[bloby-bg] WS constructor error:', err);
|
|
64
|
+
scheduleReconnect();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ws.onopen = () => {
|
|
69
|
+
console.log('[bloby-bg] WS connected');
|
|
70
|
+
reconnectDelay = 1000;
|
|
71
|
+
startHeartbeat();
|
|
72
|
+
// Notify all tabs
|
|
73
|
+
broadcastToTabs({ type: 'bloby:ws-connected' });
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
ws.onmessage = (event) => {
|
|
77
|
+
try {
|
|
78
|
+
const msg = JSON.parse(event.data);
|
|
79
|
+
// Forward all server messages to content scripts
|
|
80
|
+
broadcastToTabs(msg);
|
|
81
|
+
} catch { /* ignore non-JSON */ }
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
ws.onclose = (event) => {
|
|
85
|
+
console.log(`[bloby-bg] WS closed: code=${event.code} reason=${event.reason}`);
|
|
86
|
+
stopHeartbeat();
|
|
87
|
+
ws = null;
|
|
88
|
+
broadcastToTabs({ type: 'bloby:ws-disconnected' });
|
|
89
|
+
scheduleReconnect();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
ws.onerror = (err) => {
|
|
93
|
+
console.error('[bloby-bg] WS error');
|
|
94
|
+
// onclose will fire after this
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function disconnectWs() {
|
|
99
|
+
stopHeartbeat();
|
|
100
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
101
|
+
if (ws) {
|
|
102
|
+
ws.onclose = null; // prevent reconnect
|
|
103
|
+
ws.close();
|
|
104
|
+
ws = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function scheduleReconnect() {
|
|
109
|
+
if (reconnectTimer) return;
|
|
110
|
+
console.log(`[bloby-bg] Reconnecting in ${reconnectDelay}ms...`);
|
|
111
|
+
reconnectTimer = setTimeout(() => {
|
|
112
|
+
reconnectTimer = null;
|
|
113
|
+
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
114
|
+
connectWs();
|
|
115
|
+
}, reconnectDelay);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function startHeartbeat() {
|
|
119
|
+
stopHeartbeat();
|
|
120
|
+
heartbeatTimer = setInterval(() => {
|
|
121
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
122
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
123
|
+
}
|
|
124
|
+
}, HEARTBEAT_MS);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function stopHeartbeat() {
|
|
128
|
+
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Send to Server ─────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function sendToServer(msg) {
|
|
134
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
135
|
+
ws.send(JSON.stringify(msg));
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
console.warn('[bloby-bg] WS not connected — message dropped');
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Broadcast to All Tabs ──────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
async function broadcastToTabs(msg) {
|
|
145
|
+
try {
|
|
146
|
+
const tabs = await chrome.tabs.query({});
|
|
147
|
+
for (const tab of tabs) {
|
|
148
|
+
if (tab.id) {
|
|
149
|
+
chrome.tabs.sendMessage(tab.id, msg).catch(() => {
|
|
150
|
+
// Tab doesn't have content script — ignore
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch { /* ignore */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Message Handler (from content scripts + popup) ─────────────────────────
|
|
158
|
+
|
|
159
|
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
160
|
+
// Popup: get current state
|
|
161
|
+
if (msg.type === 'bloby:get-state') {
|
|
162
|
+
sendResponse({
|
|
163
|
+
paired: !!config?.serverUrl,
|
|
164
|
+
connected: ws?.readyState === WebSocket.OPEN,
|
|
165
|
+
config: config ? { serverUrl: config.serverUrl, username: config.username } : null,
|
|
166
|
+
});
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Popup: pair with code
|
|
171
|
+
if (msg.type === 'bloby:pair') {
|
|
172
|
+
handlePair(msg.code).then(sendResponse);
|
|
173
|
+
return true; // async response
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Popup: unpair
|
|
177
|
+
if (msg.type === 'bloby:unpair') {
|
|
178
|
+
clearConfig().then(() => sendResponse({ success: true }));
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Content script: send message to server
|
|
183
|
+
if (msg.type === 'bloby:send') {
|
|
184
|
+
const ok = sendToServer(msg.payload);
|
|
185
|
+
sendResponse({ sent: ok });
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Content script: get page context
|
|
190
|
+
if (msg.type === 'bloby:page-context') {
|
|
191
|
+
// Content script provides context, we just forward
|
|
192
|
+
sendResponse({ received: true });
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ── Pairing Flow ───────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
async function handlePair(code) {
|
|
200
|
+
try {
|
|
201
|
+
console.log(`[bloby-bg] Verifying pairing code: ${code}`);
|
|
202
|
+
|
|
203
|
+
const res = await fetch('https://api.bloby.bot/api/extension/pair/verify', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
206
|
+
body: JSON.stringify({ code }),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!res.ok) {
|
|
210
|
+
const data = await res.json().catch(() => ({}));
|
|
211
|
+
console.log(`[bloby-bg] Pairing failed: ${data.error || res.status}`);
|
|
212
|
+
return { success: false, error: data.error || 'Invalid code' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const data = await res.json();
|
|
216
|
+
console.log(`[bloby-bg] Pairing verified: ${data.serverUrl}`);
|
|
217
|
+
|
|
218
|
+
await saveConfig({
|
|
219
|
+
serverUrl: data.serverUrl,
|
|
220
|
+
username: data.username,
|
|
221
|
+
tier: data.tier,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Connect immediately
|
|
225
|
+
connectWs();
|
|
226
|
+
|
|
227
|
+
return { success: true, username: data.username, serverUrl: data.serverUrl };
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error('[bloby-bg] Pairing error:', err);
|
|
230
|
+
return { success: false, error: 'Network error — check your connection' };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Startup ────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
(async () => {
|
|
237
|
+
const hasCfg = await loadConfig();
|
|
238
|
+
if (hasCfg) connectWs();
|
|
239
|
+
})();
|
|
@@ -0,0 +1,218 @@
|
|
|
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) for the agent
|
|
8
|
+
* - Bridges messages between the panel iframe and the background worker
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
(function () {
|
|
12
|
+
// Guard: don't inject on extension pages or Bloby's own domain
|
|
13
|
+
if (document.getElementById('bloby-ext-bubble')) return;
|
|
14
|
+
if (location.hostname.endsWith('bloby.bot')) return;
|
|
15
|
+
|
|
16
|
+
const BUBBLE_SIZE = 60;
|
|
17
|
+
const BUBBLE_MARGIN = 24;
|
|
18
|
+
const PANEL_WIDTH = 420;
|
|
19
|
+
|
|
20
|
+
let panelOpen = false;
|
|
21
|
+
let paired = false;
|
|
22
|
+
|
|
23
|
+
// ── Check if paired ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
chrome.runtime.sendMessage({ type: 'bloby:get-state' }, (state) => {
|
|
26
|
+
if (chrome.runtime.lastError) return;
|
|
27
|
+
if (state?.paired) {
|
|
28
|
+
paired = true;
|
|
29
|
+
createBubble();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Listen for pairing changes
|
|
34
|
+
chrome.storage.onChanged.addListener((changes) => {
|
|
35
|
+
if (changes.serverUrl) {
|
|
36
|
+
if (changes.serverUrl.newValue) {
|
|
37
|
+
paired = true;
|
|
38
|
+
if (!document.getElementById('bloby-ext-bubble')) createBubble();
|
|
39
|
+
} else {
|
|
40
|
+
paired = false;
|
|
41
|
+
removeBubble();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── Bubble ─────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function createBubble() {
|
|
49
|
+
const bubble = document.createElement('div');
|
|
50
|
+
bubble.id = 'bloby-ext-bubble';
|
|
51
|
+
bubble.title = 'Chat with Bloby';
|
|
52
|
+
bubble.addEventListener('click', togglePanel);
|
|
53
|
+
document.body.appendChild(bubble);
|
|
54
|
+
|
|
55
|
+
// Inner dot (gradient)
|
|
56
|
+
const dot = document.createElement('div');
|
|
57
|
+
dot.className = 'bloby-ext-bubble-dot';
|
|
58
|
+
bubble.appendChild(dot);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
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();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Panel ──────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function togglePanel() {
|
|
73
|
+
if (panelOpen) {
|
|
74
|
+
closePanel();
|
|
75
|
+
} else {
|
|
76
|
+
openPanel();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openPanel() {
|
|
81
|
+
if (document.getElementById('bloby-ext-panel')) {
|
|
82
|
+
const panel = document.getElementById('bloby-ext-panel');
|
|
83
|
+
panel.classList.add('bloby-ext-panel-open');
|
|
84
|
+
panelOpen = true;
|
|
85
|
+
showBackdrop();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create backdrop
|
|
90
|
+
showBackdrop();
|
|
91
|
+
|
|
92
|
+
// Create panel
|
|
93
|
+
const panel = document.createElement('div');
|
|
94
|
+
panel.id = 'bloby-ext-panel';
|
|
95
|
+
|
|
96
|
+
// Create iframe — loads the extension's own panel page
|
|
97
|
+
const iframe = document.createElement('iframe');
|
|
98
|
+
iframe.id = 'bloby-ext-iframe';
|
|
99
|
+
iframe.src = chrome.runtime.getURL('panel/panel.html');
|
|
100
|
+
iframe.allow = 'microphone';
|
|
101
|
+
panel.appendChild(iframe);
|
|
102
|
+
|
|
103
|
+
document.body.appendChild(panel);
|
|
104
|
+
|
|
105
|
+
// Trigger slide-in animation on next frame
|
|
106
|
+
requestAnimationFrame(() => {
|
|
107
|
+
panel.classList.add('bloby-ext-panel-open');
|
|
108
|
+
panelOpen = true;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Listen for messages from the panel iframe
|
|
112
|
+
window.addEventListener('message', handlePanelMessage);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function closePanel() {
|
|
116
|
+
const panel = document.getElementById('bloby-ext-panel');
|
|
117
|
+
if (panel) {
|
|
118
|
+
panel.classList.remove('bloby-ext-panel-open');
|
|
119
|
+
panelOpen = false;
|
|
120
|
+
hideBackdrop();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function showBackdrop() {
|
|
125
|
+
let backdrop = document.getElementById('bloby-ext-backdrop');
|
|
126
|
+
if (!backdrop) {
|
|
127
|
+
backdrop = document.createElement('div');
|
|
128
|
+
backdrop.id = 'bloby-ext-backdrop';
|
|
129
|
+
backdrop.addEventListener('click', closePanel);
|
|
130
|
+
document.body.appendChild(backdrop);
|
|
131
|
+
}
|
|
132
|
+
requestAnimationFrame(() => backdrop.classList.add('bloby-ext-backdrop-visible'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function hideBackdrop() {
|
|
136
|
+
const backdrop = document.getElementById('bloby-ext-backdrop');
|
|
137
|
+
if (backdrop) {
|
|
138
|
+
backdrop.classList.remove('bloby-ext-backdrop-visible');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Panel ↔ Background Message Bridge ──────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
function handlePanelMessage(event) {
|
|
145
|
+
if (event.source !== document.getElementById('bloby-ext-iframe')?.contentWindow) return;
|
|
146
|
+
|
|
147
|
+
const msg = event.data;
|
|
148
|
+
if (!msg?.type) return;
|
|
149
|
+
|
|
150
|
+
// Close panel request
|
|
151
|
+
if (msg.type === 'bloby:close') {
|
|
152
|
+
closePanel();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
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
|
+
// Panel requests page context
|
|
163
|
+
if (msg.type === 'bloby:get-page-context') {
|
|
164
|
+
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
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
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, '*');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ── Page Context Extraction ────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function getPageContext() {
|
|
184
|
+
const context = {
|
|
185
|
+
url: location.href,
|
|
186
|
+
title: document.title,
|
|
187
|
+
selection: window.getSelection()?.toString()?.slice(0, 2000) || '',
|
|
188
|
+
};
|
|
189
|
+
|
|
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
|
+
}
|
|
201
|
+
|
|
202
|
+
// Meta description fallback
|
|
203
|
+
const metaDesc = document.querySelector('meta[name="description"]');
|
|
204
|
+
if (metaDesc && !context.description) {
|
|
205
|
+
context.description = metaDesc.content?.slice(0, 500);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return context;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Keyboard shortcut ──────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
document.addEventListener('keydown', (e) => {
|
|
214
|
+
if (e.key === 'Escape' && panelOpen) {
|
|
215
|
+
closePanel();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
})();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* Bloby Chrome Extension — Bubble & Panel Styles */
|
|
2
|
+
|
|
3
|
+
#bloby-ext-bubble {
|
|
4
|
+
position: fixed;
|
|
5
|
+
bottom: 24px;
|
|
6
|
+
right: 24px;
|
|
7
|
+
width: 60px;
|
|
8
|
+
height: 60px;
|
|
9
|
+
border-radius: 50%;
|
|
10
|
+
background: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
z-index: 2147483646;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
17
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#bloby-ext-bubble:hover {
|
|
21
|
+
transform: scale(1.08);
|
|
22
|
+
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#bloby-ext-bubble:active {
|
|
26
|
+
transform: scale(0.95);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.bloby-ext-bubble-dot {
|
|
30
|
+
width: 24px;
|
|
31
|
+
height: 24px;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
background: rgba(255, 255, 255, 0.9);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Panel */
|
|
37
|
+
#bloby-ext-panel {
|
|
38
|
+
position: fixed;
|
|
39
|
+
top: 0;
|
|
40
|
+
right: 0;
|
|
41
|
+
bottom: 0;
|
|
42
|
+
width: 420px;
|
|
43
|
+
z-index: 2147483647;
|
|
44
|
+
transform: translateX(100%);
|
|
45
|
+
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
46
|
+
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3);
|
|
47
|
+
border-left: 1px solid #3a3a3a;
|
|
48
|
+
background: #212121;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#bloby-ext-panel.bloby-ext-panel-open {
|
|
52
|
+
transform: translateX(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Mobile: full width */
|
|
56
|
+
@media (max-width: 480px) {
|
|
57
|
+
#bloby-ext-panel {
|
|
58
|
+
width: 100vw;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#bloby-ext-iframe {
|
|
63
|
+
width: 100%;
|
|
64
|
+
height: 100%;
|
|
65
|
+
border: none;
|
|
66
|
+
background: #212121;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Backdrop */
|
|
70
|
+
#bloby-ext-backdrop {
|
|
71
|
+
position: fixed;
|
|
72
|
+
top: 0;
|
|
73
|
+
left: 0;
|
|
74
|
+
right: 0;
|
|
75
|
+
bottom: 0;
|
|
76
|
+
background: rgba(0, 0, 0, 0.4);
|
|
77
|
+
z-index: 2147483645;
|
|
78
|
+
opacity: 0;
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
transition: opacity 0.25s ease;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#bloby-ext-backdrop.bloby-ext-backdrop-visible {
|
|
84
|
+
opacity: 1;
|
|
85
|
+
pointer-events: auto;
|
|
86
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "Bloby",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Your AI agent, on every page. Chat with Bloby from anywhere.",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"storage",
|
|
8
|
+
"activeTab"
|
|
9
|
+
],
|
|
10
|
+
"host_permissions": [
|
|
11
|
+
"https://*.bloby.bot/*",
|
|
12
|
+
"https://*.my.bloby.bot/*",
|
|
13
|
+
"https://*.trycloudflare.com/*"
|
|
14
|
+
],
|
|
15
|
+
"action": {
|
|
16
|
+
"default_popup": "popup/popup.html",
|
|
17
|
+
"default_icon": {
|
|
18
|
+
"16": "icons/icon-16.png",
|
|
19
|
+
"48": "icons/icon-48.png",
|
|
20
|
+
"128": "icons/icon-128.png"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"background": {
|
|
24
|
+
"service_worker": "background.js",
|
|
25
|
+
"type": "module"
|
|
26
|
+
},
|
|
27
|
+
"content_scripts": [
|
|
28
|
+
{
|
|
29
|
+
"matches": ["<all_urls>"],
|
|
30
|
+
"js": ["content-script.js"],
|
|
31
|
+
"css": ["content-style.css"],
|
|
32
|
+
"run_at": "document_idle"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"web_accessible_resources": [
|
|
36
|
+
{
|
|
37
|
+
"resources": ["panel/panel.html", "assets/*", "icons/*"],
|
|
38
|
+
"matches": ["<all_urls>"]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"icons": {
|
|
42
|
+
"16": "icons/icon-16.png",
|
|
43
|
+
"48": "icons/icon-48.png",
|
|
44
|
+
"128": "icons/icon-128.png"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
6
|
+
<title>Bloby</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { background: #212121; width: 100%; height: 100vh; overflow: hidden; }
|
|
10
|
+
iframe { width: 100%; height: 100%; border: none; }
|
|
11
|
+
.loading {
|
|
12
|
+
display: flex; align-items: center; justify-content: center;
|
|
13
|
+
height: 100vh; color: #a1a1aa; font-family: system-ui, sans-serif;
|
|
14
|
+
font-size: 14px;
|
|
15
|
+
}
|
|
16
|
+
.loading .dots span {
|
|
17
|
+
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
|
18
|
+
background: #a1a1aa; margin: 0 3px; animation: bounce 1s infinite;
|
|
19
|
+
}
|
|
20
|
+
.loading .dots span:nth-child(2) { animation-delay: 0.15s; }
|
|
21
|
+
.loading .dots span:nth-child(3) { animation-delay: 0.3s; }
|
|
22
|
+
@keyframes bounce {
|
|
23
|
+
0%, 80%, 100% { transform: scale(0); }
|
|
24
|
+
40% { transform: scale(1); }
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div class="loading" id="loading">
|
|
30
|
+
<div class="dots"><span></span><span></span><span></span></div>
|
|
31
|
+
</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>
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
width: 320px;
|
|
9
|
+
min-height: 280px;
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
background: #0a0a0b;
|
|
12
|
+
color: #e4e4e7;
|
|
13
|
+
padding: 32px 24px;
|
|
14
|
+
text-align: center;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.logo {
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
margin-bottom: 20px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.logo-dot {
|
|
24
|
+
width: 48px;
|
|
25
|
+
height: 48px;
|
|
26
|
+
border-radius: 50%;
|
|
27
|
+
background: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.logo-dot.connected {
|
|
31
|
+
box-shadow: 0 0 20px rgba(4, 209, 254, 0.4);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
h1 {
|
|
35
|
+
font-size: 18px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
margin-bottom: 6px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.subtitle {
|
|
41
|
+
font-size: 13px;
|
|
42
|
+
color: #71717a;
|
|
43
|
+
margin-bottom: 24px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Code Input */
|
|
47
|
+
.code-input {
|
|
48
|
+
display: flex;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
margin-bottom: 16px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.code-input input {
|
|
55
|
+
width: 40px;
|
|
56
|
+
height: 48px;
|
|
57
|
+
border: 2px solid #27272a;
|
|
58
|
+
border-radius: 10px;
|
|
59
|
+
background: #18181b;
|
|
60
|
+
color: #e4e4e7;
|
|
61
|
+
font-size: 22px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
text-align: center;
|
|
64
|
+
outline: none;
|
|
65
|
+
transition: border-color 0.15s;
|
|
66
|
+
caret-color: transparent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.code-input input:focus {
|
|
70
|
+
border-color: #AF27E3;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.code-input input.filled {
|
|
74
|
+
border-color: #04D1FE;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.error {
|
|
78
|
+
color: #ef4444;
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
min-height: 18px;
|
|
81
|
+
margin-bottom: 8px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.pairing {
|
|
85
|
+
color: #a1a1aa;
|
|
86
|
+
font-size: 13px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Connected Screen */
|
|
90
|
+
.status-badge {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 6px;
|
|
94
|
+
background: #18181b;
|
|
95
|
+
border: 1px solid #27272a;
|
|
96
|
+
border-radius: 999px;
|
|
97
|
+
padding: 6px 14px;
|
|
98
|
+
font-size: 13px;
|
|
99
|
+
color: #a1a1aa;
|
|
100
|
+
margin-bottom: 20px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.status-dot {
|
|
104
|
+
width: 8px;
|
|
105
|
+
height: 8px;
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
background: #22c55e;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.btn-disconnect {
|
|
111
|
+
background: none;
|
|
112
|
+
border: 1px solid #27272a;
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
color: #71717a;
|
|
115
|
+
padding: 8px 16px;
|
|
116
|
+
font-size: 13px;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
transition: all 0.15s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.btn-disconnect:hover {
|
|
122
|
+
border-color: #ef4444;
|
|
123
|
+
color: #ef4444;
|
|
124
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Bloby</title>
|
|
7
|
+
<link rel="stylesheet" href="popup.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<!-- Pairing Screen -->
|
|
11
|
+
<div id="pair-screen">
|
|
12
|
+
<div class="logo">
|
|
13
|
+
<div class="logo-dot"></div>
|
|
14
|
+
</div>
|
|
15
|
+
<h1>Connect to Bloby</h1>
|
|
16
|
+
<p class="subtitle">Ask your Bloby for a pairing code</p>
|
|
17
|
+
|
|
18
|
+
<div class="code-input" id="code-input">
|
|
19
|
+
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric" autofocus>
|
|
20
|
+
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
|
|
21
|
+
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
|
|
22
|
+
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
|
|
23
|
+
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
|
|
24
|
+
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div id="error" class="error"></div>
|
|
28
|
+
<div id="pairing" class="pairing" style="display:none">Connecting...</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Connected Screen -->
|
|
32
|
+
<div id="connected-screen" style="display:none">
|
|
33
|
+
<div class="logo">
|
|
34
|
+
<div class="logo-dot connected"></div>
|
|
35
|
+
</div>
|
|
36
|
+
<h1 id="connected-name">Connected</h1>
|
|
37
|
+
<p class="subtitle" id="connected-url"></p>
|
|
38
|
+
<div class="status-badge">
|
|
39
|
+
<span class="status-dot"></span>
|
|
40
|
+
<span id="status-text">Connected</span>
|
|
41
|
+
</div>
|
|
42
|
+
<button id="disconnect-btn" class="btn-disconnect">Disconnect</button>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script src="popup.js"></script>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloby Chrome Extension — Popup
|
|
3
|
+
*
|
|
4
|
+
* Shows either:
|
|
5
|
+
* - Pairing screen: 6-digit code input to connect to a Bloby instance
|
|
6
|
+
* - Connected screen: status + disconnect button
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const pairScreen = document.getElementById('pair-screen');
|
|
10
|
+
const connectedScreen = document.getElementById('connected-screen');
|
|
11
|
+
const errorEl = document.getElementById('error');
|
|
12
|
+
const pairingEl = document.getElementById('pairing');
|
|
13
|
+
const inputs = document.querySelectorAll('.code-input input');
|
|
14
|
+
|
|
15
|
+
// ── Init: check current state ──────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
chrome.runtime.sendMessage({ type: 'bloby:get-state' }, (state) => {
|
|
18
|
+
if (state?.paired) {
|
|
19
|
+
showConnected(state);
|
|
20
|
+
} else {
|
|
21
|
+
showPairing();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ── Pairing Screen ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function showPairing() {
|
|
28
|
+
pairScreen.style.display = 'block';
|
|
29
|
+
connectedScreen.style.display = 'none';
|
|
30
|
+
inputs[0].focus();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Auto-advance between code inputs
|
|
34
|
+
inputs.forEach((input, i) => {
|
|
35
|
+
input.addEventListener('input', (e) => {
|
|
36
|
+
const val = e.target.value.replace(/\D/g, '');
|
|
37
|
+
e.target.value = val;
|
|
38
|
+
|
|
39
|
+
if (val) {
|
|
40
|
+
e.target.classList.add('filled');
|
|
41
|
+
if (i < inputs.length - 1) {
|
|
42
|
+
inputs[i + 1].focus();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if all filled
|
|
47
|
+
const code = Array.from(inputs).map((inp) => inp.value).join('');
|
|
48
|
+
if (code.length === 6) {
|
|
49
|
+
submitCode(code);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
input.addEventListener('keydown', (e) => {
|
|
54
|
+
if (e.key === 'Backspace' && !e.target.value && i > 0) {
|
|
55
|
+
inputs[i - 1].focus();
|
|
56
|
+
inputs[i - 1].value = '';
|
|
57
|
+
inputs[i - 1].classList.remove('filled');
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Handle paste
|
|
62
|
+
input.addEventListener('paste', (e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
const pasted = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, 6);
|
|
65
|
+
pasted.split('').forEach((char, j) => {
|
|
66
|
+
if (inputs[j]) {
|
|
67
|
+
inputs[j].value = char;
|
|
68
|
+
inputs[j].classList.add('filled');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (pasted.length === 6) {
|
|
72
|
+
submitCode(pasted);
|
|
73
|
+
} else if (pasted.length > 0) {
|
|
74
|
+
inputs[Math.min(pasted.length, 5)].focus();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
async function submitCode(code) {
|
|
80
|
+
errorEl.textContent = '';
|
|
81
|
+
pairingEl.style.display = 'block';
|
|
82
|
+
inputs.forEach((inp) => { inp.disabled = true; });
|
|
83
|
+
|
|
84
|
+
chrome.runtime.sendMessage({ type: 'bloby:pair', code }, (result) => {
|
|
85
|
+
pairingEl.style.display = 'none';
|
|
86
|
+
|
|
87
|
+
if (result?.success) {
|
|
88
|
+
showConnected({
|
|
89
|
+
config: { serverUrl: result.serverUrl, username: result.username },
|
|
90
|
+
connected: true,
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
errorEl.textContent = result?.error || 'Pairing failed';
|
|
94
|
+
inputs.forEach((inp) => { inp.disabled = false; inp.value = ''; inp.classList.remove('filled'); });
|
|
95
|
+
inputs[0].focus();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Connected Screen ───────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function showConnected(state) {
|
|
103
|
+
pairScreen.style.display = 'none';
|
|
104
|
+
connectedScreen.style.display = 'block';
|
|
105
|
+
|
|
106
|
+
document.getElementById('connected-name').textContent = state.config?.username || 'Connected';
|
|
107
|
+
document.getElementById('connected-url').textContent = state.config?.serverUrl || '';
|
|
108
|
+
document.getElementById('status-text').textContent = state.connected ? 'Connected' : 'Offline';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
document.getElementById('disconnect-btn').addEventListener('click', () => {
|
|
112
|
+
chrome.runtime.sendMessage({ type: 'bloby:unpair' }, () => {
|
|
113
|
+
showPairing();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chrome-extension",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"bloby_human": "Bruno Bertapeli",
|
|
5
|
+
"bloby": "bloby-bruno",
|
|
6
|
+
"author": "newbot-official",
|
|
7
|
+
"description": "Bloby on every webpage. Browse with your AI agent — transcribe videos, compare prices, summarize articles, and more.",
|
|
8
|
+
"type": "skill",
|
|
9
|
+
"depends": [],
|
|
10
|
+
"env_keys": [],
|
|
11
|
+
"has_telemetry": false,
|
|
12
|
+
"size": "32KB",
|
|
13
|
+
"contains_binaries": false,
|
|
14
|
+
"tags": ["chrome", "extension", "browser", "productivity", "assistant"]
|
|
15
|
+
}
|