ccphoto 0.2.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.
@@ -0,0 +1,1554 @@
1
+ export function renderMobilePage(token) {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
+ <meta name="theme-color" content="#111111">
10
+ <title>CCPhoto</title>
11
+ <style>
12
+ *, *::before, *::after {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ html, body {
19
+ height: 100%;
20
+ width: 100%;
21
+ overflow: hidden;
22
+ }
23
+
24
+ body {
25
+ background: #111;
26
+ color: #fff;
27
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
28
+ display: flex;
29
+ flex-direction: column;
30
+ align-items: center;
31
+ justify-content: center;
32
+ min-height: 100vh;
33
+ min-height: -webkit-fill-available;
34
+ -webkit-tap-highlight-color: transparent;
35
+ -webkit-text-size-adjust: 100%;
36
+ }
37
+
38
+ .app-title {
39
+ position: absolute;
40
+ top: 48px;
41
+ font-size: 14px;
42
+ font-weight: 500;
43
+ letter-spacing: 2px;
44
+ text-transform: uppercase;
45
+ color: #555;
46
+ }
47
+
48
+ /* Mode toggle pill */
49
+ .mode-toggle {
50
+ position: absolute;
51
+ top: 80px;
52
+ display: flex;
53
+ background: #222;
54
+ border-radius: 24px;
55
+ padding: 3px;
56
+ gap: 0;
57
+ }
58
+
59
+ .mode-btn {
60
+ padding: 8px 24px;
61
+ border: none;
62
+ border-radius: 21px;
63
+ background: transparent;
64
+ color: #888;
65
+ font-size: 14px;
66
+ font-weight: 600;
67
+ font-family: inherit;
68
+ cursor: pointer;
69
+ touch-action: manipulation;
70
+ -webkit-appearance: none;
71
+ transition: background 0.2s, color 0.2s;
72
+ min-height: 48px;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ }
77
+
78
+ .mode-btn.active {
79
+ background: #2563eb;
80
+ color: #fff;
81
+ }
82
+
83
+ .main {
84
+ display: flex;
85
+ flex-direction: column;
86
+ align-items: center;
87
+ gap: 24px;
88
+ }
89
+
90
+ #file-input {
91
+ display: none;
92
+ }
93
+
94
+ #take-photo-btn {
95
+ width: 280px;
96
+ height: 72px;
97
+ border: none;
98
+ border-radius: 16px;
99
+ background: #2563eb;
100
+ color: #fff;
101
+ font-size: 20px;
102
+ font-weight: 700;
103
+ font-family: inherit;
104
+ cursor: pointer;
105
+ min-height: 48px;
106
+ touch-action: manipulation;
107
+ -webkit-appearance: none;
108
+ transition: background 0.15s ease;
109
+ }
110
+
111
+ #take-photo-btn:active {
112
+ background: #1d4ed8;
113
+ }
114
+
115
+ .uploading {
116
+ display: flex;
117
+ flex-direction: column;
118
+ align-items: center;
119
+ gap: 16px;
120
+ }
121
+
122
+ .uploading-text {
123
+ font-size: 18px;
124
+ font-weight: 600;
125
+ color: #aaa;
126
+ }
127
+
128
+ .spinner {
129
+ width: 48px;
130
+ height: 48px;
131
+ border: 4px solid #333;
132
+ border-top-color: #2563eb;
133
+ border-radius: 50%;
134
+ animation: spin 0.8s linear infinite;
135
+ }
136
+
137
+ @keyframes spin {
138
+ to { transform: rotate(360deg); }
139
+ }
140
+
141
+ .success {
142
+ display: flex;
143
+ flex-direction: column;
144
+ align-items: center;
145
+ gap: 16px;
146
+ }
147
+
148
+ .checkmark {
149
+ font-size: 56px;
150
+ line-height: 1;
151
+ color: #22c55e;
152
+ }
153
+
154
+ .thumbnail {
155
+ width: 120px;
156
+ height: 120px;
157
+ border-radius: 12px;
158
+ object-fit: cover;
159
+ border: 2px solid #333;
160
+ }
161
+
162
+ .annotate-resend-btn {
163
+ padding: 10px 24px;
164
+ border: 1px solid #2563eb;
165
+ border-radius: 12px;
166
+ background: transparent;
167
+ color: #60a5fa;
168
+ font-size: 14px;
169
+ font-weight: 600;
170
+ font-family: inherit;
171
+ cursor: pointer;
172
+ touch-action: manipulation;
173
+ -webkit-appearance: none;
174
+ min-height: 48px;
175
+ }
176
+
177
+ .annotate-resend-btn:active {
178
+ background: rgba(37, 99, 235, 0.15);
179
+ }
180
+
181
+ .error-msg {
182
+ font-size: 16px;
183
+ font-weight: 600;
184
+ color: #ef4444;
185
+ text-align: center;
186
+ padding: 0 24px;
187
+ }
188
+
189
+ /* Status bar at bottom */
190
+ .status-bar {
191
+ position: fixed;
192
+ bottom: 0;
193
+ left: 0;
194
+ right: 0;
195
+ height: 44px;
196
+ background: #1a1a1a;
197
+ border-top: 1px solid #333;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: space-between;
201
+ padding: 0 16px;
202
+ z-index: 50;
203
+ }
204
+
205
+ .photo-count {
206
+ font-size: 14px;
207
+ color: #555;
208
+ font-weight: 500;
209
+ }
210
+
211
+ .photo-count.active {
212
+ color: #888;
213
+ }
214
+
215
+ .history-btn {
216
+ border: none;
217
+ background: transparent;
218
+ color: #60a5fa;
219
+ font-size: 14px;
220
+ font-weight: 600;
221
+ font-family: inherit;
222
+ cursor: pointer;
223
+ touch-action: manipulation;
224
+ -webkit-appearance: none;
225
+ padding: 8px 12px;
226
+ min-height: 48px;
227
+ display: flex;
228
+ align-items: center;
229
+ }
230
+
231
+ @keyframes pulse {
232
+ 0%, 100% { background: #2563eb; }
233
+ 50% { background: #60a5fa; }
234
+ }
235
+
236
+ #take-photo-btn.requesting {
237
+ animation: pulse 1s ease-in-out infinite;
238
+ }
239
+
240
+ .request-notice {
241
+ font-size: 16px;
242
+ font-weight: 600;
243
+ color: #60a5fa;
244
+ margin-bottom: 8px;
245
+ }
246
+
247
+ /* Toast container */
248
+ .toast-container {
249
+ position: fixed;
250
+ top: 0;
251
+ left: 0;
252
+ right: 0;
253
+ z-index: 200;
254
+ display: flex;
255
+ flex-direction: column;
256
+ align-items: center;
257
+ padding: 12px 16px;
258
+ gap: 8px;
259
+ pointer-events: none;
260
+ }
261
+
262
+ .toast {
263
+ width: 100%;
264
+ max-width: 400px;
265
+ background: rgba(30, 30, 30, 0.9);
266
+ -webkit-backdrop-filter: blur(12px);
267
+ backdrop-filter: blur(12px);
268
+ border: 1px solid #333;
269
+ border-radius: 12px;
270
+ padding: 12px 40px 12px 16px;
271
+ color: #fff;
272
+ font-size: 14px;
273
+ line-height: 1.5;
274
+ pointer-events: auto;
275
+ animation: toastSlideIn 0.3s ease-out;
276
+ position: relative;
277
+ word-wrap: break-word;
278
+ }
279
+
280
+ @keyframes toastSlideIn {
281
+ from { transform: translateY(-100%); opacity: 0; }
282
+ to { transform: translateY(0); opacity: 1; }
283
+ }
284
+
285
+ .toast-dismiss {
286
+ position: absolute;
287
+ top: 8px;
288
+ right: 8px;
289
+ width: 24px;
290
+ height: 24px;
291
+ border: none;
292
+ background: rgba(255,255,255,0.15);
293
+ color: #aaa;
294
+ border-radius: 50%;
295
+ font-size: 12px;
296
+ cursor: pointer;
297
+ touch-action: manipulation;
298
+ -webkit-appearance: none;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ line-height: 1;
303
+ }
304
+
305
+ .toast-truncated {
306
+ color: #60a5fa;
307
+ font-size: 12px;
308
+ margin-top: 4px;
309
+ cursor: pointer;
310
+ }
311
+
312
+ .toast-image {
313
+ width: 48px;
314
+ height: 48px;
315
+ border-radius: 6px;
316
+ object-fit: cover;
317
+ margin-top: 8px;
318
+ }
319
+
320
+ .toast .content-text h1 { font-size: 18px; margin: 4px 0; color: #fff; }
321
+ .toast .content-text h2 { font-size: 16px; margin: 4px 0; color: #fff; }
322
+ .toast .content-text h3 { font-size: 14px; margin: 2px 0; color: #fff; }
323
+ .toast .content-text pre { background: #000; padding: 6px; border-radius: 4px; overflow-x: auto; margin: 4px 0; font-size: 12px; }
324
+ .toast .content-text code { background: #000; padding: 1px 3px; border-radius: 2px; font-size: 12px; }
325
+ .toast .content-text pre code { padding: 0; background: none; }
326
+ .toast .content-text ul { padding-left: 16px; margin: 2px 0; }
327
+ .toast .content-text strong { color: #fff; }
328
+
329
+ /* History panel */
330
+ .history-panel {
331
+ position: fixed;
332
+ inset: 0;
333
+ z-index: 150;
334
+ background: #111;
335
+ display: flex;
336
+ flex-direction: column;
337
+ }
338
+
339
+ .history-header {
340
+ height: 56px;
341
+ background: #1a1a1a;
342
+ border-bottom: 1px solid #333;
343
+ display: flex;
344
+ align-items: center;
345
+ justify-content: space-between;
346
+ padding: 0 16px;
347
+ flex-shrink: 0;
348
+ }
349
+
350
+ .history-title {
351
+ font-size: 16px;
352
+ font-weight: 600;
353
+ color: #fff;
354
+ }
355
+
356
+ .history-close-btn {
357
+ border: none;
358
+ background: transparent;
359
+ color: #60a5fa;
360
+ font-size: 14px;
361
+ font-weight: 600;
362
+ font-family: inherit;
363
+ cursor: pointer;
364
+ touch-action: manipulation;
365
+ -webkit-appearance: none;
366
+ padding: 8px 12px;
367
+ min-height: 48px;
368
+ display: flex;
369
+ align-items: center;
370
+ }
371
+
372
+ .history-body {
373
+ flex: 1;
374
+ overflow-y: auto;
375
+ -webkit-overflow-scrolling: touch;
376
+ }
377
+
378
+ .history-empty {
379
+ text-align: center;
380
+ color: #555;
381
+ padding: 48px 16px;
382
+ font-size: 14px;
383
+ }
384
+
385
+ .content-message {
386
+ padding: 12px 16px;
387
+ border-bottom: 1px solid #222;
388
+ }
389
+
390
+ .content-label {
391
+ font-size: 11px;
392
+ color: #555;
393
+ text-transform: uppercase;
394
+ letter-spacing: 1px;
395
+ margin-bottom: 6px;
396
+ }
397
+
398
+ .content-text {
399
+ font-size: 14px;
400
+ line-height: 1.5;
401
+ color: #ddd;
402
+ word-wrap: break-word;
403
+ }
404
+
405
+ .content-text h1 { font-size: 20px; margin: 8px 0; color: #fff; }
406
+ .content-text h2 { font-size: 17px; margin: 6px 0; color: #fff; }
407
+ .content-text h3 { font-size: 15px; margin: 4px 0; color: #fff; }
408
+ .content-text pre { background: #000; padding: 8px; border-radius: 6px; overflow-x: auto; margin: 6px 0; }
409
+ .content-text code { background: #000; padding: 1px 4px; border-radius: 3px; font-size: 13px; }
410
+ .content-text pre code { padding: 0; background: none; }
411
+ .content-text ul { padding-left: 20px; margin: 4px 0; }
412
+ .content-text strong { color: #fff; }
413
+
414
+ .content-image {
415
+ max-width: 100%;
416
+ border-radius: 8px;
417
+ margin-top: 8px;
418
+ }
419
+
420
+ /* Annotation overlay */
421
+ .annotation-screen {
422
+ position: fixed;
423
+ inset: 0;
424
+ z-index: 100;
425
+ background: #000;
426
+ display: flex;
427
+ flex-direction: column;
428
+ }
429
+
430
+ .annotation-canvas {
431
+ flex: 1;
432
+ touch-action: none;
433
+ cursor: crosshair;
434
+ }
435
+
436
+ .annotation-toolbar {
437
+ height: 60px;
438
+ background: #1a1a1a;
439
+ display: flex;
440
+ align-items: center;
441
+ justify-content: center;
442
+ gap: 8px;
443
+ padding: 0 12px;
444
+ flex-shrink: 0;
445
+ }
446
+
447
+ .color-btn {
448
+ width: 32px;
449
+ height: 32px;
450
+ border-radius: 50%;
451
+ border: 2px solid #444;
452
+ cursor: pointer;
453
+ touch-action: manipulation;
454
+ -webkit-appearance: none;
455
+ padding: 0;
456
+ }
457
+
458
+ .color-btn.active {
459
+ border-color: #fff;
460
+ box-shadow: 0 0 0 2px #2563eb;
461
+ }
462
+
463
+ .size-btn, .tool-btn {
464
+ height: 32px;
465
+ padding: 0 10px;
466
+ border: 1px solid #444;
467
+ border-radius: 8px;
468
+ background: #222;
469
+ color: #ddd;
470
+ font-size: 12px;
471
+ font-family: inherit;
472
+ cursor: pointer;
473
+ touch-action: manipulation;
474
+ -webkit-appearance: none;
475
+ }
476
+
477
+ .tool-btn.primary {
478
+ background: #2563eb;
479
+ border-color: #2563eb;
480
+ color: #fff;
481
+ font-weight: 600;
482
+ }
483
+
484
+ .toolbar-sep {
485
+ width: 1px;
486
+ height: 24px;
487
+ background: #333;
488
+ }
489
+
490
+ /* Live mode */
491
+ .live-container {
492
+ position: relative;
493
+ width: 100%;
494
+ display: flex;
495
+ flex-direction: column;
496
+ align-items: center;
497
+ justify-content: center;
498
+ gap: 12px;
499
+ }
500
+
501
+ .live-video {
502
+ width: 100%;
503
+ max-width: 640px;
504
+ max-height: 60vh;
505
+ object-fit: contain;
506
+ border-radius: 12px;
507
+ background: #000;
508
+ }
509
+
510
+ .live-badge {
511
+ position: absolute;
512
+ top: 12px;
513
+ left: 12px;
514
+ background: #ef4444;
515
+ color: #fff;
516
+ padding: 4px 12px;
517
+ border-radius: 12px;
518
+ font-size: 12px;
519
+ font-weight: 700;
520
+ letter-spacing: 1px;
521
+ text-transform: uppercase;
522
+ display: flex;
523
+ align-items: center;
524
+ gap: 6px;
525
+ }
526
+
527
+ .live-dot {
528
+ width: 8px;
529
+ height: 8px;
530
+ border-radius: 50%;
531
+ background: #fff;
532
+ animation: livePulse 1.5s ease-in-out infinite;
533
+ }
534
+
535
+ @keyframes livePulse {
536
+ 0%, 100% { opacity: 1; }
537
+ 50% { opacity: 0.3; }
538
+ }
539
+
540
+ .streaming-indicator {
541
+ position: absolute;
542
+ top: 12px;
543
+ right: 12px;
544
+ background: rgba(0,0,0,0.6);
545
+ color: #aaa;
546
+ padding: 4px 10px;
547
+ border-radius: 6px;
548
+ font-size: 12px;
549
+ display: flex;
550
+ align-items: center;
551
+ gap: 4px;
552
+ }
553
+
554
+ .streaming-dots {
555
+ display: inline-flex;
556
+ gap: 3px;
557
+ }
558
+
559
+ .streaming-dots span {
560
+ width: 4px;
561
+ height: 4px;
562
+ border-radius: 50%;
563
+ background: #60a5fa;
564
+ animation: streamDot 1.4s ease-in-out infinite;
565
+ }
566
+
567
+ .streaming-dots span:nth-child(2) {
568
+ animation-delay: 0.2s;
569
+ }
570
+
571
+ .streaming-dots span:nth-child(3) {
572
+ animation-delay: 0.4s;
573
+ }
574
+
575
+ @keyframes streamDot {
576
+ 0%, 80%, 100% { opacity: 0.3; }
577
+ 40% { opacity: 1; }
578
+ }
579
+
580
+ .live-status {
581
+ font-size: 14px;
582
+ color: #888;
583
+ }
584
+
585
+ .stop-btn {
586
+ width: 200px;
587
+ height: 48px;
588
+ border: 2px solid #ef4444;
589
+ border-radius: 12px;
590
+ background: transparent;
591
+ color: #ef4444;
592
+ font-size: 16px;
593
+ font-weight: 600;
594
+ font-family: inherit;
595
+ cursor: pointer;
596
+ touch-action: manipulation;
597
+ -webkit-appearance: none;
598
+ min-height: 48px;
599
+ }
600
+
601
+ .stop-btn:active {
602
+ background: rgba(239, 68, 68, 0.15);
603
+ }
604
+
605
+ .voice-btn {
606
+ position: fixed;
607
+ bottom: 56px;
608
+ right: 16px;
609
+ width: 56px;
610
+ height: 56px;
611
+ border-radius: 50%;
612
+ border: none;
613
+ background: #333;
614
+ color: #aaa;
615
+ font-size: 24px;
616
+ cursor: pointer;
617
+ touch-action: manipulation;
618
+ -webkit-appearance: none;
619
+ z-index: 120;
620
+ display: flex;
621
+ align-items: center;
622
+ justify-content: center;
623
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
624
+ transition: background 0.2s, transform 0.1s;
625
+ }
626
+
627
+ .voice-btn:active {
628
+ transform: scale(0.95);
629
+ }
630
+
631
+ .voice-btn.listening {
632
+ background: #ef4444;
633
+ color: #fff;
634
+ animation: voicePulse 1.2s ease-in-out infinite;
635
+ }
636
+
637
+ .voice-btn.processing {
638
+ background: #f59e0b;
639
+ color: #fff;
640
+ }
641
+
642
+ .voice-btn.hidden {
643
+ display: none;
644
+ }
645
+
646
+ @keyframes voicePulse {
647
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5); }
648
+ 50% { box-shadow: 0 0 0 12px rgba(239, 68, 68, 0); }
649
+ }
650
+
651
+ .voice-status {
652
+ position: fixed;
653
+ bottom: 120px;
654
+ right: 16px;
655
+ background: rgba(30, 30, 30, 0.9);
656
+ -webkit-backdrop-filter: blur(8px);
657
+ backdrop-filter: blur(8px);
658
+ border: 1px solid #333;
659
+ border-radius: 8px;
660
+ padding: 6px 12px;
661
+ color: #ddd;
662
+ font-size: 12px;
663
+ z-index: 120;
664
+ max-width: 200px;
665
+ text-align: center;
666
+ pointer-events: none;
667
+ }
668
+ </style>
669
+ </head>
670
+ <body>
671
+ <div class="app-title">CCPhoto</div>
672
+
673
+ <div class="mode-toggle">
674
+ <button class="mode-btn active" id="mode-photo-btn" type="button">Photo</button>
675
+ <button class="mode-btn" id="mode-live-btn" type="button">Live</button>
676
+ </div>
677
+
678
+ <!-- Photo view (default) -->
679
+ <div class="main" id="photo-view">
680
+ <div id="main-area">
681
+ <button id="take-photo-btn" type="button">Take Photo</button>
682
+ </div>
683
+ </div>
684
+ <input type="file" id="file-input" accept="image/*" capture="environment">
685
+
686
+ <!-- Live view (hidden by default) -->
687
+ <div class="main" id="live-view" style="display:none;">
688
+ <div class="live-container" id="live-container">
689
+ <video id="live-video" class="live-video" playsinline muted autoplay></video>
690
+ <div class="live-badge"><div class="live-dot"></div> LIVE</div>
691
+ <div class="streaming-indicator" id="streaming-indicator" style="display:none;">
692
+ Streaming<div class="streaming-dots"><span></span><span></span><span></span></div>
693
+ </div>
694
+ <canvas id="frame-canvas" style="display:none;"></canvas>
695
+ </div>
696
+ <div class="live-status" id="live-status">Starting camera...</div>
697
+ <button class="stop-btn" id="stop-btn" type="button">Stop Streaming</button>
698
+ </div>
699
+
700
+ <!-- Toast container -->
701
+ <div class="toast-container" id="toast-container"></div>
702
+
703
+ <button class="voice-btn hidden" id="voice-btn" type="button" aria-label="Voice message">
704
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
705
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
706
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
707
+ <line x1="12" y1="19" x2="12" y2="23"/>
708
+ <line x1="8" y1="23" x2="16" y2="23"/>
709
+ </svg>
710
+ </button>
711
+ <div class="voice-status" id="voice-status" style="display:none;"></div>
712
+
713
+ <!-- Status bar -->
714
+ <div class="status-bar">
715
+ <div class="photo-count" id="photo-count"></div>
716
+ <button class="history-btn" id="history-btn" type="button">History</button>
717
+ </div>
718
+
719
+ <script>
720
+ (function() {
721
+ var token = ${JSON.stringify(token)};
722
+ var origin = window.location.origin;
723
+ var uploadUrl = origin + '/upload';
724
+ var frameUrl = origin + '/frame';
725
+ var evtUrl = origin + '/events';
726
+ var photoCount = 0;
727
+ var busy = false;
728
+ var currentMode = 'photo';
729
+ var liveStream = null;
730
+ var frameInterval = null;
731
+ var messageHistory = [];
732
+ var autoResetTimer = null;
733
+ var lastUploadedDataUrl = null;
734
+ var lastUploadedFile = null;
735
+
736
+ var mainArea = document.getElementById('main-area');
737
+ var fileInput = document.getElementById('file-input');
738
+ var photoCountEl = document.getElementById('photo-count');
739
+ var photoView = document.getElementById('photo-view');
740
+ var liveView = document.getElementById('live-view');
741
+ var modePhotoBtn = document.getElementById('mode-photo-btn');
742
+ var modeLiveBtn = document.getElementById('mode-live-btn');
743
+ var toastContainer = document.getElementById('toast-container');
744
+ var historyBtn = document.getElementById('history-btn');
745
+ var stopBtn = document.getElementById('stop-btn');
746
+ var liveVideo = document.getElementById('live-video');
747
+ var frameCanvas = document.getElementById('frame-canvas');
748
+ var frameCtx = frameCanvas.getContext('2d');
749
+ var streamingIndicator = document.getElementById('streaming-indicator');
750
+ var liveStatus = document.getElementById('live-status');
751
+
752
+ // --- Mode toggle ---
753
+ modePhotoBtn.addEventListener('click', function() {
754
+ if (currentMode === 'photo') return;
755
+ switchToPhoto();
756
+ });
757
+
758
+ modeLiveBtn.addEventListener('click', function() {
759
+ if (currentMode === 'live') return;
760
+ switchToLive();
761
+ });
762
+
763
+ function switchToPhoto() {
764
+ currentMode = 'photo';
765
+ modePhotoBtn.classList.add('active');
766
+ modeLiveBtn.classList.remove('active');
767
+ photoView.style.display = '';
768
+ liveView.style.display = 'none';
769
+ stopLiveStream();
770
+ }
771
+
772
+ function switchToLive() {
773
+ currentMode = 'live';
774
+ modeLiveBtn.classList.add('active');
775
+ modePhotoBtn.classList.remove('active');
776
+ photoView.style.display = 'none';
777
+ liveView.style.display = '';
778
+ startLiveStream();
779
+ // Notify server so Claude knows user switched to live mode
780
+ fetch(origin + '/mode-switch?token=' + encodeURIComponent(token), {
781
+ method: 'POST',
782
+ headers: { 'Content-Type': 'application/json' },
783
+ body: JSON.stringify({ action: 'start_livestream' })
784
+ }).catch(function() {});
785
+ }
786
+
787
+ // --- Photo mode ---
788
+ function bindTakePhotoBtn() {
789
+ var btn = document.getElementById('take-photo-btn');
790
+ if (btn) {
791
+ btn.addEventListener('click', function() {
792
+ if (!busy) {
793
+ clearRequestState();
794
+ fileInput.value = '';
795
+ fileInput.click();
796
+ }
797
+ });
798
+ }
799
+ }
800
+ bindTakePhotoBtn();
801
+
802
+ fileInput.addEventListener('change', function() {
803
+ if (!fileInput.files || !fileInput.files[0]) return;
804
+ if (busy) return;
805
+ clearRequestState();
806
+ var file = fileInput.files[0];
807
+
808
+ // Read data URL for potential annotation later
809
+ var reader = new FileReader();
810
+ reader.onload = function() {
811
+ lastUploadedDataUrl = reader.result;
812
+ lastUploadedFile = file;
813
+ uploadFile(file);
814
+ };
815
+ reader.readAsDataURL(file);
816
+ });
817
+
818
+ function clearRequestState() {
819
+ var btn = document.getElementById('take-photo-btn');
820
+ if (btn) {
821
+ btn.classList.remove('requesting');
822
+ }
823
+ var notice = document.querySelector('.request-notice');
824
+ if (notice && notice.parentNode) {
825
+ notice.parentNode.removeChild(notice);
826
+ }
827
+ }
828
+
829
+ function resetToButton() {
830
+ busy = false;
831
+ lastUploadedDataUrl = null;
832
+ lastUploadedFile = null;
833
+ if (autoResetTimer) {
834
+ clearTimeout(autoResetTimer);
835
+ autoResetTimer = null;
836
+ }
837
+ mainArea.innerHTML = '<button id="take-photo-btn" type="button">Take Photo</button>';
838
+ bindTakePhotoBtn();
839
+ }
840
+
841
+ function updateCount() {
842
+ if (photoCount === 0) {
843
+ photoCountEl.textContent = '';
844
+ photoCountEl.classList.remove('active');
845
+ } else {
846
+ photoCountEl.textContent = photoCount + (photoCount === 1 ? ' photo' : ' photos') + ' sent';
847
+ photoCountEl.classList.add('active');
848
+ }
849
+ }
850
+
851
+ function escapeHtml(str) {
852
+ var div = document.createElement('div');
853
+ div.appendChild(document.createTextNode(str));
854
+ return div.innerHTML;
855
+ }
856
+
857
+ function renderBasicMarkdown(text) {
858
+ var escaped = escapeHtml(text);
859
+ var lines = escaped.split('\\n');
860
+ var result = [];
861
+ var inCode = false;
862
+ var codeLines = [];
863
+
864
+ for (var i = 0; i < lines.length; i++) {
865
+ var line = lines[i];
866
+ if (line.match(/^\\\`\\\`\\\`/)) {
867
+ if (inCode) {
868
+ result.push('<pre><code>' + codeLines.join('\\n') + '</code></pre>');
869
+ codeLines = [];
870
+ inCode = false;
871
+ } else {
872
+ inCode = true;
873
+ }
874
+ continue;
875
+ }
876
+ if (inCode) {
877
+ codeLines.push(line);
878
+ continue;
879
+ }
880
+ if (line.match(/^### /)) { result.push('<h3>' + line.slice(4) + '</h3>'); continue; }
881
+ if (line.match(/^## /)) { result.push('<h2>' + line.slice(3) + '</h2>'); continue; }
882
+ if (line.match(/^# /)) { result.push('<h1>' + line.slice(2) + '</h1>'); continue; }
883
+ if (line.match(/^- /)) { result.push('<ul><li>' + line.slice(2) + '</li></ul>'); continue; }
884
+
885
+ line = line.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
886
+ line = line.replace(/\\\`([^\\\`]+)\\\`/g, '<code>$1</code>');
887
+ result.push(line === '' ? '<br>' : line + '<br>');
888
+ }
889
+ if (inCode) {
890
+ result.push('<pre><code>' + codeLines.join('\\n') + '</code></pre>');
891
+ }
892
+ return result.join('\\n');
893
+ }
894
+
895
+ // --- Upload ---
896
+ function uploadFile(file) {
897
+ busy = true;
898
+ mainArea.innerHTML =
899
+ '<div class="uploading">' +
900
+ '<div class="spinner"></div>' +
901
+ '<div class="uploading-text">Uploading...</div>' +
902
+ '</div>';
903
+
904
+ var thumbUrl = URL.createObjectURL(file);
905
+ var formData = new FormData();
906
+ formData.append('photo', file);
907
+
908
+ var url = uploadUrl + '?token=' + encodeURIComponent(token);
909
+
910
+ fetch(url, { method: 'POST', body: formData })
911
+ .then(function(res) {
912
+ if (!res.ok) throw new Error('Upload failed (' + res.status + ')');
913
+ return res;
914
+ })
915
+ .then(function() {
916
+ photoCount++;
917
+ updateCount();
918
+ mainArea.innerHTML =
919
+ '<div class="success">' +
920
+ '<div class="checkmark">&#10003;</div>' +
921
+ '<img class="thumbnail" src="' + thumbUrl + '" alt="Uploaded photo">' +
922
+ '<button class="annotate-resend-btn" id="annotate-resend-btn" type="button">Annotate &amp; Resend</button>' +
923
+ '</div>';
924
+
925
+ var annotateBtn = document.getElementById('annotate-resend-btn');
926
+ annotateBtn.addEventListener('click', function() {
927
+ if (autoResetTimer) {
928
+ clearTimeout(autoResetTimer);
929
+ autoResetTimer = null;
930
+ }
931
+ if (lastUploadedDataUrl && lastUploadedFile) {
932
+ showAnnotationScreen(lastUploadedDataUrl, lastUploadedFile);
933
+ }
934
+ });
935
+
936
+ autoResetTimer = setTimeout(function() {
937
+ URL.revokeObjectURL(thumbUrl);
938
+ resetToButton();
939
+ }, 4000);
940
+ })
941
+ .catch(function(err) {
942
+ URL.revokeObjectURL(thumbUrl);
943
+ mainArea.innerHTML =
944
+ '<div class="error-msg">' + escapeHtml(err.message || 'Upload failed') + '</div>';
945
+ setTimeout(function() { resetToButton(); }, 3000);
946
+ });
947
+ }
948
+
949
+ // --- Toast system ---
950
+ var toastCount = 0;
951
+ var MAX_TOASTS = 3;
952
+
953
+ function showToast(data) {
954
+ // Enforce max toasts
955
+ while (toastContainer.children.length >= MAX_TOASTS) {
956
+ toastContainer.removeChild(toastContainer.firstChild);
957
+ }
958
+
959
+ var toast = document.createElement('div');
960
+ toast.className = 'toast';
961
+
962
+ // Dismiss button
963
+ var dismissBtn = document.createElement('button');
964
+ dismissBtn.className = 'toast-dismiss';
965
+ dismissBtn.textContent = 'X';
966
+ dismissBtn.addEventListener('click', function() {
967
+ removeToast(toast);
968
+ });
969
+ toast.appendChild(dismissBtn);
970
+
971
+ if (data.text) {
972
+ var textDiv = document.createElement('div');
973
+ textDiv.className = 'content-text';
974
+ var fullHtml = renderBasicMarkdown(data.text);
975
+ if (data.text.length > 150) {
976
+ var truncatedHtml = renderBasicMarkdown(data.text.substring(0, 150) + '...');
977
+ textDiv.innerHTML = truncatedHtml;
978
+ var expandHint = document.createElement('div');
979
+ expandHint.className = 'toast-truncated';
980
+ expandHint.textContent = 'tap to expand';
981
+ toast.appendChild(textDiv);
982
+ toast.appendChild(expandHint);
983
+ var expanded = false;
984
+ expandHint.addEventListener('click', function() {
985
+ if (!expanded) {
986
+ textDiv.innerHTML = fullHtml;
987
+ expandHint.style.display = 'none';
988
+ expanded = true;
989
+ }
990
+ });
991
+ } else {
992
+ textDiv.innerHTML = fullHtml;
993
+ toast.appendChild(textDiv);
994
+ }
995
+ }
996
+
997
+ if (data.imageData && data.mimeType) {
998
+ var img = document.createElement('img');
999
+ img.className = 'toast-image';
1000
+ img.src = 'data:' + data.mimeType + ';base64,' + data.imageData;
1001
+ img.alt = 'From Claude';
1002
+ toast.appendChild(img);
1003
+ }
1004
+
1005
+ toastContainer.appendChild(toast);
1006
+
1007
+ // Auto-dismiss after 6 seconds
1008
+ var dismissTimer = setTimeout(function() {
1009
+ removeToast(toast);
1010
+ }, 6000);
1011
+
1012
+ toast._dismissTimer = dismissTimer;
1013
+ }
1014
+
1015
+ function removeToast(toast) {
1016
+ if (toast._dismissTimer) clearTimeout(toast._dismissTimer);
1017
+ if (toast.parentNode) {
1018
+ toast.parentNode.removeChild(toast);
1019
+ }
1020
+ }
1021
+
1022
+ // --- History panel ---
1023
+ function showHistoryPanel() {
1024
+ var panel = document.createElement('div');
1025
+ panel.className = 'history-panel';
1026
+ panel.id = 'history-panel';
1027
+
1028
+ var header = document.createElement('div');
1029
+ header.className = 'history-header';
1030
+
1031
+ var title = document.createElement('div');
1032
+ title.className = 'history-title';
1033
+ title.textContent = 'Message History';
1034
+ header.appendChild(title);
1035
+
1036
+ var closeBtn = document.createElement('button');
1037
+ closeBtn.className = 'history-close-btn';
1038
+ closeBtn.textContent = 'Close';
1039
+ closeBtn.type = 'button';
1040
+ closeBtn.addEventListener('click', function() {
1041
+ document.body.removeChild(panel);
1042
+ });
1043
+ header.appendChild(closeBtn);
1044
+ panel.appendChild(header);
1045
+
1046
+ var body = document.createElement('div');
1047
+ body.className = 'history-body';
1048
+
1049
+ if (messageHistory.length === 0) {
1050
+ var empty = document.createElement('div');
1051
+ empty.className = 'history-empty';
1052
+ empty.textContent = 'No messages yet';
1053
+ body.appendChild(empty);
1054
+ } else {
1055
+ for (var i = messageHistory.length - 1; i >= 0; i--) {
1056
+ var item = messageHistory[i];
1057
+ var msg = document.createElement('div');
1058
+ msg.className = 'content-message';
1059
+
1060
+ var label = document.createElement('div');
1061
+ label.className = 'content-label';
1062
+ label.textContent = 'From Claude';
1063
+ msg.appendChild(label);
1064
+
1065
+ if (item.text) {
1066
+ var textDiv = document.createElement('div');
1067
+ textDiv.className = 'content-text';
1068
+ textDiv.innerHTML = renderBasicMarkdown(item.text);
1069
+ msg.appendChild(textDiv);
1070
+ }
1071
+
1072
+ if (item.imageData && item.mimeType) {
1073
+ var img = document.createElement('img');
1074
+ img.className = 'content-image';
1075
+ img.src = 'data:' + item.mimeType + ';base64,' + item.imageData;
1076
+ img.alt = 'From Claude';
1077
+ msg.appendChild(img);
1078
+ }
1079
+
1080
+ body.appendChild(msg);
1081
+ }
1082
+ }
1083
+
1084
+ panel.appendChild(body);
1085
+ document.body.appendChild(panel);
1086
+ }
1087
+
1088
+ historyBtn.addEventListener('click', function() {
1089
+ var existing = document.getElementById('history-panel');
1090
+ if (existing) {
1091
+ document.body.removeChild(existing);
1092
+ } else {
1093
+ showHistoryPanel();
1094
+ }
1095
+ });
1096
+
1097
+ // --- SSE ---
1098
+ var evtSource = new EventSource(evtUrl + '?token=' + encodeURIComponent(token));
1099
+
1100
+ evtSource.addEventListener('photo-requested', function() {
1101
+ if (navigator.vibrate) navigator.vibrate(200);
1102
+
1103
+ // Switch to photo mode if in live
1104
+ if (currentMode === 'live') {
1105
+ switchToPhoto();
1106
+ }
1107
+
1108
+ var btn = document.getElementById('take-photo-btn');
1109
+ if (btn) {
1110
+ btn.classList.add('requesting');
1111
+ var existing = document.querySelector('.request-notice');
1112
+ if (!existing) {
1113
+ var notice = document.createElement('div');
1114
+ notice.className = 'request-notice';
1115
+ notice.textContent = 'Photo requested!';
1116
+ mainArea.insertBefore(notice, mainArea.firstChild);
1117
+ }
1118
+ }
1119
+ });
1120
+
1121
+ evtSource.addEventListener('content-push', function(e) {
1122
+ var data = JSON.parse(e.data);
1123
+
1124
+ if (navigator.vibrate) navigator.vibrate(100);
1125
+
1126
+ // Store in history
1127
+ messageHistory.push(data);
1128
+
1129
+ // Show toast
1130
+ showToast(data);
1131
+
1132
+ // TTS: speak the message if requested
1133
+ if (data.speak && data.text && window.speechSynthesis) {
1134
+ speechSynthesis.cancel();
1135
+ setTimeout(function() {
1136
+ var utterance = new SpeechSynthesisUtterance(data.text);
1137
+ utterance.rate = 1.0;
1138
+ utterance.pitch = 1.0;
1139
+ speechSynthesis.speak(utterance);
1140
+ }, 50);
1141
+ }
1142
+ });
1143
+
1144
+ evtSource.addEventListener('switch-to-live', function() {
1145
+ if (navigator.vibrate) navigator.vibrate(200);
1146
+ if (currentMode !== 'live') {
1147
+ switchToLive();
1148
+ }
1149
+ });
1150
+
1151
+ // --- Live mode ---
1152
+ var FRAME_INTERVAL_MS = 3000;
1153
+
1154
+ function startLiveStream() {
1155
+ liveStatus.style.color = '#888';
1156
+ streamingIndicator.style.display = 'none';
1157
+
1158
+ if (!window.isSecureContext || !navigator.mediaDevices) {
1159
+ liveStatus.textContent = 'Live mode requires HTTPS. Say "start livestream" in Claude Code.';
1160
+ liveStatus.style.color = '#f59e0b';
1161
+ return;
1162
+ }
1163
+
1164
+ liveStatus.textContent = 'Starting camera...';
1165
+
1166
+ navigator.mediaDevices.getUserMedia({
1167
+ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } }
1168
+ })
1169
+ .then(function(stream) {
1170
+ liveStream = stream;
1171
+ liveVideo.srcObject = stream;
1172
+ liveStatus.textContent = 'Camera active -- streaming to Claude';
1173
+
1174
+ liveVideo.onloadedmetadata = function() {
1175
+ frameCanvas.width = liveVideo.videoWidth;
1176
+ frameCanvas.height = liveVideo.videoHeight;
1177
+ startFrameCapture();
1178
+ };
1179
+ })
1180
+ .catch(function(err) {
1181
+ liveStatus.textContent = 'Camera access denied: ' + err.message;
1182
+ liveStatus.style.color = '#ef4444';
1183
+ });
1184
+ }
1185
+
1186
+ function startFrameCapture() {
1187
+ streamingIndicator.style.display = '';
1188
+
1189
+ frameInterval = setInterval(function() {
1190
+ if (liveVideo.readyState < 2) return;
1191
+
1192
+ frameCtx.drawImage(liveVideo, 0, 0);
1193
+ frameCanvas.toBlob(function(blob) {
1194
+ if (!blob) return;
1195
+
1196
+ var url = frameUrl + '?token=' + encodeURIComponent(token)
1197
+ + '&w=' + frameCanvas.width
1198
+ + '&h=' + frameCanvas.height;
1199
+
1200
+ fetch(url, {
1201
+ method: 'POST',
1202
+ headers: { 'Content-Type': 'image/jpeg' },
1203
+ body: blob,
1204
+ })
1205
+ .then(function() {
1206
+ // streaming indicator handled by CSS animation
1207
+ })
1208
+ .catch(function() {
1209
+ // silent fail, indicator keeps animating
1210
+ });
1211
+ }, 'image/jpeg', 0.7);
1212
+ }, FRAME_INTERVAL_MS);
1213
+ }
1214
+
1215
+ function stopLiveStream() {
1216
+ if (frameInterval) {
1217
+ clearInterval(frameInterval);
1218
+ frameInterval = null;
1219
+ }
1220
+ if (liveStream) {
1221
+ liveStream.getTracks().forEach(function(track) {
1222
+ track.stop();
1223
+ });
1224
+ liveStream = null;
1225
+ liveVideo.srcObject = null;
1226
+ }
1227
+ streamingIndicator.style.display = 'none';
1228
+ liveStatus.textContent = 'Starting camera...';
1229
+ liveStatus.style.color = '#888';
1230
+ }
1231
+
1232
+ stopBtn.addEventListener('click', function() {
1233
+ switchToPhoto();
1234
+ });
1235
+
1236
+ // --- Annotation screen (opt-in) ---
1237
+ var annotationOverlay = null;
1238
+ var annotationCanvas = null;
1239
+ var annotationCtx = null;
1240
+ var originalImage = null;
1241
+ var originalFile = null;
1242
+ var strokes = [];
1243
+ var currentStroke = null;
1244
+ var drawColor = '#ff0000';
1245
+ var drawWidth = 3;
1246
+ var dpr = window.devicePixelRatio || 1;
1247
+
1248
+ function showAnnotationScreen(dataUrl, file) {
1249
+ originalFile = file;
1250
+
1251
+ annotationOverlay = document.createElement('div');
1252
+ annotationOverlay.className = 'annotation-screen';
1253
+
1254
+ annotationCanvas = document.createElement('canvas');
1255
+ annotationCanvas.className = 'annotation-canvas';
1256
+ annotationOverlay.appendChild(annotationCanvas);
1257
+
1258
+ // Toolbar
1259
+ var toolbar = document.createElement('div');
1260
+ toolbar.className = 'annotation-toolbar';
1261
+
1262
+ var colors = [
1263
+ { color: '#ff0000', label: 'Red' },
1264
+ { color: '#2563eb', label: 'Blue' },
1265
+ { color: '#22c55e', label: 'Green' },
1266
+ { color: '#ffffff', label: 'White' }
1267
+ ];
1268
+
1269
+ colors.forEach(function(c) {
1270
+ var btn = document.createElement('button');
1271
+ btn.className = 'color-btn' + (c.color === drawColor ? ' active' : '');
1272
+ btn.style.background = c.color;
1273
+ btn.setAttribute('data-color', c.color);
1274
+ btn.type = 'button';
1275
+ btn.addEventListener('click', function() {
1276
+ drawColor = c.color;
1277
+ toolbar.querySelectorAll('.color-btn').forEach(function(b) { b.classList.remove('active'); });
1278
+ btn.classList.add('active');
1279
+ });
1280
+ toolbar.appendChild(btn);
1281
+ });
1282
+
1283
+ var sep1 = document.createElement('div');
1284
+ sep1.className = 'toolbar-sep';
1285
+ toolbar.appendChild(sep1);
1286
+
1287
+ var sizeBtn = document.createElement('button');
1288
+ sizeBtn.className = 'size-btn';
1289
+ sizeBtn.textContent = 'Thin';
1290
+ sizeBtn.type = 'button';
1291
+ sizeBtn.addEventListener('click', function() {
1292
+ drawWidth = drawWidth === 3 ? 8 : 3;
1293
+ sizeBtn.textContent = drawWidth === 3 ? 'Thin' : 'Thick';
1294
+ });
1295
+ toolbar.appendChild(sizeBtn);
1296
+
1297
+ var undoBtn = document.createElement('button');
1298
+ undoBtn.className = 'tool-btn';
1299
+ undoBtn.textContent = 'Undo';
1300
+ undoBtn.type = 'button';
1301
+ undoBtn.addEventListener('click', function() {
1302
+ if (strokes.length > 0) {
1303
+ strokes.pop();
1304
+ redrawCanvas();
1305
+ }
1306
+ });
1307
+ toolbar.appendChild(undoBtn);
1308
+
1309
+ var clearBtn = document.createElement('button');
1310
+ clearBtn.className = 'tool-btn';
1311
+ clearBtn.textContent = 'Clear';
1312
+ clearBtn.type = 'button';
1313
+ clearBtn.addEventListener('click', function() {
1314
+ strokes = [];
1315
+ redrawCanvas();
1316
+ });
1317
+ toolbar.appendChild(clearBtn);
1318
+
1319
+ var sep2 = document.createElement('div');
1320
+ sep2.className = 'toolbar-sep';
1321
+ toolbar.appendChild(sep2);
1322
+
1323
+ var cancelBtn = document.createElement('button');
1324
+ cancelBtn.className = 'tool-btn';
1325
+ cancelBtn.textContent = 'Cancel';
1326
+ cancelBtn.type = 'button';
1327
+ cancelBtn.addEventListener('click', function() {
1328
+ closeAnnotation();
1329
+ });
1330
+ toolbar.appendChild(cancelBtn);
1331
+
1332
+ var sendBtn = document.createElement('button');
1333
+ sendBtn.className = 'tool-btn primary';
1334
+ sendBtn.textContent = 'Send';
1335
+ sendBtn.type = 'button';
1336
+ sendBtn.addEventListener('click', function() {
1337
+ annotationCanvas.toBlob(function(blob) {
1338
+ var annotatedFile = new File([blob], 'annotated.jpg', { type: 'image/jpeg' });
1339
+ uploadFile(annotatedFile);
1340
+ closeAnnotation();
1341
+ }, 'image/jpeg', 0.92);
1342
+ });
1343
+ toolbar.appendChild(sendBtn);
1344
+
1345
+ annotationOverlay.appendChild(toolbar);
1346
+ document.body.appendChild(annotationOverlay);
1347
+
1348
+ // Load image onto canvas
1349
+ originalImage = new Image();
1350
+ originalImage.onload = function() {
1351
+ var cw = annotationOverlay.clientWidth;
1352
+ var ch = annotationOverlay.clientHeight - 60;
1353
+ annotationCanvas.style.width = cw + 'px';
1354
+ annotationCanvas.style.height = ch + 'px';
1355
+ annotationCanvas.width = cw * dpr;
1356
+ annotationCanvas.height = ch * dpr;
1357
+ annotationCtx = annotationCanvas.getContext('2d');
1358
+ annotationCtx.scale(dpr, dpr);
1359
+ strokes = [];
1360
+ redrawCanvas();
1361
+ setupDrawing();
1362
+ };
1363
+ originalImage.src = dataUrl;
1364
+ }
1365
+
1366
+ function redrawCanvas() {
1367
+ var cw = annotationCanvas.width / dpr;
1368
+ var ch = annotationCanvas.height / dpr;
1369
+ annotationCtx.clearRect(0, 0, cw, ch);
1370
+
1371
+ var scale = Math.min(cw / originalImage.width, ch / originalImage.height);
1372
+ var w = originalImage.width * scale;
1373
+ var h = originalImage.height * scale;
1374
+ var x = (cw - w) / 2;
1375
+ var y = (ch - h) / 2;
1376
+ annotationCtx.drawImage(originalImage, x, y, w, h);
1377
+
1378
+ for (var i = 0; i < strokes.length; i++) {
1379
+ var s = strokes[i];
1380
+ annotationCtx.beginPath();
1381
+ annotationCtx.strokeStyle = s.color;
1382
+ annotationCtx.lineWidth = s.width;
1383
+ annotationCtx.lineCap = 'round';
1384
+ annotationCtx.lineJoin = 'round';
1385
+ for (var j = 0; j < s.points.length; j++) {
1386
+ if (j === 0) annotationCtx.moveTo(s.points[j].x, s.points[j].y);
1387
+ else annotationCtx.lineTo(s.points[j].x, s.points[j].y);
1388
+ }
1389
+ annotationCtx.stroke();
1390
+ }
1391
+ }
1392
+
1393
+ function setupDrawing() {
1394
+ function getPos(e) {
1395
+ var rect = annotationCanvas.getBoundingClientRect();
1396
+ var touch = e.touches ? e.touches[0] : e;
1397
+ return { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
1398
+ }
1399
+
1400
+ function startDraw(e) {
1401
+ e.preventDefault();
1402
+ var pos = getPos(e);
1403
+ currentStroke = { points: [pos], color: drawColor, width: drawWidth };
1404
+ }
1405
+
1406
+ function moveDraw(e) {
1407
+ if (!currentStroke) return;
1408
+ e.preventDefault();
1409
+ var pos = getPos(e);
1410
+ currentStroke.points.push(pos);
1411
+ annotationCtx.beginPath();
1412
+ annotationCtx.strokeStyle = currentStroke.color;
1413
+ annotationCtx.lineWidth = currentStroke.width;
1414
+ annotationCtx.lineCap = 'round';
1415
+ annotationCtx.lineJoin = 'round';
1416
+ var pts = currentStroke.points;
1417
+ if (pts.length >= 2) {
1418
+ annotationCtx.moveTo(pts[pts.length - 2].x, pts[pts.length - 2].y);
1419
+ annotationCtx.lineTo(pos.x, pos.y);
1420
+ }
1421
+ annotationCtx.stroke();
1422
+ }
1423
+
1424
+ function endDraw() {
1425
+ if (currentStroke && currentStroke.points.length > 1) {
1426
+ strokes.push(currentStroke);
1427
+ }
1428
+ currentStroke = null;
1429
+ }
1430
+
1431
+ annotationCanvas.addEventListener('touchstart', startDraw, { passive: false });
1432
+ annotationCanvas.addEventListener('touchmove', moveDraw, { passive: false });
1433
+ annotationCanvas.addEventListener('touchend', endDraw);
1434
+ annotationCanvas.addEventListener('mousedown', startDraw);
1435
+ annotationCanvas.addEventListener('mousemove', moveDraw);
1436
+ annotationCanvas.addEventListener('mouseup', endDraw);
1437
+ }
1438
+
1439
+ function closeAnnotation() {
1440
+ if (annotationOverlay) {
1441
+ document.body.removeChild(annotationOverlay);
1442
+ annotationOverlay = null;
1443
+ }
1444
+ }
1445
+
1446
+ // --- Voice: Speech-to-Text + Text-to-Speech ---
1447
+ var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
1448
+ var voiceBtn = document.getElementById('voice-btn');
1449
+ var voiceStatusEl = document.getElementById('voice-status');
1450
+ var voiceRecognition = null;
1451
+ var voiceActive = false;
1452
+ var voiceNoticeShown = false;
1453
+
1454
+ if (SpeechRecognition && window.isSecureContext) {
1455
+ voiceBtn.classList.remove('hidden');
1456
+
1457
+ voiceRecognition = new SpeechRecognition();
1458
+ voiceRecognition.continuous = false;
1459
+ voiceRecognition.interimResults = false;
1460
+ voiceRecognition.lang = '';
1461
+ voiceRecognition.maxAlternatives = 1;
1462
+
1463
+ voiceBtn.addEventListener('click', function() {
1464
+ if (voiceActive) {
1465
+ voiceRecognition.abort();
1466
+ resetVoiceState();
1467
+ return;
1468
+ }
1469
+
1470
+ voiceActive = true;
1471
+ voiceBtn.classList.add('listening');
1472
+ if (!voiceNoticeShown) {
1473
+ showVoiceStatus('Voice uses cloud speech recognition');
1474
+ voiceNoticeShown = true;
1475
+ setTimeout(function() {
1476
+ showVoiceStatus('Listening...');
1477
+ }, 1500);
1478
+ } else {
1479
+ showVoiceStatus('Listening...');
1480
+ }
1481
+
1482
+ try {
1483
+ voiceRecognition.start();
1484
+ } catch (e) {
1485
+ resetVoiceState();
1486
+ }
1487
+ });
1488
+
1489
+ voiceRecognition.onresult = function(event) {
1490
+ var transcript = event.results[0][0].transcript;
1491
+ var confidence = event.results[0][0].confidence;
1492
+
1493
+ voiceBtn.classList.remove('listening');
1494
+ voiceBtn.classList.add('processing');
1495
+ showVoiceStatus(transcript.length > 40 ? transcript.substring(0, 40) + '...' : transcript);
1496
+
1497
+ fetch(origin + '/mode-switch?token=' + encodeURIComponent(token), {
1498
+ method: 'POST',
1499
+ headers: { 'Content-Type': 'application/json' },
1500
+ body: JSON.stringify({
1501
+ action: 'voice_message',
1502
+ text: transcript,
1503
+ confidence: confidence
1504
+ })
1505
+ })
1506
+ .then(function(res) {
1507
+ if (!res.ok) throw new Error('Send failed');
1508
+ showVoiceStatus('Sent!');
1509
+ setTimeout(resetVoiceState, 1500);
1510
+ })
1511
+ .catch(function() {
1512
+ showVoiceStatus('Failed to send');
1513
+ setTimeout(resetVoiceState, 2000);
1514
+ });
1515
+ };
1516
+
1517
+ voiceRecognition.onerror = function(event) {
1518
+ var msg = 'Error';
1519
+ if (event.error === 'no-speech') msg = 'No speech detected';
1520
+ else if (event.error === 'not-allowed') msg = 'Mic access denied';
1521
+ else if (event.error === 'network') msg = 'Need internet for voice';
1522
+ else if (event.error === 'aborted') { resetVoiceState(); return; }
1523
+
1524
+ showVoiceStatus(msg);
1525
+ setTimeout(resetVoiceState, 2000);
1526
+ };
1527
+
1528
+ voiceRecognition.onend = function() {
1529
+ if (voiceActive && voiceBtn.classList.contains('listening')) {
1530
+ resetVoiceState();
1531
+ }
1532
+ };
1533
+ }
1534
+
1535
+ function resetVoiceState() {
1536
+ voiceActive = false;
1537
+ voiceBtn.classList.remove('listening', 'processing');
1538
+ hideVoiceStatus();
1539
+ }
1540
+
1541
+ function showVoiceStatus(text) {
1542
+ voiceStatusEl.textContent = text;
1543
+ voiceStatusEl.style.display = '';
1544
+ }
1545
+
1546
+ function hideVoiceStatus() {
1547
+ voiceStatusEl.style.display = 'none';
1548
+ }
1549
+ })();
1550
+ </script>
1551
+ </body>
1552
+ </html>`;
1553
+ }
1554
+ //# sourceMappingURL=mobile-page.js.map