main
html 619 lines 26.4 KB
Raw
1 <html>
2
3 <head>
4 <title>WhatsApp Integration</title>
5 <script type="module">
6 import { store } from "/plugins/_whatsapp_integration/webui/whatsapp-config-store.js";
7 </script>
8 </head>
9
10 <body>
11 <div x-data x-init="$store.whatsappConfig.init(config, context)" x-destroy="$store.whatsappConfig.cleanup()">
12 <template x-if="config">
13 <div class="wa-page">
14 <div class="section-title">WhatsApp</div>
15
16 <template x-if="config.enabled && $store.whatsappConfig.hasMeaningfulConfig()">
17 <div class="wa-summary-card">
18 <div class="wa-summary-copy">
19 <div class="wa-summary-title">Current setup</div>
20 <div class="wa-summary-subtitle"
21 x-text="$store.whatsappConfig.modeSummary() + ' · ' + $store.whatsappConfig.projectSummary()">
22 </div>
23 </div>
24 <span class="wa-status-pill" :class="'tone-' + $store.whatsappConfig.statusTone()"
25 x-text="$store.whatsappConfig.statusLabel()"></span>
26 </div>
27 </template>
28
29 <div class="wa-wizard-card">
30 <div class="wa-step-header">
31 <div class="wa-step-copy">
32 <div class="wa-step-title" x-text="$store.whatsappConfig.currentStepMeta().title"></div>
33 <div class="wa-step-description"
34 x-text="$store.whatsappConfig.currentStepMeta().description"></div>
35 </div>
36 <div class="wa-step-dots" aria-hidden="true">
37 <template x-for="(step, idx) in $store.whatsappConfig.steps" :key="step.title">
38 <button type="button" class="wa-step-dot"
39 :class="{ 'is-active': $store.whatsappConfig.currentStep === idx }"
40 @click="$store.whatsappConfig.setStep(idx)"></button>
41 </template>
42 </div>
43 </div>
44
45 <template x-if="$store.whatsappConfig.currentStep === 0">
46 <div class="wa-step-panel">
47
48 <div class="field">
49 <div class="field-label">
50 <div class="field-title">Turn on WhatsApp</div>
51 <div class="field-description">Enable the local bridge to start.</div>
52 </div>
53 <div class="field-control">
54 <label class="toggle">
55 <input type="checkbox"
56 x-model="config.enabled"
57 @change="$store.whatsappConfig.onEnabledChange()" />
58 <span class="toggler"></span>
59 </label>
60 </div>
61 </div>
62
63 <template x-if="config.enabled">
64 <div>
65 <div class="field">
66 <div class="field-label">
67 <div class="field-title">WhatsApp account</div>
68 <div class="field-description">
69 <span x-show="!$store.whatsappConfig.disconnectMessage">Click "Show QR code" to pair your WhatsApp account.</span>
70 <span x-show="$store.whatsappConfig.disconnectMessage"
71 x-text="$store.whatsappConfig.disconnectMessage"></span>
72 </div>
73 </div>
74 <div class="field-control wa-inline-actions">
75 <button class="btn btn-field" @click="$store.whatsappConfig.showQr()">
76 Show QR code
77 </button>
78 <button class="btn btn-field" @click="$store.whatsappConfig.disconnectAccount()"
79 :disabled="$store.whatsappConfig.disconnecting">
80 <span x-show="!$store.whatsappConfig.disconnecting">Disconnect</span>
81 <span x-show="$store.whatsappConfig.disconnecting">Disconnecting...</span>
82 </button>
83 </div>
84 </div>
85
86 <template x-if="$store.whatsappConfig.qrVisible">
87 <div class="wa-qr-panel">
88 <template x-if="$store.whatsappConfig.qrStatus === 'connected'">
89 <div>
90 <div class="wa-qr-status ok">Connected</div>
91 <div class="wa-qr-message" x-text="$store.whatsappConfig.qrMessage"></div>
92 <button class="btn btn-field" @click="$store.whatsappConfig.hideQr()"
93 style="margin-top: 12px;">
94 Close
95 </button>
96 </div>
97 </template>
98
99 <template
100 x-if="$store.whatsappConfig.qrStatus === 'waiting_scan' && $store.whatsappConfig.qrDataUrl">
101 <div>
102 <div class="wa-qr-status">Scan with WhatsApp on your phone</div>
103 <img :src="$store.whatsappConfig.qrDataUrl" alt="WhatsApp QR Code"
104 class="wa-qr-image" />
105 <div class="wa-qr-help">The QR code refreshes automatically.</div>
106 <button class="btn btn-field" @click="$store.whatsappConfig.hideQr()"
107 style="margin-top: 12px;">
108 Cancel
109 </button>
110 </div>
111 </template>
112
113 <template
114 x-if="$store.whatsappConfig.qrStatus !== 'connected' && !($store.whatsappConfig.qrStatus === 'waiting_scan' && $store.whatsappConfig.qrDataUrl)">
115 <div>
116 <div class="wa-qr-status"
117 x-text="$store.whatsappConfig.qrMessage || 'Connecting...'"></div>
118 <div class="wa-qr-help"
119 x-show="$store.whatsappConfig.qrStatus !== 'error'">
120 Please wait...</div>
121 <div class="wa-qr-help error"
122 x-show="$store.whatsappConfig.qrStatus === 'error'"
123 x-text="$store.whatsappConfig.qrMessage"></div>
124 <button class="btn btn-field" @click="$store.whatsappConfig.hideQr()"
125 style="margin-top: 12px;">
126 Cancel
127 </button>
128 </div>
129 </template>
130 </div>
131 </template>
132 </div>
133 </template>
134
135 <div class="wa-note" x-show="!config.enabled">
136 Turn on WhatsApp to pair or switch your account.
137 </div>
138
139 <div class="field">
140 <div class="field-label">
141 <div class="field-title">Mode</div>
142 <div class="field-description">
143 <span x-show="config.mode === 'self-chat'">
144 Use your own number. You can message yourself to talk to the agent.
145 </span>
146 <span x-show="config.mode !== 'self-chat'">
147 Use a separate number dedicated to Agent Zero conversations.
148 </span>
149 </div>
150 </div>
151 <div class="field-control">
152 <select x-model="config.mode">
153 <option value="self-chat">Personal number (self-chat)</option>
154 <option value="dedicated">Separate number (dedicated)</option>
155 </select>
156 </div>
157 </div>
158
159 <div class="wa-mode-note">
160 <strong>Good to know:</strong> Self-chat uses your own number. Dedicated is better for
161 shared or public access. You can pair now or come back to it later.
162 </div>
163
164 <div class="wa-warning" x-show="$store.whatsappConfig.accessWarning()">
165 <div class="wa-warning-title">
166 <span aria-hidden="true">&#9888;</span>
167 <span>Warning</span>
168 </div>
169 <div class="wa-warning-body" x-text="$store.whatsappConfig.accessWarning()"></div>
170 </div>
171
172 <div class="field">
173 <div class="field-label">
174 <div class="field-title">Allowed numbers</div>
175 <div class="field-description">Comma-separated phone numbers. Punctuation and +
176 prefixes are okay. Leave empty only if you want open access.</div>
177 </div>
178 <div class="field-control">
179 <input type="text" :value="$store.whatsappConfig.allowedText()"
180 @input="$store.whatsappConfig.setAllowed($event.target.value)"
181 placeholder="+1 (415) 555-1234, +44 7911 123456" />
182 </div>
183 </div>
184
185 <div class="field">
186 <div class="field-label">
187 <div class="field-title">Allow groups</div>
188 <div class="field-description">Reply in group chats when mentioned or replied to.
189 </div>
190 </div>
191 <div class="field-control">
192 <label class="toggle">
193 <input type="checkbox" x-model="config.allow_group" />
194 <span class="toggler"></span>
195 </label>
196 </div>
197 </div>
198 </div>
199 </template>
200
201 <template x-if="$store.whatsappConfig.currentStep === 1">
202 <div class="wa-step-panel">
203 <div class="field">
204 <div class="field-label">
205 <div class="field-title">Project</div>
206 <div class="field-description">Optional project to activate for WhatsApp
207 conversations.</div>
208 </div>
209 <div class="field-control">
210 <select :value="config.project" @change="config.project = $event.target.value">
211 <option value="">No project</option>
212 <template x-for="proj in $store.whatsappConfig.projects" :key="$store.whatsappConfig.projectOptionValue(proj)">
213 <option :value="$store.whatsappConfig.projectOptionValue(proj)"
214 x-text="$store.whatsappConfig.projectOptionLabel(proj)"
215 :selected="config.project === $store.whatsappConfig.projectOptionValue(proj)">
216 </option>
217 </template>
218 </select>
219 </div>
220 </div>
221
222 <div class="field">
223 <div class="field-label">
224 <div class="field-title">Agent instructions</div>
225 <div class="field-description">Extra guidance for how the agent should reply in
226 WhatsApp chats.</div>
227 </div>
228 <div class="field-control">
229 <textarea x-model="config.agent_instructions" rows="3"
230 placeholder="Reply briefly and naturally, like a mobile conversation."></textarea>
231 </div>
232 </div>
233 </div>
234 </template>
235
236 <div class="wa-test-panel">
237 <div class="wa-test-copy">
238 <div class="wa-test-title">Check the connection</div>
239 <div class="wa-test-description">We will check whether the local WhatsApp bridge is up and
240 connected.</div>
241 </div>
242 <button class="btn btn-field" @click="$store.whatsappConfig.testConnection()"
243 :disabled="$store.whatsappConfig.testing">
244 <span x-text="$store.whatsappConfig.testButtonLabel()"></span>
245 </button>
246 </div>
247
248 <template x-if="$store.whatsappConfig.testResults">
249 <div class="wa-results" :class="{ 'is-error': !$store.whatsappConfig.testResults.success }">
250 <template x-for="result in $store.whatsappConfig.testResults.results"
251 :key="result.test + result.message">
252 <div class="wa-result-row">
253 <span class="wa-result-icon" x-text="result.ok ? '✓' : '✗'"></span>
254 <div class="wa-result-copy">
255 <div class="wa-result-title" x-text="result.test"></div>
256 <div class="wa-result-message" x-text="result.message"></div>
257 </div>
258 </div>
259 </template>
260 </div>
261 </template>
262
263 <template x-if="$store.whatsappConfig.currentStep > 0">
264 <details class="wa-advanced">
265 <summary>
266 <span>Advanced</span>
267 <x-icon class="wa-advanced-chevron"
268 aria-hidden="true" name="keyboard_arrow_down"></x-icon>
269 </summary>
270 <div class="wa-advanced-body">
271 <div class="wa-info-box">
272 These settings are here when you need them, but the defaults are fine for most
273 setups.
274 </div>
275
276 <div class="field">
277 <div class="field-label">
278 <div class="field-title">Bridge port</div>
279 <div class="field-description">Local port for the WhatsApp bridge HTTP server.
280 </div>
281 </div>
282 <div class="field-control">
283 <input type="number" x-model.number="config.bridge_port" placeholder="3100" />
284 </div>
285 </div>
286
287 <div class="field">
288 <div class="field-label">
289 <div class="field-title">Poll interval (seconds)</div>
290 <div class="field-description">How often to check for new messages. The minimum
291 is 2 seconds.</div>
292 </div>
293 <div class="field-control">
294 <input type="number" x-model.number="config.poll_interval_seconds" min="2"
295 placeholder="3" />
296 </div>
297 </div>
298
299 </div>
300 </details>
301 </template>
302 </div>
303 </div>
304 </template>
305 </div>
306
307 <style>
308 .wa-page {
309 display: flex;
310 flex-direction: column;
311 gap: 0.9rem;
312 }
313
314 .wa-summary-card,
315 .wa-wizard-card {
316 border-radius: 0.5rem;
317 }
318
319 .wa-advanced summary {
320 cursor: pointer;
321 list-style: none;
322 font-weight: 700;
323 display: flex;
324 align-items: center;
325 gap: 0.75rem;
326 }
327
328 .wa-advanced summary::-webkit-details-marker {
329 display: none;
330 }
331
332 .wa-summary-title,
333 .wa-step-title,
334 .wa-test-title {
335 font-weight: 700;
336 }
337
338 .wa-summary-subtitle,
339 .wa-step-description,
340 .wa-note,
341 .wa-mode-note,
342 .wa-test-description,
343 .wa-result-message {
344 color: var(--color-text-secondary);
345 line-height: 1.5;
346 }
347
348 .wa-summary-card {
349 padding: 1rem;
350 border: 1px solid var(--color-border);
351 display: flex;
352 justify-content: space-between;
353 gap: 1rem;
354 align-items: center;
355 }
356
357 .wa-step-header {
358 display: flex;
359 justify-content: space-between;
360 gap: 1rem;
361 align-items: flex-start;
362 margin-bottom: 1rem;
363 }
364
365 .wa-step-dots {
366 display: flex;
367 gap: 0.5rem;
368 flex-wrap: wrap;
369 justify-content: flex-end;
370 align-items: center;
371 }
372
373 .wa-step-dot {
374 width: 0.85rem;
375 height: 0.85rem;
376 border-radius: 999px;
377 border: 1px solid var(--color-border);
378 background: transparent;
379 cursor: pointer;
380 padding: 0;
381 }
382
383 .wa-step-dot.is-active {
384 background: rgba(59, 130, 246, 0.9);
385 border-color: rgba(59, 130, 246, 0.9);
386 }
387
388 .wa-step-panel {
389 display: flex;
390 flex-direction: column;
391 gap: 0.2rem;
392 }
393
394 .wa-info-box,
395 .wa-note {
396 margin-bottom: 0.9rem;
397 padding: 0.8rem 0.9rem;
398 border-radius: 12px;
399 font-size: var(--font-size-small);
400 }
401
402 .wa-info-box {
403 background: color-mix(in srgb, #3b82f6 12%, var(--color-background) 88%);
404 }
405
406 .wa-note {
407 background: color-mix(in srgb, var(--color-background) 88%, white 12%);
408 }
409
410 .wa-warning {
411 margin: 0.5rem 0 1.25rem;
412 padding: 0.75rem 0.9rem;
413 border-radius: 10px;
414 border: 1px solid rgba(255, 170, 0, 0.45);
415 background: rgba(255, 170, 0, 0.12);
416 color: var(--color-warning-text);
417 font-size: var(--font-size-small);
418 }
419
420 .wa-warning-title {
421 font-weight: 600;
422 display: flex;
423 align-items: center;
424 gap: 0.5rem;
425 }
426
427 .wa-warning-body {
428 margin-top: 0.25rem;
429 line-height: 1.45;
430 }
431
432 .wa-mode-note {
433 margin-top: -0.2rem;
434 margin-bottom: 0.9rem;
435 font-size: var(--font-size-small);
436 }
437
438 .wa-inline-actions {
439 display: flex;
440 gap: 0.75rem;
441 flex-wrap: wrap;
442 }
443
444 .wa-qr-panel {
445 margin-top: 0.25rem;
446 padding: 1rem;
447 border-radius: 14px;
448 border: 1px solid color-mix(in srgb, var(--color-border) 88%, white 12%);
449 background: color-mix(in srgb, var(--color-background) 90%, white 10%);
450 text-align: center;
451 }
452
453 .wa-qr-status {
454 font-weight: 700;
455 margin-bottom: 0.5rem;
456 }
457
458 .wa-qr-status.ok {
459 color: #7ee7a4;
460 }
461
462 .wa-qr-message,
463 .wa-qr-help {
464 color: var(--color-text-secondary);
465 line-height: 1.5;
466 }
467
468 .wa-qr-help {
469 font-size: var(--font-size-small);
470 margin-top: 0.4rem;
471 }
472
473 .wa-qr-help.error {
474 color: #fca5a5;
475 }
476
477 .wa-qr-image {
478 width: 256px;
479 height: 256px;
480 max-width: 100%;
481 border-radius: 8px;
482 background: white;
483 padding: 4px;
484 }
485
486 .wa-advanced {
487 margin-top: 1rem;
488 border: 1px solid color-mix(in srgb, var(--color-border) 88%, white 12%);
489 border-radius: 14px;
490 overflow: hidden;
491 }
492
493 .wa-advanced summary {
494 padding: 0.9rem 1rem;
495 background: color-mix(in srgb, var(--color-background) 88%, white 12%);
496 }
497
498 .wa-advanced-chevron {
499 margin-left: auto;
500 flex: 0 0 auto;
501 transition: transform 0.18s ease, opacity 0.18s ease;
502 opacity: 0.72;
503 }
504
505 .wa-advanced[open] .wa-advanced-chevron {
506 transform: rotate(180deg);
507 opacity: 1;
508 }
509
510 .wa-advanced-body {
511 padding: 1rem;
512 display: flex;
513 flex-direction: column;
514 gap: 0.2rem;
515 }
516
517 .wa-test-panel {
518 margin-top: 1rem;
519 padding: var(--spacing-xs) 0;
520 display: flex;
521 justify-content: space-between;
522 align-items: center;
523 gap: 1rem;
524 }
525
526 .wa-test-copy,
527 .wa-result-copy {
528 min-width: 0;
529 }
530
531 .wa-results {
532 margin-top: 0.9rem;
533 border-radius: 14px;
534 border: 1px solid color-mix(in srgb, #22c55e 28%, var(--color-border) 72%);
535 background: color-mix(in srgb, #22c55e 8%, var(--color-background) 92%);
536 padding: 0.35rem 0.9rem;
537 }
538
539 .wa-results.is-error {
540 border-color: color-mix(in srgb, #ef4444 28%, var(--color-border) 72%);
541 background: color-mix(in srgb, #ef4444 8%, var(--color-background) 92%);
542 }
543
544 .wa-result-row {
545 display: flex;
546 align-items: flex-start;
547 gap: 0.8rem;
548 padding: 0.7rem 0;
549 }
550
551 .wa-result-row+.wa-result-row {
552 border-top: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%);
553 }
554
555 .wa-result-icon {
556 width: 1.25rem;
557 font-weight: 800;
558 line-height: 1.3;
559 }
560
561 .wa-result-title {
562 font-weight: 700;
563 margin-bottom: 0.18rem;
564 }
565
566 .wa-status-pill {
567 display: inline-flex;
568 align-items: center;
569 justify-content: center;
570 min-width: 5.25rem;
571 padding: 0.35rem 0.7rem;
572 border-radius: 999px;
573 font-size: 0.8rem;
574 font-weight: 700;
575 }
576
577 .wa-status-pill.tone-success {
578 background: rgba(34, 197, 94, 0.14);
579 color: #7ee7a4;
580 }
581
582 .wa-status-pill.tone-ready {
583 background: rgba(59, 130, 246, 0.16);
584 color: #93c5fd;
585 }
586
587 .wa-status-pill.tone-warning {
588 background: rgba(245, 158, 11, 0.16);
589 color: #fcd34d;
590 }
591
592 .wa-status-pill.tone-muted {
593 background: rgba(148, 163, 184, 0.14);
594 color: #cbd5e1;
595 }
596
597 @media (max-width: 640px) {
598
599 .wa-summary-card,
600 .wa-step-header,
601 .wa-test-panel {
602 flex-direction: column;
603 align-items: stretch;
604 }
605
606 .wa-step-dots {
607 width: 100%;
608 justify-content: space-between;
609 }
610
611 .wa-status-pill {
612 min-width: 0;
613 align-self: flex-start;
614 }
615 }
616 </style>
617 </body>
618
619 </html>