screenpipe-mcp 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui/search.html ADDED
@@ -0,0 +1,559 @@
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>screenpipe search</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ body {
15
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
16
+ background: #0a0a0a;
17
+ color: #fafafa;
18
+ padding: 16px;
19
+ min-height: 100vh;
20
+ }
21
+
22
+ .container {
23
+ max-width: 700px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .header {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 10px;
31
+ margin-bottom: 20px;
32
+ }
33
+
34
+ .header h1 {
35
+ font-size: 18px;
36
+ font-weight: 600;
37
+ }
38
+
39
+ .logo {
40
+ width: 24px;
41
+ height: 24px;
42
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
43
+ border-radius: 6px;
44
+ }
45
+
46
+ .search-box {
47
+ display: flex;
48
+ gap: 8px;
49
+ margin-bottom: 16px;
50
+ }
51
+
52
+ .search-input {
53
+ flex: 1;
54
+ padding: 12px 16px;
55
+ border: 1px solid #333;
56
+ border-radius: 8px;
57
+ background: #1a1a1a;
58
+ color: #fff;
59
+ font-size: 14px;
60
+ outline: none;
61
+ transition: border-color 0.2s;
62
+ }
63
+
64
+ .search-input:focus {
65
+ border-color: #666;
66
+ }
67
+
68
+ .search-input::placeholder {
69
+ color: #666;
70
+ }
71
+
72
+ .btn {
73
+ padding: 12px 20px;
74
+ border: none;
75
+ border-radius: 8px;
76
+ font-size: 14px;
77
+ font-weight: 500;
78
+ cursor: pointer;
79
+ transition: all 0.2s;
80
+ }
81
+
82
+ .btn-primary {
83
+ background: #fff;
84
+ color: #000;
85
+ }
86
+
87
+ .btn-primary:hover {
88
+ background: #e0e0e0;
89
+ }
90
+
91
+ .btn-secondary {
92
+ background: #1a1a1a;
93
+ color: #fff;
94
+ border: 1px solid #333;
95
+ }
96
+
97
+ .btn-secondary:hover {
98
+ background: #252525;
99
+ }
100
+
101
+ .filters {
102
+ display: flex;
103
+ gap: 8px;
104
+ margin-bottom: 20px;
105
+ flex-wrap: wrap;
106
+ }
107
+
108
+ .filter-btn {
109
+ padding: 6px 14px;
110
+ border: 1px solid #333;
111
+ border-radius: 20px;
112
+ background: transparent;
113
+ color: #888;
114
+ font-size: 13px;
115
+ cursor: pointer;
116
+ transition: all 0.2s;
117
+ }
118
+
119
+ .filter-btn:hover {
120
+ border-color: #555;
121
+ color: #fff;
122
+ }
123
+
124
+ .filter-btn.active {
125
+ background: #fff;
126
+ color: #000;
127
+ border-color: #fff;
128
+ }
129
+
130
+ .stats {
131
+ display: flex;
132
+ gap: 20px;
133
+ padding: 12px 16px;
134
+ background: #1a1a1a;
135
+ border-radius: 8px;
136
+ margin-bottom: 20px;
137
+ font-size: 13px;
138
+ }
139
+
140
+ .stat {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 6px;
144
+ }
145
+
146
+ .stat-value {
147
+ font-weight: 600;
148
+ color: #fff;
149
+ }
150
+
151
+ .stat-label {
152
+ color: #666;
153
+ }
154
+
155
+ .results {
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 12px;
159
+ }
160
+
161
+ .result-card {
162
+ background: #1a1a1a;
163
+ border: 1px solid #252525;
164
+ border-radius: 10px;
165
+ padding: 16px;
166
+ transition: border-color 0.2s;
167
+ }
168
+
169
+ .result-card:hover {
170
+ border-color: #333;
171
+ }
172
+
173
+ .result-header {
174
+ display: flex;
175
+ justify-content: space-between;
176
+ align-items: flex-start;
177
+ margin-bottom: 10px;
178
+ }
179
+
180
+ .result-type {
181
+ display: inline-flex;
182
+ align-items: center;
183
+ gap: 6px;
184
+ padding: 4px 10px;
185
+ border-radius: 4px;
186
+ font-size: 11px;
187
+ font-weight: 600;
188
+ text-transform: uppercase;
189
+ letter-spacing: 0.5px;
190
+ }
191
+
192
+ .type-ocr {
193
+ background: rgba(59, 130, 246, 0.2);
194
+ color: #60a5fa;
195
+ }
196
+
197
+ .type-audio {
198
+ background: rgba(34, 197, 94, 0.2);
199
+ color: #4ade80;
200
+ }
201
+
202
+ .type-ui {
203
+ background: rgba(168, 85, 247, 0.2);
204
+ color: #c084fc;
205
+ }
206
+
207
+ .result-time {
208
+ font-size: 12px;
209
+ color: #666;
210
+ }
211
+
212
+ .result-app {
213
+ font-size: 13px;
214
+ color: #888;
215
+ margin-bottom: 8px;
216
+ }
217
+
218
+ .result-text {
219
+ font-size: 14px;
220
+ line-height: 1.5;
221
+ color: #ccc;
222
+ }
223
+
224
+ .result-actions {
225
+ display: flex;
226
+ gap: 8px;
227
+ margin-top: 12px;
228
+ padding-top: 12px;
229
+ border-top: 1px solid #252525;
230
+ }
231
+
232
+ .action-btn {
233
+ padding: 6px 12px;
234
+ border: 1px solid #333;
235
+ border-radius: 6px;
236
+ background: transparent;
237
+ color: #888;
238
+ font-size: 12px;
239
+ cursor: pointer;
240
+ transition: all 0.2s;
241
+ }
242
+
243
+ .action-btn:hover {
244
+ border-color: #555;
245
+ color: #fff;
246
+ }
247
+
248
+ .empty-state {
249
+ text-align: center;
250
+ padding: 60px 20px;
251
+ color: #666;
252
+ }
253
+
254
+ .empty-state h3 {
255
+ font-size: 16px;
256
+ margin-bottom: 8px;
257
+ color: #888;
258
+ }
259
+
260
+ .loading {
261
+ display: flex;
262
+ justify-content: center;
263
+ padding: 40px;
264
+ }
265
+
266
+ .spinner {
267
+ width: 24px;
268
+ height: 24px;
269
+ border: 2px solid #333;
270
+ border-top-color: #fff;
271
+ border-radius: 50%;
272
+ animation: spin 0.8s linear infinite;
273
+ }
274
+
275
+ @keyframes spin {
276
+ to { transform: rotate(360deg); }
277
+ }
278
+
279
+ .hidden {
280
+ display: none;
281
+ }
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <div class="container">
286
+ <div class="header">
287
+ <div class="logo"></div>
288
+ <h1>screenpipe search</h1>
289
+ </div>
290
+
291
+ <div class="search-box">
292
+ <input
293
+ type="text"
294
+ id="query"
295
+ class="search-input"
296
+ placeholder="search your screen history, audio, and more..."
297
+ onkeydown="if(event.key==='Enter')search()"
298
+ />
299
+ <button class="btn btn-primary" onclick="search()">search</button>
300
+ </div>
301
+
302
+ <div class="filters">
303
+ <button class="filter-btn active" data-type="all" onclick="setFilter('all')">all</button>
304
+ <button class="filter-btn" data-type="ocr" onclick="setFilter('ocr')">screen</button>
305
+ <button class="filter-btn" data-type="audio" onclick="setFilter('audio')">audio</button>
306
+ <button class="filter-btn" data-type="ui" onclick="setFilter('ui')">ui elements</button>
307
+ </div>
308
+
309
+ <div id="stats" class="stats hidden">
310
+ <div class="stat">
311
+ <span class="stat-value" id="result-count">0</span>
312
+ <span class="stat-label">results</span>
313
+ </div>
314
+ <div class="stat">
315
+ <span class="stat-value" id="time-range">-</span>
316
+ <span class="stat-label">time range</span>
317
+ </div>
318
+ </div>
319
+
320
+ <div id="loading" class="loading hidden">
321
+ <div class="spinner"></div>
322
+ </div>
323
+
324
+ <div id="results" class="results">
325
+ <div class="empty-state">
326
+ <h3>search your digital history</h3>
327
+ <p>type a query above to search through your screen recordings and audio transcriptions</p>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <script>
333
+ let currentFilter = 'all';
334
+ let pendingRequestId = null;
335
+
336
+ // MCP App communication layer
337
+ const mcp = {
338
+ requestId: 0,
339
+
340
+ callTool: function(name, args) {
341
+ this.requestId++;
342
+ pendingRequestId = this.requestId;
343
+ window.parent.postMessage({
344
+ jsonrpc: '2.0',
345
+ id: this.requestId,
346
+ method: 'tools/call',
347
+ params: { name, arguments: args }
348
+ }, '*');
349
+ return this.requestId;
350
+ },
351
+
352
+ sendMessage: function(text) {
353
+ window.parent.postMessage({
354
+ jsonrpc: '2.0',
355
+ method: 'message/send',
356
+ params: { content: text }
357
+ }, '*');
358
+ }
359
+ };
360
+
361
+ // listen for messages from host
362
+ window.addEventListener('message', (event) => {
363
+ const data = event.data;
364
+
365
+ // handle tool results
366
+ if (data?.result || data?.method === 'tool/result') {
367
+ hideLoading();
368
+ const result = data.result || data.params?.result;
369
+ if (result) {
370
+ displayResults(result);
371
+ }
372
+ }
373
+
374
+ // handle errors
375
+ if (data?.error) {
376
+ hideLoading();
377
+ showError(data.error.message || 'search failed');
378
+ }
379
+ });
380
+
381
+ function setFilter(type) {
382
+ currentFilter = type;
383
+ document.querySelectorAll('.filter-btn').forEach(btn => {
384
+ btn.classList.toggle('active', btn.dataset.type === type);
385
+ });
386
+ // re-run search with new filter if there's a query
387
+ const query = document.getElementById('query').value;
388
+ if (query) {
389
+ search();
390
+ }
391
+ }
392
+
393
+ function search() {
394
+ const query = document.getElementById('query').value;
395
+ showLoading();
396
+
397
+ const args = {
398
+ q: query || undefined,
399
+ content_type: currentFilter,
400
+ limit: 20
401
+ };
402
+
403
+ mcp.callTool('search-content', args);
404
+ }
405
+
406
+ function showLoading() {
407
+ document.getElementById('loading').classList.remove('hidden');
408
+ document.getElementById('results').innerHTML = '';
409
+ document.getElementById('stats').classList.add('hidden');
410
+ }
411
+
412
+ function hideLoading() {
413
+ document.getElementById('loading').classList.add('hidden');
414
+ }
415
+
416
+ function showError(message) {
417
+ document.getElementById('results').innerHTML = `
418
+ <div class="empty-state">
419
+ <h3>error</h3>
420
+ <p>${escapeHtml(message)}</p>
421
+ </div>
422
+ `;
423
+ }
424
+
425
+ function displayResults(data) {
426
+ const results = data.data || data || [];
427
+ const container = document.getElementById('results');
428
+ const statsEl = document.getElementById('stats');
429
+
430
+ if (!Array.isArray(results) || results.length === 0) {
431
+ container.innerHTML = `
432
+ <div class="empty-state">
433
+ <h3>no results found</h3>
434
+ <p>try a different search term or adjust your filters</p>
435
+ </div>
436
+ `;
437
+ statsEl.classList.add('hidden');
438
+ return;
439
+ }
440
+
441
+ // update stats
442
+ document.getElementById('result-count').textContent = results.length;
443
+
444
+ // calculate time range
445
+ const timestamps = results
446
+ .map(r => r.content?.timestamp)
447
+ .filter(Boolean)
448
+ .map(t => new Date(t).getTime())
449
+ .sort((a, b) => a - b);
450
+
451
+ if (timestamps.length > 1) {
452
+ const start = new Date(timestamps[0]);
453
+ const end = new Date(timestamps[timestamps.length - 1]);
454
+ document.getElementById('time-range').textContent = formatTimeRange(start, end);
455
+ } else {
456
+ document.getElementById('time-range').textContent = 'now';
457
+ }
458
+
459
+ statsEl.classList.remove('hidden');
460
+
461
+ // render results
462
+ container.innerHTML = results.map(result => {
463
+ const content = result.content || {};
464
+ const type = result.type?.toLowerCase() || 'unknown';
465
+ const text = content.text || content.transcription || '';
466
+ const app = content.app_name || content.device_name || 'unknown';
467
+ const window = content.window_name || '';
468
+ const time = content.timestamp ? formatTime(new Date(content.timestamp)) : '';
469
+
470
+ return `
471
+ <div class="result-card">
472
+ <div class="result-header">
473
+ <span class="result-type type-${type}">${getTypeIcon(type)} ${type}</span>
474
+ <span class="result-time">${time}</span>
475
+ </div>
476
+ <div class="result-app">${escapeHtml(app)}${window ? ' - ' + escapeHtml(window) : ''}</div>
477
+ <div class="result-text">${escapeHtml(truncate(text, 300))}</div>
478
+ <div class="result-actions">
479
+ <button class="action-btn" onclick="copyText('${escapeJs(text)}')">copy</button>
480
+ <button class="action-btn" onclick="askAbout('${escapeJs(text.substring(0, 100))}')">ask AI</button>
481
+ </div>
482
+ </div>
483
+ `;
484
+ }).join('');
485
+ }
486
+
487
+ function getTypeIcon(type) {
488
+ switch(type) {
489
+ case 'ocr': return '';
490
+ case 'audio': return '';
491
+ case 'ui': return '';
492
+ default: return '';
493
+ }
494
+ }
495
+
496
+ function formatTime(date) {
497
+ const now = new Date();
498
+ const diff = now - date;
499
+
500
+ if (diff < 60000) return 'just now';
501
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
502
+ if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
503
+
504
+ return date.toLocaleDateString('en-US', {
505
+ month: 'short',
506
+ day: 'numeric',
507
+ hour: 'numeric',
508
+ minute: '2-digit'
509
+ });
510
+ }
511
+
512
+ function formatTimeRange(start, end) {
513
+ const diff = end - start;
514
+ if (diff < 3600000) return Math.floor(diff / 60000) + ' min';
515
+ if (diff < 86400000) return Math.floor(diff / 3600000) + ' hours';
516
+ return Math.floor(diff / 86400000) + ' days';
517
+ }
518
+
519
+ function truncate(str, len) {
520
+ if (!str) return '';
521
+ if (str.length <= len) return str;
522
+ return str.substring(0, len) + '...';
523
+ }
524
+
525
+ function escapeHtml(str) {
526
+ if (!str) return '';
527
+ return str
528
+ .replace(/&/g, '&amp;')
529
+ .replace(/</g, '&lt;')
530
+ .replace(/>/g, '&gt;')
531
+ .replace(/"/g, '&quot;')
532
+ .replace(/'/g, '&#039;');
533
+ }
534
+
535
+ function escapeJs(str) {
536
+ if (!str) return '';
537
+ return str
538
+ .replace(/\\/g, '\\\\')
539
+ .replace(/'/g, "\\'")
540
+ .replace(/"/g, '\\"')
541
+ .replace(/\n/g, '\\n')
542
+ .replace(/\r/g, '\\r');
543
+ }
544
+
545
+ function copyText(text) {
546
+ navigator.clipboard.writeText(text).then(() => {
547
+ // could show a toast here
548
+ });
549
+ }
550
+
551
+ function askAbout(text) {
552
+ mcp.sendMessage(`Tell me more about: "${text}"`);
553
+ }
554
+
555
+ // auto-focus search input
556
+ document.getElementById('query').focus();
557
+ </script>
558
+ </body>
559
+ </html>