@conversionpros/aiva 1.0.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/README.md +148 -0
- package/auto-deploy.js +190 -0
- package/bin/aiva.js +81 -0
- package/cli-sync.js +126 -0
- package/d2a-prompt-template.txt +106 -0
- package/diagnostics-api.js +304 -0
- package/docs/ara-dedup-fix-scope.md +112 -0
- package/docs/ara-fix-round2-scope.md +61 -0
- package/docs/ara-greeting-fix-scope.md +70 -0
- package/docs/calendar-date-fix-scope.md +28 -0
- package/docs/getting-started.md +115 -0
- package/docs/network-architecture-rollout-scope.md +43 -0
- package/docs/scope-google-oauth-integration.md +351 -0
- package/docs/settings-page-scope.md +50 -0
- package/docs/xai-imagine-scope.md +116 -0
- package/docs/xai-voice-integration-scope.md +115 -0
- package/docs/xai-voice-tools-scope.md +165 -0
- package/email-router.js +512 -0
- package/follow-up-handler.js +606 -0
- package/gateway-monitor.js +158 -0
- package/google-email.js +379 -0
- package/google-oauth.js +310 -0
- package/grok-imagine.js +97 -0
- package/health-reporter.js +287 -0
- package/invisible-prefix-base.txt +206 -0
- package/invisible-prefix-owner.txt +26 -0
- package/invisible-prefix-slim.txt +10 -0
- package/invisible-prefix.txt +43 -0
- package/knowledge-base.js +472 -0
- package/lib/cli.js +19 -0
- package/lib/config.js +124 -0
- package/lib/health.js +57 -0
- package/lib/process.js +207 -0
- package/lib/server.js +42 -0
- package/lib/setup.js +472 -0
- package/meta-capi.js +206 -0
- package/meta-leads.js +411 -0
- package/notion-oauth.js +323 -0
- package/package.json +61 -0
- package/public/agent-config.html +241 -0
- package/public/aiva-avatar-anime.png +0 -0
- package/public/css/docs.css.bak +688 -0
- package/public/css/onboarding.css +543 -0
- package/public/diagrams/claude-subscription-pool.html +329 -0
- package/public/diagrams/claude-subscription-pool.png +0 -0
- package/public/docs-icon.png +0 -0
- package/public/escalation.html +237 -0
- package/public/group-config.html +300 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icons/agents.svg +1 -0
- package/public/icons/attach.svg +1 -0
- package/public/icons/characters.svg +1 -0
- package/public/icons/chat.svg +1 -0
- package/public/icons/docs.svg +1 -0
- package/public/icons/heartbeat.svg +1 -0
- package/public/icons/messages.svg +1 -0
- package/public/icons/mic.svg +1 -0
- package/public/icons/notes.svg +1 -0
- package/public/icons/settings.svg +1 -0
- package/public/icons/tasks.svg +1 -0
- package/public/images/onboarding/p0-communication-layer.png +0 -0
- package/public/images/onboarding/p0-infinite-surface.png +0 -0
- package/public/images/onboarding/p0-learning-model.png +0 -0
- package/public/images/onboarding/p0-meet-aiva.png +0 -0
- package/public/images/onboarding/p4-contact-intelligence.png +0 -0
- package/public/images/onboarding/p4-context-compounds.png +0 -0
- package/public/images/onboarding/p4-message-router.png +0 -0
- package/public/images/onboarding/p4-per-contact-rules.png +0 -0
- package/public/images/onboarding/p4-send-messages.png +0 -0
- package/public/images/onboarding/p6-be-precise.png +0 -0
- package/public/images/onboarding/p6-review-escalations.png +0 -0
- package/public/images/onboarding/p6-voice-input.png +0 -0
- package/public/images/onboarding/p7-completion.png +0 -0
- package/public/index.html +11594 -0
- package/public/js/onboarding.js +699 -0
- package/public/manifest.json +24 -0
- package/public/messages-v2.html +2824 -0
- package/public/permission-approve.html.bak +107 -0
- package/public/permissions.html +150 -0
- package/public/styles/design-system.css +68 -0
- package/router-db.js +604 -0
- package/router-utils.js +28 -0
- package/router-v2/adapters/imessage.js +191 -0
- package/router-v2/adapters/quo.js +82 -0
- package/router-v2/adapters/whatsapp.js +192 -0
- package/router-v2/contact-manager.js +234 -0
- package/router-v2/conversation-engine.js +498 -0
- package/router-v2/data/knowledge-base.json +176 -0
- package/router-v2/data/router-v2.db +0 -0
- package/router-v2/data/router-v2.db-shm +0 -0
- package/router-v2/data/router-v2.db-wal +0 -0
- package/router-v2/data/router.db +0 -0
- package/router-v2/db.js +457 -0
- package/router-v2/escalation-bridge.js +540 -0
- package/router-v2/follow-up-engine.js +347 -0
- package/router-v2/index.js +441 -0
- package/router-v2/ingestion.js +213 -0
- package/router-v2/knowledge-base.js +231 -0
- package/router-v2/lead-qualifier.js +152 -0
- package/router-v2/learning-loop.js +202 -0
- package/router-v2/outbound-sender.js +160 -0
- package/router-v2/package.json +13 -0
- package/router-v2/permission-gate.js +86 -0
- package/router-v2/playbook.js +177 -0
- package/router-v2/prompts/base.js +52 -0
- package/router-v2/prompts/first-contact.js +38 -0
- package/router-v2/prompts/lead-qualification.js +37 -0
- package/router-v2/prompts/scheduling.js +72 -0
- package/router-v2/prompts/style-overrides.js +22 -0
- package/router-v2/scheduler.js +301 -0
- package/router-v2/scripts/migrate-v1-to-v2.js +215 -0
- package/router-v2/scripts/seed-faq.js +67 -0
- package/router-v2/seed-knowledge-base.js +39 -0
- package/router-v2/utils/ai.js +129 -0
- package/router-v2/utils/phone.js +52 -0
- package/router-v2/utils/response-validator.js +98 -0
- package/router-v2/utils/sanitize.js +222 -0
- package/router.js +5005 -0
- package/routes/google-calendar.js +186 -0
- package/scripts/deploy.sh +62 -0
- package/scripts/macos-calendar.sh +232 -0
- package/scripts/onboard-device.sh +466 -0
- package/server.js +5131 -0
- package/start.sh +24 -0
- package/templates/AGENTS.md +548 -0
- package/templates/IDENTITY.md +15 -0
- package/templates/docs-agents.html +132 -0
- package/templates/docs-app.html +130 -0
- package/templates/docs-home.html +83 -0
- package/templates/docs-imessage.html +121 -0
- package/templates/docs-tasks.html +123 -0
- package/templates/docs-tips.html +175 -0
- package/templates/getting-started.html +809 -0
- package/templates/invisible-prefix-base.txt +171 -0
- package/templates/invisible-prefix-owner.txt +282 -0
- package/templates/invisible-prefix.txt +338 -0
- package/templates/manifest.json +61 -0
- package/templates/memory-org/clients.md +7 -0
- package/templates/memory-org/credentials.md +9 -0
- package/templates/memory-org/devices.md +7 -0
- package/templates/updates.html +464 -0
- package/templates/workspace/AGENTS.md.tmpl +161 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +17 -0
- package/templates/workspace/IDENTITY.md.tmpl +15 -0
- package/templates/workspace/MEMORY.md.tmpl +16 -0
- package/templates/workspace/SOUL.md.tmpl +51 -0
- package/templates/workspace/USER.md.tmpl +25 -0
- package/tts-proxy.js +96 -0
- package/voice-call-local.js +731 -0
- package/voice-call.js +732 -0
- package/wa-listener.js +354 -0
|
@@ -0,0 +1,2824 @@
|
|
|
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>AIVA - Messages</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
9
|
+
<style>
|
|
10
|
+
/* ============================================
|
|
11
|
+
AIVA Messages v2 - Premium Contact Manager
|
|
12
|
+
============================================ */
|
|
13
|
+
|
|
14
|
+
:root {
|
|
15
|
+
--red: #FF3333;
|
|
16
|
+
--red-dim: rgba(255, 51, 51, 0.15);
|
|
17
|
+
--red-glow: rgba(255, 51, 51, 0.3);
|
|
18
|
+
--void: #050505;
|
|
19
|
+
--bg: #0a0a0a;
|
|
20
|
+
--surface: #111111;
|
|
21
|
+
--surface-2: #181818;
|
|
22
|
+
--surface-3: #1f1f1f;
|
|
23
|
+
--border: #252525;
|
|
24
|
+
--border-light: #333333;
|
|
25
|
+
--text: #e8e8e8;
|
|
26
|
+
--text-secondary: #999999;
|
|
27
|
+
--text-muted: #666666;
|
|
28
|
+
--white: #ffffff;
|
|
29
|
+
|
|
30
|
+
/* Pipeline colors */
|
|
31
|
+
--stage-cold: #6b7280;
|
|
32
|
+
--stage-warm: #eab308;
|
|
33
|
+
--stage-hot: #f97316;
|
|
34
|
+
--stage-qualified: #22c55e;
|
|
35
|
+
--stage-client: #3b82f6;
|
|
36
|
+
--stage-none: #444444;
|
|
37
|
+
|
|
38
|
+
/* Category colors */
|
|
39
|
+
--cat-lead: #eab308;
|
|
40
|
+
--cat-client: #3b82f6;
|
|
41
|
+
--cat-family: #a855f7;
|
|
42
|
+
--cat-friend: #22c55e;
|
|
43
|
+
--cat-team: #06b6d4;
|
|
44
|
+
--cat-unknown: #6b7280;
|
|
45
|
+
--cat-blocked: #ef4444;
|
|
46
|
+
|
|
47
|
+
--radius: 8px;
|
|
48
|
+
--radius-lg: 12px;
|
|
49
|
+
--transition: 150ms ease;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
53
|
+
|
|
54
|
+
body {
|
|
55
|
+
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
56
|
+
background: var(--bg);
|
|
57
|
+
color: var(--text);
|
|
58
|
+
height: 100vh;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
-webkit-font-smoothing: antialiased;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ---- Layout ---- */
|
|
64
|
+
.app {
|
|
65
|
+
display: flex;
|
|
66
|
+
height: 100vh;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ---- Left Sidebar Navigation ---- */
|
|
70
|
+
.sidebar-nav {
|
|
71
|
+
width: 56px;
|
|
72
|
+
background: var(--void);
|
|
73
|
+
border-right: 1px solid var(--border);
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
align-items: center;
|
|
77
|
+
padding: 16px 0;
|
|
78
|
+
gap: 4px;
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.sidebar-nav .nav-btn {
|
|
83
|
+
width: 40px;
|
|
84
|
+
height: 40px;
|
|
85
|
+
border: none;
|
|
86
|
+
background: transparent;
|
|
87
|
+
border-radius: var(--radius);
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
color: var(--text-muted);
|
|
93
|
+
transition: all var(--transition);
|
|
94
|
+
position: relative;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.sidebar-nav .nav-btn:hover { background: var(--surface-2); color: var(--text-secondary); }
|
|
98
|
+
.sidebar-nav .nav-btn.active { background: var(--red-dim); color: var(--red); }
|
|
99
|
+
.sidebar-nav .nav-btn.active::before {
|
|
100
|
+
content: '';
|
|
101
|
+
position: absolute;
|
|
102
|
+
left: -8px;
|
|
103
|
+
top: 50%;
|
|
104
|
+
transform: translateY(-50%);
|
|
105
|
+
width: 3px;
|
|
106
|
+
height: 20px;
|
|
107
|
+
background: var(--red);
|
|
108
|
+
border-radius: 0 3px 3px 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.sidebar-nav .nav-spacer { flex: 1; }
|
|
112
|
+
|
|
113
|
+
.sidebar-nav svg { width: 20px; height: 20px; }
|
|
114
|
+
|
|
115
|
+
/* ---- Contact List Panel ---- */
|
|
116
|
+
.contact-panel {
|
|
117
|
+
width: 340px;
|
|
118
|
+
background: var(--surface);
|
|
119
|
+
border-right: 1px solid var(--border);
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
flex-shrink: 0;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.contact-panel-header {
|
|
127
|
+
padding: 20px 16px 12px;
|
|
128
|
+
border-bottom: 1px solid var(--border);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.contact-panel-header h2 {
|
|
132
|
+
font-size: 18px;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
margin-bottom: 12px;
|
|
135
|
+
letter-spacing: -0.3px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.search-bar {
|
|
139
|
+
position: relative;
|
|
140
|
+
margin-bottom: 10px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.search-bar input {
|
|
144
|
+
width: 100%;
|
|
145
|
+
padding: 8px 12px 8px 34px;
|
|
146
|
+
background: var(--surface-2);
|
|
147
|
+
border: 1px solid var(--border);
|
|
148
|
+
border-radius: var(--radius);
|
|
149
|
+
color: var(--text);
|
|
150
|
+
font-size: 13px;
|
|
151
|
+
font-family: inherit;
|
|
152
|
+
outline: none;
|
|
153
|
+
transition: border-color var(--transition);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.search-bar input:focus { border-color: var(--red); }
|
|
157
|
+
|
|
158
|
+
.search-bar input::placeholder { color: var(--text-muted); }
|
|
159
|
+
|
|
160
|
+
.search-bar .search-icon {
|
|
161
|
+
position: absolute;
|
|
162
|
+
left: 10px;
|
|
163
|
+
top: 50%;
|
|
164
|
+
transform: translateY(-50%);
|
|
165
|
+
color: var(--text-muted);
|
|
166
|
+
width: 16px;
|
|
167
|
+
height: 16px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.filter-row {
|
|
171
|
+
display: flex;
|
|
172
|
+
gap: 6px;
|
|
173
|
+
flex-wrap: wrap;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.filter-select {
|
|
177
|
+
padding: 5px 8px;
|
|
178
|
+
background: var(--surface-2);
|
|
179
|
+
border: 1px solid var(--border);
|
|
180
|
+
border-radius: 6px;
|
|
181
|
+
color: var(--text-secondary);
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
font-family: inherit;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
outline: none;
|
|
186
|
+
flex: 1;
|
|
187
|
+
min-width: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.filter-select:focus { border-color: var(--red); }
|
|
191
|
+
|
|
192
|
+
/* Contact List */
|
|
193
|
+
.contact-list {
|
|
194
|
+
flex: 1;
|
|
195
|
+
overflow-y: auto;
|
|
196
|
+
padding: 4px 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.contact-list::-webkit-scrollbar { width: 4px; }
|
|
200
|
+
.contact-list::-webkit-scrollbar-track { background: transparent; }
|
|
201
|
+
.contact-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
202
|
+
|
|
203
|
+
.contact-item {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: flex-start;
|
|
206
|
+
gap: 10px;
|
|
207
|
+
padding: 12px 16px;
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
transition: background var(--transition);
|
|
210
|
+
border-left: 3px solid transparent;
|
|
211
|
+
position: relative;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.contact-item:hover { background: var(--surface-2); }
|
|
215
|
+
.contact-item.active { background: var(--surface-2); border-left-color: var(--red); }
|
|
216
|
+
|
|
217
|
+
.contact-avatar {
|
|
218
|
+
width: 38px;
|
|
219
|
+
height: 38px;
|
|
220
|
+
border-radius: 50%;
|
|
221
|
+
background: var(--surface-3);
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: center;
|
|
225
|
+
font-size: 14px;
|
|
226
|
+
font-weight: 600;
|
|
227
|
+
color: var(--text-secondary);
|
|
228
|
+
flex-shrink: 0;
|
|
229
|
+
position: relative;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.contact-avatar .mode-dot {
|
|
233
|
+
position: absolute;
|
|
234
|
+
bottom: -1px;
|
|
235
|
+
right: -1px;
|
|
236
|
+
width: 12px;
|
|
237
|
+
height: 12px;
|
|
238
|
+
border-radius: 50%;
|
|
239
|
+
border: 2px solid var(--surface);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.mode-dot.auto { background: var(--stage-qualified); }
|
|
243
|
+
.mode-dot.escalate { background: var(--stage-warm); }
|
|
244
|
+
.mode-dot.monitor { background: var(--stage-cold); }
|
|
245
|
+
.mode-dot.block { background: var(--cat-blocked); }
|
|
246
|
+
|
|
247
|
+
.contact-info {
|
|
248
|
+
flex: 1;
|
|
249
|
+
min-width: 0;
|
|
250
|
+
display: flex;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
gap: 2px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.contact-name-row {
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: 6px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.contact-name {
|
|
262
|
+
font-size: 13.5px;
|
|
263
|
+
font-weight: 500;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
overflow: hidden;
|
|
266
|
+
text-overflow: ellipsis;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.pipeline-badge {
|
|
270
|
+
font-size: 10px;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
padding: 1px 6px;
|
|
273
|
+
border-radius: 10px;
|
|
274
|
+
text-transform: uppercase;
|
|
275
|
+
letter-spacing: 0.5px;
|
|
276
|
+
flex-shrink: 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.pipeline-badge.cold { background: rgba(107,114,128,0.2); color: var(--stage-cold); }
|
|
280
|
+
.pipeline-badge.warm { background: rgba(234,179,8,0.15); color: var(--stage-warm); }
|
|
281
|
+
.pipeline-badge.hot { background: rgba(249,115,22,0.15); color: var(--stage-hot); }
|
|
282
|
+
.pipeline-badge.qualified { background: rgba(34,197,94,0.15); color: var(--stage-qualified); }
|
|
283
|
+
.pipeline-badge.client { background: rgba(59,130,246,0.15); color: var(--stage-client); }
|
|
284
|
+
.pipeline-badge.none { display: none; }
|
|
285
|
+
|
|
286
|
+
.contact-preview {
|
|
287
|
+
font-size: 12px;
|
|
288
|
+
color: var(--text-muted);
|
|
289
|
+
white-space: nowrap;
|
|
290
|
+
overflow: hidden;
|
|
291
|
+
text-overflow: ellipsis;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.contact-meta {
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
align-items: flex-end;
|
|
298
|
+
gap: 4px;
|
|
299
|
+
flex-shrink: 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.contact-time {
|
|
303
|
+
font-size: 11px;
|
|
304
|
+
color: var(--text-muted);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.contact-score {
|
|
308
|
+
font-size: 10px;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
color: var(--text-muted);
|
|
311
|
+
background: var(--surface-3);
|
|
312
|
+
padding: 1px 5px;
|
|
313
|
+
border-radius: 8px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.category-tag {
|
|
317
|
+
font-size: 10px;
|
|
318
|
+
color: var(--text-muted);
|
|
319
|
+
margin-top: 1px;
|
|
320
|
+
text-transform: capitalize;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.category-tag.cat-lead, .category-tag.cat-qualified-lead { color: var(--cat-lead); }
|
|
324
|
+
.category-tag.cat-client { color: var(--cat-client); }
|
|
325
|
+
.category-tag.cat-family { color: var(--cat-family); }
|
|
326
|
+
.category-tag.cat-friend { color: var(--cat-friend); }
|
|
327
|
+
.category-tag.cat-team { color: var(--cat-team); }
|
|
328
|
+
.category-tag.cat-blocked { color: var(--cat-blocked); }
|
|
329
|
+
|
|
330
|
+
/* ---- Detail Panel ---- */
|
|
331
|
+
.detail-panel {
|
|
332
|
+
flex: 1;
|
|
333
|
+
display: flex;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
overflow: hidden;
|
|
336
|
+
background: var(--bg);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* Empty state */
|
|
340
|
+
.empty-state {
|
|
341
|
+
flex: 1;
|
|
342
|
+
display: flex;
|
|
343
|
+
flex-direction: column;
|
|
344
|
+
align-items: center;
|
|
345
|
+
justify-content: center;
|
|
346
|
+
gap: 12px;
|
|
347
|
+
color: var(--text-muted);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.empty-state svg { width: 48px; height: 48px; opacity: 0.3; }
|
|
351
|
+
.empty-state p { font-size: 14px; }
|
|
352
|
+
|
|
353
|
+
/* Detail Header */
|
|
354
|
+
.detail-header {
|
|
355
|
+
padding: 16px 24px;
|
|
356
|
+
border-bottom: 1px solid var(--border);
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
gap: 14px;
|
|
360
|
+
background: var(--surface);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.detail-avatar {
|
|
364
|
+
width: 44px;
|
|
365
|
+
height: 44px;
|
|
366
|
+
border-radius: 50%;
|
|
367
|
+
background: var(--surface-3);
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
justify-content: center;
|
|
371
|
+
font-size: 16px;
|
|
372
|
+
font-weight: 600;
|
|
373
|
+
color: var(--text-secondary);
|
|
374
|
+
flex-shrink: 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.detail-info { flex: 1; }
|
|
378
|
+
.detail-info .name { font-size: 15px; font-weight: 600; }
|
|
379
|
+
.detail-info .phone { font-size: 12px; color: var(--text-muted); margin-top: 1px; font-family: 'JetBrains Mono', monospace; }
|
|
380
|
+
|
|
381
|
+
.detail-badges {
|
|
382
|
+
display: flex;
|
|
383
|
+
gap: 8px;
|
|
384
|
+
align-items: center;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.detail-category {
|
|
388
|
+
font-size: 11px;
|
|
389
|
+
font-weight: 500;
|
|
390
|
+
padding: 3px 10px;
|
|
391
|
+
border-radius: 12px;
|
|
392
|
+
text-transform: capitalize;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.detail-category.lead { background: rgba(234,179,8,0.15); color: var(--cat-lead); }
|
|
396
|
+
.detail-category.client { background: rgba(59,130,246,0.15); color: var(--cat-client); }
|
|
397
|
+
.detail-category.family { background: rgba(168,85,247,0.15); color: var(--cat-family); }
|
|
398
|
+
.detail-category.friend { background: rgba(34,197,94,0.15); color: var(--cat-friend); }
|
|
399
|
+
.detail-category.team { background: rgba(6,182,212,0.15); color: var(--cat-team); }
|
|
400
|
+
.detail-category.unknown { background: rgba(107,114,128,0.15); color: var(--cat-unknown); }
|
|
401
|
+
.detail-category.blocked { background: rgba(239,68,68,0.15); color: var(--cat-blocked); }
|
|
402
|
+
.detail-category.qualifiedlead { background: rgba(234,179,8,0.15); color: var(--cat-lead); }
|
|
403
|
+
|
|
404
|
+
.score-mini {
|
|
405
|
+
display: flex;
|
|
406
|
+
align-items: center;
|
|
407
|
+
gap: 4px;
|
|
408
|
+
font-size: 12px;
|
|
409
|
+
color: var(--text-secondary);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.score-mini .score-ring {
|
|
413
|
+
width: 28px;
|
|
414
|
+
height: 28px;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* Tabs */
|
|
418
|
+
.tabs-bar {
|
|
419
|
+
display: flex;
|
|
420
|
+
border-bottom: 1px solid var(--border);
|
|
421
|
+
background: var(--surface);
|
|
422
|
+
padding: 0 24px;
|
|
423
|
+
gap: 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.tab-btn {
|
|
427
|
+
padding: 10px 16px;
|
|
428
|
+
font-size: 12.5px;
|
|
429
|
+
font-weight: 500;
|
|
430
|
+
color: var(--text-muted);
|
|
431
|
+
background: none;
|
|
432
|
+
border: none;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
position: relative;
|
|
435
|
+
font-family: inherit;
|
|
436
|
+
transition: color var(--transition);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.tab-btn:hover { color: var(--text-secondary); }
|
|
440
|
+
|
|
441
|
+
.tab-btn.active {
|
|
442
|
+
color: var(--text);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.tab-btn.active::after {
|
|
446
|
+
content: '';
|
|
447
|
+
position: absolute;
|
|
448
|
+
bottom: -1px;
|
|
449
|
+
left: 16px;
|
|
450
|
+
right: 16px;
|
|
451
|
+
height: 2px;
|
|
452
|
+
background: var(--red);
|
|
453
|
+
border-radius: 2px 2px 0 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* Tab Content */
|
|
457
|
+
.tab-content {
|
|
458
|
+
flex: 1;
|
|
459
|
+
overflow-y: auto;
|
|
460
|
+
display: none;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.tab-content.active { display: flex; flex-direction: column; }
|
|
464
|
+
|
|
465
|
+
.tab-content::-webkit-scrollbar { width: 5px; }
|
|
466
|
+
.tab-content::-webkit-scrollbar-track { background: transparent; }
|
|
467
|
+
.tab-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
468
|
+
|
|
469
|
+
/* ---- Conversation Tab ---- */
|
|
470
|
+
.messages-container {
|
|
471
|
+
flex: 1;
|
|
472
|
+
padding: 20px 24px;
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-direction: column;
|
|
475
|
+
gap: 8px;
|
|
476
|
+
overflow-y: auto;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.message-bubble {
|
|
480
|
+
max-width: 70%;
|
|
481
|
+
padding: 10px 14px;
|
|
482
|
+
border-radius: 14px;
|
|
483
|
+
font-size: 13.5px;
|
|
484
|
+
line-height: 1.5;
|
|
485
|
+
position: relative;
|
|
486
|
+
animation: fadeIn 200ms ease;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
@keyframes fadeIn {
|
|
490
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
491
|
+
to { opacity: 1; transform: translateY(0); }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.message-bubble.inbound {
|
|
495
|
+
align-self: flex-start;
|
|
496
|
+
background: var(--surface-2);
|
|
497
|
+
border-bottom-left-radius: 4px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.message-bubble.outbound {
|
|
501
|
+
align-self: flex-end;
|
|
502
|
+
background: var(--red);
|
|
503
|
+
color: var(--white);
|
|
504
|
+
border-bottom-right-radius: 4px;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.message-bubble .msg-time {
|
|
508
|
+
font-size: 10px;
|
|
509
|
+
opacity: 0.5;
|
|
510
|
+
margin-top: 4px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.message-bubble.outbound .msg-time { text-align: right; }
|
|
514
|
+
|
|
515
|
+
.message-input-bar {
|
|
516
|
+
padding: 12px 24px;
|
|
517
|
+
border-top: 1px solid var(--border);
|
|
518
|
+
display: flex;
|
|
519
|
+
gap: 10px;
|
|
520
|
+
background: var(--surface);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.message-input-bar input {
|
|
524
|
+
flex: 1;
|
|
525
|
+
padding: 10px 14px;
|
|
526
|
+
background: var(--surface-2);
|
|
527
|
+
border: 1px solid var(--border);
|
|
528
|
+
border-radius: var(--radius);
|
|
529
|
+
color: var(--text);
|
|
530
|
+
font-size: 13px;
|
|
531
|
+
font-family: inherit;
|
|
532
|
+
outline: none;
|
|
533
|
+
transition: border-color var(--transition);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.message-input-bar input:focus { border-color: var(--red); }
|
|
537
|
+
.message-input-bar input::placeholder { color: var(--text-muted); }
|
|
538
|
+
|
|
539
|
+
.btn-send {
|
|
540
|
+
padding: 8px 18px;
|
|
541
|
+
background: var(--red);
|
|
542
|
+
border: none;
|
|
543
|
+
border-radius: var(--radius);
|
|
544
|
+
color: var(--white);
|
|
545
|
+
font-size: 13px;
|
|
546
|
+
font-weight: 500;
|
|
547
|
+
font-family: inherit;
|
|
548
|
+
cursor: pointer;
|
|
549
|
+
transition: opacity var(--transition);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.btn-send:hover { opacity: 0.9; }
|
|
553
|
+
|
|
554
|
+
/* ---- Settings Tab ---- */
|
|
555
|
+
.settings-form {
|
|
556
|
+
padding: 24px;
|
|
557
|
+
display: flex;
|
|
558
|
+
flex-direction: column;
|
|
559
|
+
gap: 20px;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.form-group {
|
|
563
|
+
display: flex;
|
|
564
|
+
flex-direction: column;
|
|
565
|
+
gap: 6px;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.form-group label {
|
|
569
|
+
font-size: 12px;
|
|
570
|
+
font-weight: 500;
|
|
571
|
+
color: var(--text-secondary);
|
|
572
|
+
text-transform: uppercase;
|
|
573
|
+
letter-spacing: 0.5px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.form-group select,
|
|
577
|
+
.form-group input[type="text"],
|
|
578
|
+
.form-group input[type="number"],
|
|
579
|
+
.form-group input[type="time"],
|
|
580
|
+
.form-group textarea {
|
|
581
|
+
padding: 9px 12px;
|
|
582
|
+
background: var(--surface-2);
|
|
583
|
+
border: 1px solid var(--border);
|
|
584
|
+
border-radius: var(--radius);
|
|
585
|
+
color: var(--text);
|
|
586
|
+
font-size: 13px;
|
|
587
|
+
font-family: inherit;
|
|
588
|
+
outline: none;
|
|
589
|
+
transition: border-color var(--transition);
|
|
590
|
+
resize: vertical;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.form-group select:focus,
|
|
594
|
+
.form-group input:focus,
|
|
595
|
+
.form-group textarea:focus { border-color: var(--red); }
|
|
596
|
+
|
|
597
|
+
.form-group textarea { min-height: 80px; }
|
|
598
|
+
|
|
599
|
+
.form-row {
|
|
600
|
+
display: grid;
|
|
601
|
+
grid-template-columns: 1fr 1fr;
|
|
602
|
+
gap: 16px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.btn-save {
|
|
606
|
+
padding: 10px 20px;
|
|
607
|
+
background: var(--red);
|
|
608
|
+
border: none;
|
|
609
|
+
border-radius: var(--radius);
|
|
610
|
+
color: var(--white);
|
|
611
|
+
font-size: 13px;
|
|
612
|
+
font-weight: 500;
|
|
613
|
+
font-family: inherit;
|
|
614
|
+
cursor: pointer;
|
|
615
|
+
align-self: flex-start;
|
|
616
|
+
transition: opacity var(--transition);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.btn-save:hover { opacity: 0.9; }
|
|
620
|
+
|
|
621
|
+
/* ---- Permissions Tab ---- */
|
|
622
|
+
.permissions-grid {
|
|
623
|
+
padding: 24px;
|
|
624
|
+
display: flex;
|
|
625
|
+
flex-direction: column;
|
|
626
|
+
gap: 12px;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.perm-actions {
|
|
630
|
+
display: flex;
|
|
631
|
+
gap: 8px;
|
|
632
|
+
margin-bottom: 8px;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.btn-outline {
|
|
636
|
+
padding: 7px 14px;
|
|
637
|
+
background: transparent;
|
|
638
|
+
border: 1px solid var(--border);
|
|
639
|
+
border-radius: var(--radius);
|
|
640
|
+
color: var(--text-secondary);
|
|
641
|
+
font-size: 12px;
|
|
642
|
+
font-family: inherit;
|
|
643
|
+
cursor: pointer;
|
|
644
|
+
transition: all var(--transition);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.btn-outline:hover { border-color: var(--text-secondary); color: var(--text); }
|
|
648
|
+
|
|
649
|
+
.perm-item {
|
|
650
|
+
display: flex;
|
|
651
|
+
align-items: center;
|
|
652
|
+
justify-content: space-between;
|
|
653
|
+
padding: 10px 14px;
|
|
654
|
+
background: var(--surface-2);
|
|
655
|
+
border-radius: var(--radius);
|
|
656
|
+
transition: background var(--transition);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.perm-item:hover { background: var(--surface-3); }
|
|
660
|
+
|
|
661
|
+
.perm-label .scope-name {
|
|
662
|
+
font-size: 13px;
|
|
663
|
+
font-weight: 500;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.perm-label .scope-desc {
|
|
667
|
+
font-size: 11px;
|
|
668
|
+
color: var(--text-muted);
|
|
669
|
+
margin-top: 1px;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* Toggle Switch */
|
|
673
|
+
.toggle {
|
|
674
|
+
position: relative;
|
|
675
|
+
width: 40px;
|
|
676
|
+
height: 22px;
|
|
677
|
+
flex-shrink: 0;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.toggle input { display: none; }
|
|
681
|
+
|
|
682
|
+
.toggle-slider {
|
|
683
|
+
position: absolute;
|
|
684
|
+
inset: 0;
|
|
685
|
+
background: var(--border);
|
|
686
|
+
border-radius: 11px;
|
|
687
|
+
cursor: pointer;
|
|
688
|
+
transition: background 200ms ease;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.toggle-slider::after {
|
|
692
|
+
content: '';
|
|
693
|
+
position: absolute;
|
|
694
|
+
width: 16px;
|
|
695
|
+
height: 16px;
|
|
696
|
+
border-radius: 50%;
|
|
697
|
+
background: var(--white);
|
|
698
|
+
top: 3px;
|
|
699
|
+
left: 3px;
|
|
700
|
+
transition: transform 200ms ease;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.toggle input:checked + .toggle-slider {
|
|
704
|
+
background: var(--red);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.toggle input:checked + .toggle-slider::after {
|
|
708
|
+
transform: translateX(18px);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* ---- Qualification Tab ---- */
|
|
712
|
+
.qual-content {
|
|
713
|
+
padding: 24px;
|
|
714
|
+
display: flex;
|
|
715
|
+
flex-direction: column;
|
|
716
|
+
gap: 24px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.score-gauge {
|
|
720
|
+
display: flex;
|
|
721
|
+
flex-direction: column;
|
|
722
|
+
align-items: center;
|
|
723
|
+
gap: 8px;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.gauge-svg { width: 160px; height: 100px; }
|
|
727
|
+
|
|
728
|
+
.gauge-label {
|
|
729
|
+
font-size: 28px;
|
|
730
|
+
font-weight: 700;
|
|
731
|
+
letter-spacing: -1px;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.gauge-sub {
|
|
735
|
+
font-size: 12px;
|
|
736
|
+
color: var(--text-muted);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/* Pipeline Visualization */
|
|
740
|
+
.pipeline-vis {
|
|
741
|
+
display: flex;
|
|
742
|
+
align-items: center;
|
|
743
|
+
gap: 0;
|
|
744
|
+
padding: 16px 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.pipeline-stage {
|
|
748
|
+
display: flex;
|
|
749
|
+
flex-direction: column;
|
|
750
|
+
align-items: center;
|
|
751
|
+
gap: 6px;
|
|
752
|
+
flex: 1;
|
|
753
|
+
position: relative;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.pipeline-stage::before {
|
|
757
|
+
content: '';
|
|
758
|
+
position: absolute;
|
|
759
|
+
top: 10px;
|
|
760
|
+
left: -50%;
|
|
761
|
+
right: 50%;
|
|
762
|
+
height: 2px;
|
|
763
|
+
background: var(--border);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.pipeline-stage:first-child::before { display: none; }
|
|
767
|
+
|
|
768
|
+
.pipeline-dot {
|
|
769
|
+
width: 22px;
|
|
770
|
+
height: 22px;
|
|
771
|
+
border-radius: 50%;
|
|
772
|
+
background: var(--surface-3);
|
|
773
|
+
border: 2px solid var(--border);
|
|
774
|
+
position: relative;
|
|
775
|
+
z-index: 1;
|
|
776
|
+
transition: all 300ms ease;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.pipeline-dot.reached {
|
|
780
|
+
border-color: transparent;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.pipeline-dot.current {
|
|
784
|
+
box-shadow: 0 0 0 4px var(--red-dim);
|
|
785
|
+
transform: scale(1.15);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.pipeline-stage-label {
|
|
789
|
+
font-size: 10px;
|
|
790
|
+
color: var(--text-muted);
|
|
791
|
+
text-transform: uppercase;
|
|
792
|
+
letter-spacing: 0.3px;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.pipeline-stage-label.current { color: var(--text); font-weight: 600; }
|
|
796
|
+
|
|
797
|
+
/* Signals list */
|
|
798
|
+
.signals-list {
|
|
799
|
+
display: flex;
|
|
800
|
+
flex-direction: column;
|
|
801
|
+
gap: 8px;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.signal-item {
|
|
805
|
+
display: flex;
|
|
806
|
+
align-items: center;
|
|
807
|
+
gap: 10px;
|
|
808
|
+
padding: 8px 12px;
|
|
809
|
+
background: var(--surface-2);
|
|
810
|
+
border-radius: var(--radius);
|
|
811
|
+
font-size: 13px;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.signal-dot {
|
|
815
|
+
width: 6px;
|
|
816
|
+
height: 6px;
|
|
817
|
+
border-radius: 50%;
|
|
818
|
+
background: var(--stage-qualified);
|
|
819
|
+
flex-shrink: 0;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.signal-points {
|
|
823
|
+
margin-left: auto;
|
|
824
|
+
font-size: 12px;
|
|
825
|
+
color: var(--text-muted);
|
|
826
|
+
font-weight: 500;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/* ---- Context Tab ---- */
|
|
830
|
+
.context-content {
|
|
831
|
+
padding: 24px;
|
|
832
|
+
display: flex;
|
|
833
|
+
flex-direction: column;
|
|
834
|
+
gap: 16px;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.context-card {
|
|
838
|
+
background: var(--surface-2);
|
|
839
|
+
border-radius: var(--radius);
|
|
840
|
+
padding: 14px 16px;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.context-card h4 {
|
|
844
|
+
font-size: 11px;
|
|
845
|
+
font-weight: 600;
|
|
846
|
+
color: var(--text-muted);
|
|
847
|
+
text-transform: uppercase;
|
|
848
|
+
letter-spacing: 0.5px;
|
|
849
|
+
margin-bottom: 8px;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.context-card p {
|
|
853
|
+
font-size: 13px;
|
|
854
|
+
line-height: 1.6;
|
|
855
|
+
color: var(--text-secondary);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.context-card .items-list {
|
|
859
|
+
list-style: none;
|
|
860
|
+
display: flex;
|
|
861
|
+
flex-direction: column;
|
|
862
|
+
gap: 4px;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.context-card .items-list li {
|
|
866
|
+
font-size: 13px;
|
|
867
|
+
color: var(--text-secondary);
|
|
868
|
+
padding-left: 12px;
|
|
869
|
+
position: relative;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.context-card .items-list li::before {
|
|
873
|
+
content: '';
|
|
874
|
+
width: 4px;
|
|
875
|
+
height: 4px;
|
|
876
|
+
border-radius: 50%;
|
|
877
|
+
background: var(--text-muted);
|
|
878
|
+
position: absolute;
|
|
879
|
+
left: 0;
|
|
880
|
+
top: 8px;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/* ---- Top-level Pages (FAQ, Playbook, Settings) ---- */
|
|
884
|
+
.page-panel {
|
|
885
|
+
flex: 1;
|
|
886
|
+
overflow-y: auto;
|
|
887
|
+
display: none;
|
|
888
|
+
flex-direction: column;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.page-panel.active { display: flex; }
|
|
892
|
+
|
|
893
|
+
.page-header {
|
|
894
|
+
padding: 24px 32px 16px;
|
|
895
|
+
border-bottom: 1px solid var(--border);
|
|
896
|
+
background: var(--surface);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.page-header h2 {
|
|
900
|
+
font-size: 20px;
|
|
901
|
+
font-weight: 600;
|
|
902
|
+
letter-spacing: -0.3px;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.page-header p {
|
|
906
|
+
font-size: 13px;
|
|
907
|
+
color: var(--text-muted);
|
|
908
|
+
margin-top: 4px;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.page-body {
|
|
912
|
+
padding: 24px 32px;
|
|
913
|
+
display: flex;
|
|
914
|
+
flex-direction: column;
|
|
915
|
+
gap: 16px;
|
|
916
|
+
flex: 1;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/* FAQ Items */
|
|
920
|
+
.faq-toolbar {
|
|
921
|
+
display: flex;
|
|
922
|
+
gap: 10px;
|
|
923
|
+
align-items: center;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.faq-toolbar input {
|
|
927
|
+
flex: 1;
|
|
928
|
+
padding: 8px 12px;
|
|
929
|
+
background: var(--surface-2);
|
|
930
|
+
border: 1px solid var(--border);
|
|
931
|
+
border-radius: var(--radius);
|
|
932
|
+
color: var(--text);
|
|
933
|
+
font-size: 13px;
|
|
934
|
+
font-family: inherit;
|
|
935
|
+
outline: none;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.faq-toolbar input:focus { border-color: var(--red); }
|
|
939
|
+
|
|
940
|
+
.btn-primary {
|
|
941
|
+
padding: 8px 16px;
|
|
942
|
+
background: var(--red);
|
|
943
|
+
border: none;
|
|
944
|
+
border-radius: var(--radius);
|
|
945
|
+
color: var(--white);
|
|
946
|
+
font-size: 13px;
|
|
947
|
+
font-weight: 500;
|
|
948
|
+
font-family: inherit;
|
|
949
|
+
cursor: pointer;
|
|
950
|
+
transition: opacity var(--transition);
|
|
951
|
+
white-space: nowrap;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.btn-primary:hover { opacity: 0.9; }
|
|
955
|
+
|
|
956
|
+
.faq-item {
|
|
957
|
+
background: var(--surface-2);
|
|
958
|
+
border-radius: var(--radius);
|
|
959
|
+
padding: 16px;
|
|
960
|
+
display: flex;
|
|
961
|
+
flex-direction: column;
|
|
962
|
+
gap: 8px;
|
|
963
|
+
transition: background var(--transition);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.faq-item:hover { background: var(--surface-3); }
|
|
967
|
+
|
|
968
|
+
.faq-item .faq-q {
|
|
969
|
+
font-size: 14px;
|
|
970
|
+
font-weight: 500;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.faq-item .faq-a {
|
|
974
|
+
font-size: 13px;
|
|
975
|
+
color: var(--text-secondary);
|
|
976
|
+
line-height: 1.5;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.faq-item .faq-meta {
|
|
980
|
+
display: flex;
|
|
981
|
+
align-items: center;
|
|
982
|
+
gap: 8px;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.faq-item .faq-cat {
|
|
986
|
+
font-size: 10px;
|
|
987
|
+
font-weight: 600;
|
|
988
|
+
padding: 2px 8px;
|
|
989
|
+
border-radius: 8px;
|
|
990
|
+
background: var(--surface);
|
|
991
|
+
color: var(--text-muted);
|
|
992
|
+
text-transform: uppercase;
|
|
993
|
+
letter-spacing: 0.3px;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.faq-item .faq-actions {
|
|
997
|
+
margin-left: auto;
|
|
998
|
+
display: flex;
|
|
999
|
+
gap: 6px;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.btn-icon {
|
|
1003
|
+
width: 28px;
|
|
1004
|
+
height: 28px;
|
|
1005
|
+
background: transparent;
|
|
1006
|
+
border: 1px solid var(--border);
|
|
1007
|
+
border-radius: 6px;
|
|
1008
|
+
color: var(--text-muted);
|
|
1009
|
+
cursor: pointer;
|
|
1010
|
+
display: flex;
|
|
1011
|
+
align-items: center;
|
|
1012
|
+
justify-content: center;
|
|
1013
|
+
transition: all var(--transition);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.btn-icon:hover { border-color: var(--text-secondary); color: var(--text); }
|
|
1017
|
+
.btn-icon.danger:hover { border-color: var(--cat-blocked); color: var(--cat-blocked); }
|
|
1018
|
+
.btn-icon svg { width: 14px; height: 14px; }
|
|
1019
|
+
|
|
1020
|
+
/* Playbook */
|
|
1021
|
+
.playbook-section {
|
|
1022
|
+
background: var(--surface-2);
|
|
1023
|
+
border-radius: var(--radius);
|
|
1024
|
+
padding: 20px;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.playbook-section h3 {
|
|
1028
|
+
font-size: 14px;
|
|
1029
|
+
font-weight: 600;
|
|
1030
|
+
margin-bottom: 10px;
|
|
1031
|
+
display: flex;
|
|
1032
|
+
align-items: center;
|
|
1033
|
+
gap: 8px;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.playbook-section textarea {
|
|
1037
|
+
width: 100%;
|
|
1038
|
+
min-height: 120px;
|
|
1039
|
+
padding: 12px;
|
|
1040
|
+
background: var(--surface);
|
|
1041
|
+
border: 1px solid var(--border);
|
|
1042
|
+
border-radius: var(--radius);
|
|
1043
|
+
color: var(--text);
|
|
1044
|
+
font-size: 13px;
|
|
1045
|
+
font-family: inherit;
|
|
1046
|
+
line-height: 1.6;
|
|
1047
|
+
resize: vertical;
|
|
1048
|
+
outline: none;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.playbook-section textarea:focus { border-color: var(--red); }
|
|
1052
|
+
|
|
1053
|
+
/* Global Settings */
|
|
1054
|
+
.settings-grid {
|
|
1055
|
+
display: grid;
|
|
1056
|
+
grid-template-columns: 1fr 1fr;
|
|
1057
|
+
gap: 16px;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
@media (max-width: 900px) {
|
|
1061
|
+
.settings-grid { grid-template-columns: 1fr; }
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.settings-card {
|
|
1065
|
+
background: var(--surface-2);
|
|
1066
|
+
border-radius: var(--radius);
|
|
1067
|
+
padding: 20px;
|
|
1068
|
+
display: flex;
|
|
1069
|
+
flex-direction: column;
|
|
1070
|
+
gap: 14px;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.settings-card h3 {
|
|
1074
|
+
font-size: 14px;
|
|
1075
|
+
font-weight: 600;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/* Modal */
|
|
1079
|
+
.modal-backdrop {
|
|
1080
|
+
position: fixed;
|
|
1081
|
+
inset: 0;
|
|
1082
|
+
background: rgba(0,0,0,0.6);
|
|
1083
|
+
backdrop-filter: blur(4px);
|
|
1084
|
+
z-index: 1000;
|
|
1085
|
+
display: none;
|
|
1086
|
+
align-items: center;
|
|
1087
|
+
justify-content: center;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.modal-backdrop.open { display: flex; }
|
|
1091
|
+
|
|
1092
|
+
.modal {
|
|
1093
|
+
background: var(--surface);
|
|
1094
|
+
border: 1px solid var(--border);
|
|
1095
|
+
border-radius: var(--radius-lg);
|
|
1096
|
+
width: 480px;
|
|
1097
|
+
max-width: 90vw;
|
|
1098
|
+
max-height: 80vh;
|
|
1099
|
+
overflow-y: auto;
|
|
1100
|
+
padding: 24px;
|
|
1101
|
+
animation: modalIn 200ms ease;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
@keyframes modalIn {
|
|
1105
|
+
from { opacity: 0; transform: scale(0.97) translateY(8px); }
|
|
1106
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.modal h3 {
|
|
1110
|
+
font-size: 16px;
|
|
1111
|
+
font-weight: 600;
|
|
1112
|
+
margin-bottom: 16px;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
.modal-actions {
|
|
1116
|
+
display: flex;
|
|
1117
|
+
gap: 8px;
|
|
1118
|
+
justify-content: flex-end;
|
|
1119
|
+
margin-top: 20px;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.btn-ghost {
|
|
1123
|
+
padding: 8px 16px;
|
|
1124
|
+
background: transparent;
|
|
1125
|
+
border: 1px solid var(--border);
|
|
1126
|
+
border-radius: var(--radius);
|
|
1127
|
+
color: var(--text-secondary);
|
|
1128
|
+
font-size: 13px;
|
|
1129
|
+
font-family: inherit;
|
|
1130
|
+
cursor: pointer;
|
|
1131
|
+
transition: all var(--transition);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
.btn-ghost:hover { border-color: var(--text-secondary); color: var(--text); }
|
|
1135
|
+
|
|
1136
|
+
/* Loading / Toast */
|
|
1137
|
+
.loading-spinner {
|
|
1138
|
+
width: 24px;
|
|
1139
|
+
height: 24px;
|
|
1140
|
+
border: 2px solid var(--border);
|
|
1141
|
+
border-top-color: var(--red);
|
|
1142
|
+
border-radius: 50%;
|
|
1143
|
+
animation: spin 600ms linear infinite;
|
|
1144
|
+
margin: 40px auto;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1148
|
+
|
|
1149
|
+
.toast {
|
|
1150
|
+
position: fixed;
|
|
1151
|
+
bottom: 24px;
|
|
1152
|
+
right: 24px;
|
|
1153
|
+
padding: 12px 20px;
|
|
1154
|
+
background: var(--surface-2);
|
|
1155
|
+
border: 1px solid var(--border);
|
|
1156
|
+
border-radius: var(--radius);
|
|
1157
|
+
font-size: 13px;
|
|
1158
|
+
color: var(--text);
|
|
1159
|
+
z-index: 2000;
|
|
1160
|
+
animation: toastIn 300ms ease;
|
|
1161
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
@keyframes toastIn {
|
|
1165
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
1166
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.toast.error { border-color: var(--cat-blocked); }
|
|
1170
|
+
|
|
1171
|
+
/* Stats bar at top of contact panel */
|
|
1172
|
+
.stats-bar {
|
|
1173
|
+
display: flex;
|
|
1174
|
+
gap: 12px;
|
|
1175
|
+
padding: 12px 16px;
|
|
1176
|
+
border-bottom: 1px solid var(--border);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
.stat-chip {
|
|
1180
|
+
display: flex;
|
|
1181
|
+
align-items: center;
|
|
1182
|
+
gap: 4px;
|
|
1183
|
+
font-size: 11px;
|
|
1184
|
+
color: var(--text-muted);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
.stat-chip .stat-num {
|
|
1188
|
+
font-weight: 600;
|
|
1189
|
+
color: var(--text-secondary);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/* ---- Escalation Queue ---- */
|
|
1193
|
+
.escalation-list {
|
|
1194
|
+
padding: 16px;
|
|
1195
|
+
display: flex;
|
|
1196
|
+
flex-direction: column;
|
|
1197
|
+
gap: 12px;
|
|
1198
|
+
flex: 1;
|
|
1199
|
+
overflow-y: auto;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.escalation-empty {
|
|
1203
|
+
flex: 1;
|
|
1204
|
+
display: flex;
|
|
1205
|
+
flex-direction: column;
|
|
1206
|
+
align-items: center;
|
|
1207
|
+
justify-content: center;
|
|
1208
|
+
gap: 12px;
|
|
1209
|
+
color: var(--text-muted);
|
|
1210
|
+
padding: 60px 0;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.escalation-empty svg { width: 40px; height: 40px; opacity: 0.3; }
|
|
1214
|
+
|
|
1215
|
+
.escalation-card {
|
|
1216
|
+
background: var(--surface-2);
|
|
1217
|
+
border-radius: var(--radius);
|
|
1218
|
+
padding: 16px;
|
|
1219
|
+
border-left: 3px solid var(--text-muted);
|
|
1220
|
+
display: flex;
|
|
1221
|
+
flex-direction: column;
|
|
1222
|
+
gap: 10px;
|
|
1223
|
+
transition: background var(--transition);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
.escalation-card:hover { background: var(--surface-3); }
|
|
1227
|
+
.escalation-card.urgency-low { border-left-color: var(--stage-qualified); }
|
|
1228
|
+
.escalation-card.urgency-medium { border-left-color: var(--stage-warm); }
|
|
1229
|
+
.escalation-card.urgency-high { border-left-color: var(--stage-hot); }
|
|
1230
|
+
.escalation-card.urgency-critical { border-left-color: var(--cat-blocked); }
|
|
1231
|
+
|
|
1232
|
+
.esc-header {
|
|
1233
|
+
display: flex;
|
|
1234
|
+
align-items: center;
|
|
1235
|
+
gap: 10px;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.esc-name {
|
|
1239
|
+
font-size: 14px;
|
|
1240
|
+
font-weight: 600;
|
|
1241
|
+
flex: 1;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.esc-category {
|
|
1245
|
+
font-size: 10px;
|
|
1246
|
+
font-weight: 600;
|
|
1247
|
+
padding: 2px 8px;
|
|
1248
|
+
border-radius: 8px;
|
|
1249
|
+
background: var(--surface);
|
|
1250
|
+
color: var(--text-muted);
|
|
1251
|
+
text-transform: uppercase;
|
|
1252
|
+
letter-spacing: 0.3px;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
.esc-timer {
|
|
1256
|
+
font-size: 11px;
|
|
1257
|
+
font-weight: 600;
|
|
1258
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.esc-timer.urgency-low { color: var(--stage-qualified); }
|
|
1262
|
+
.esc-timer.urgency-medium { color: var(--stage-warm); }
|
|
1263
|
+
.esc-timer.urgency-high { color: var(--stage-hot); }
|
|
1264
|
+
.esc-timer.urgency-critical { color: var(--cat-blocked); }
|
|
1265
|
+
|
|
1266
|
+
.esc-message {
|
|
1267
|
+
font-size: 13px;
|
|
1268
|
+
color: var(--text-secondary);
|
|
1269
|
+
line-height: 1.5;
|
|
1270
|
+
background: var(--surface);
|
|
1271
|
+
padding: 8px 12px;
|
|
1272
|
+
border-radius: 6px;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
.esc-actions {
|
|
1276
|
+
display: flex;
|
|
1277
|
+
gap: 8px;
|
|
1278
|
+
align-items: flex-end;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.esc-reply-input {
|
|
1282
|
+
flex: 1;
|
|
1283
|
+
padding: 8px 12px;
|
|
1284
|
+
background: var(--surface);
|
|
1285
|
+
border: 1px solid var(--border);
|
|
1286
|
+
border-radius: var(--radius);
|
|
1287
|
+
color: var(--text);
|
|
1288
|
+
font-size: 13px;
|
|
1289
|
+
font-family: inherit;
|
|
1290
|
+
outline: none;
|
|
1291
|
+
transition: border-color var(--transition);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.esc-reply-input:focus { border-color: var(--red); }
|
|
1295
|
+
.esc-reply-input::placeholder { color: var(--text-muted); }
|
|
1296
|
+
|
|
1297
|
+
.btn-approve {
|
|
1298
|
+
padding: 7px 14px;
|
|
1299
|
+
background: rgba(34,197,94,0.15);
|
|
1300
|
+
border: 1px solid rgba(34,197,94,0.3);
|
|
1301
|
+
border-radius: var(--radius);
|
|
1302
|
+
color: var(--stage-qualified);
|
|
1303
|
+
font-size: 12px;
|
|
1304
|
+
font-weight: 500;
|
|
1305
|
+
font-family: inherit;
|
|
1306
|
+
cursor: pointer;
|
|
1307
|
+
transition: all var(--transition);
|
|
1308
|
+
white-space: nowrap;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.btn-approve:hover { background: rgba(34,197,94,0.25); }
|
|
1312
|
+
|
|
1313
|
+
.btn-deny {
|
|
1314
|
+
padding: 7px 14px;
|
|
1315
|
+
background: rgba(239,68,68,0.1);
|
|
1316
|
+
border: 1px solid rgba(239,68,68,0.25);
|
|
1317
|
+
border-radius: var(--radius);
|
|
1318
|
+
color: var(--cat-blocked);
|
|
1319
|
+
font-size: 12px;
|
|
1320
|
+
font-weight: 500;
|
|
1321
|
+
font-family: inherit;
|
|
1322
|
+
cursor: pointer;
|
|
1323
|
+
transition: all var(--transition);
|
|
1324
|
+
white-space: nowrap;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.btn-deny:hover { background: rgba(239,68,68,0.2); }
|
|
1328
|
+
|
|
1329
|
+
.esc-note-row {
|
|
1330
|
+
display: flex;
|
|
1331
|
+
gap: 8px;
|
|
1332
|
+
align-items: center;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
.esc-note-input {
|
|
1336
|
+
flex: 1;
|
|
1337
|
+
padding: 6px 10px;
|
|
1338
|
+
background: var(--surface);
|
|
1339
|
+
border: 1px solid var(--border);
|
|
1340
|
+
border-radius: 6px;
|
|
1341
|
+
color: var(--text);
|
|
1342
|
+
font-size: 12px;
|
|
1343
|
+
font-family: inherit;
|
|
1344
|
+
outline: none;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.esc-note-input::placeholder { color: var(--text-muted); }
|
|
1348
|
+
.esc-note-input:focus { border-color: var(--red); }
|
|
1349
|
+
|
|
1350
|
+
.sidebar-nav .nav-btn .badge {
|
|
1351
|
+
position: absolute;
|
|
1352
|
+
top: 4px;
|
|
1353
|
+
right: 4px;
|
|
1354
|
+
min-width: 16px;
|
|
1355
|
+
height: 16px;
|
|
1356
|
+
padding: 0 4px;
|
|
1357
|
+
background: var(--red);
|
|
1358
|
+
color: var(--white);
|
|
1359
|
+
font-size: 9px;
|
|
1360
|
+
font-weight: 700;
|
|
1361
|
+
border-radius: 8px;
|
|
1362
|
+
display: flex;
|
|
1363
|
+
align-items: center;
|
|
1364
|
+
justify-content: center;
|
|
1365
|
+
line-height: 1;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/* ---- Mobile Back Button ---- */
|
|
1369
|
+
.mobile-back-btn {
|
|
1370
|
+
display: none;
|
|
1371
|
+
width: 36px;
|
|
1372
|
+
height: 36px;
|
|
1373
|
+
border: none;
|
|
1374
|
+
background: var(--surface-2);
|
|
1375
|
+
border-radius: var(--radius);
|
|
1376
|
+
color: var(--text-secondary);
|
|
1377
|
+
cursor: pointer;
|
|
1378
|
+
align-items: center;
|
|
1379
|
+
justify-content: center;
|
|
1380
|
+
flex-shrink: 0;
|
|
1381
|
+
transition: all var(--transition);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
.mobile-back-btn:hover { background: var(--surface-3); color: var(--text); }
|
|
1385
|
+
.mobile-back-btn svg { width: 18px; height: 18px; }
|
|
1386
|
+
|
|
1387
|
+
@media (max-width: 768px) {
|
|
1388
|
+
.mobile-back-btn { display: flex; }
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/* Responsive */
|
|
1392
|
+
@media (max-width: 768px) {
|
|
1393
|
+
.contact-panel { width: 100%; }
|
|
1394
|
+
.detail-panel { display: none; }
|
|
1395
|
+
.detail-panel.mobile-show { display: flex; position: fixed; inset: 0; z-index: 100; }
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/* Embedded mode - hide sidebar when inside iframe */
|
|
1399
|
+
.embedded .sidebar-nav { display: none; }
|
|
1400
|
+
.embedded .contact-panel { width: 100%; }
|
|
1401
|
+
.embedded .detail-panel { display: none; }
|
|
1402
|
+
.embedded .detail-panel.mobile-show { display: flex; position: fixed; inset: 0; z-index: 100; }
|
|
1403
|
+
.embedded .page-content { display: none; }
|
|
1404
|
+
.embedded .page-content.active { display: flex; flex-direction: column; flex: 1; overflow: auto; }
|
|
1405
|
+
.embedded .faq-page,
|
|
1406
|
+
.embedded .playbook-page,
|
|
1407
|
+
.embedded .settings-page,
|
|
1408
|
+
.embedded .escalations-page { padding: 16px; }
|
|
1409
|
+
</style>
|
|
1410
|
+
</head>
|
|
1411
|
+
<body>
|
|
1412
|
+
|
|
1413
|
+
<div class="app">
|
|
1414
|
+
<!-- Sidebar Navigation -->
|
|
1415
|
+
<nav class="sidebar-nav">
|
|
1416
|
+
<button class="nav-btn active" onclick="showPage('contacts')" title="Contacts">
|
|
1417
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
1418
|
+
</button>
|
|
1419
|
+
<button class="nav-btn" onclick="showPage('escalations')" title="Escalations" id="navEscalations">
|
|
1420
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
|
|
1421
|
+
<span class="badge" id="escBadge" style="display:none;">0</span>
|
|
1422
|
+
</button>
|
|
1423
|
+
<button class="nav-btn" onclick="showPage('faq')" title="FAQ">
|
|
1424
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
1425
|
+
</button>
|
|
1426
|
+
<button class="nav-btn" onclick="showPage('playbook')" title="Playbook">
|
|
1427
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
|
1428
|
+
</button>
|
|
1429
|
+
<button class="nav-btn" onclick="showPage('settings')" title="Settings">
|
|
1430
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
1431
|
+
</button>
|
|
1432
|
+
<div class="nav-spacer"></div>
|
|
1433
|
+
</nav>
|
|
1434
|
+
|
|
1435
|
+
<!-- Contact List Panel -->
|
|
1436
|
+
<div class="contact-panel" id="contactPanel">
|
|
1437
|
+
<div class="contact-panel-header">
|
|
1438
|
+
<h2>Messages</h2>
|
|
1439
|
+
<div class="search-bar">
|
|
1440
|
+
<svg class="search-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
|
1441
|
+
<input type="text" id="searchInput" placeholder="Search contacts..." oninput="filterContacts()">
|
|
1442
|
+
</div>
|
|
1443
|
+
<div class="filter-row">
|
|
1444
|
+
<select class="filter-select" id="filterCategory" onchange="filterContacts()">
|
|
1445
|
+
<option value="">All Categories</option>
|
|
1446
|
+
<option value="lead">Lead</option>
|
|
1447
|
+
<option value="qualified-lead">Qualified Lead</option>
|
|
1448
|
+
<option value="client">Client</option>
|
|
1449
|
+
<option value="family">Family</option>
|
|
1450
|
+
<option value="friend">Friend</option>
|
|
1451
|
+
<option value="team">Team</option>
|
|
1452
|
+
<option value="unknown">Unknown</option>
|
|
1453
|
+
<option value="blocked">Blocked</option>
|
|
1454
|
+
</select>
|
|
1455
|
+
<select class="filter-select" id="filterPipeline" onchange="filterContacts()">
|
|
1456
|
+
<option value="">All Stages</option>
|
|
1457
|
+
<option value="cold">Cold</option>
|
|
1458
|
+
<option value="warm">Warm</option>
|
|
1459
|
+
<option value="hot">Hot</option>
|
|
1460
|
+
<option value="qualified">Qualified</option>
|
|
1461
|
+
<option value="client">Client</option>
|
|
1462
|
+
</select>
|
|
1463
|
+
<select class="filter-select" id="sortBy" onchange="filterContacts()">
|
|
1464
|
+
<option value="lastInteraction">Recent</option>
|
|
1465
|
+
<option value="score">Score</option>
|
|
1466
|
+
<option value="name">Name</option>
|
|
1467
|
+
</select>
|
|
1468
|
+
</div>
|
|
1469
|
+
</div>
|
|
1470
|
+
<div class="stats-bar" id="statsBar">
|
|
1471
|
+
<div class="stat-chip"><span class="stat-num" id="statTotal">0</span> contacts</div>
|
|
1472
|
+
<div class="stat-chip"><span class="stat-num" id="statLeads">0</span> leads</div>
|
|
1473
|
+
<div class="stat-chip"><span class="stat-num" id="statClients">0</span> clients</div>
|
|
1474
|
+
</div>
|
|
1475
|
+
<div class="contact-list" id="contactList">
|
|
1476
|
+
<div class="loading-spinner"></div>
|
|
1477
|
+
</div>
|
|
1478
|
+
</div>
|
|
1479
|
+
|
|
1480
|
+
<!-- Detail / Page Panel -->
|
|
1481
|
+
<div class="detail-panel" id="detailPanel">
|
|
1482
|
+
<!-- Empty State -->
|
|
1483
|
+
<div class="empty-state" id="emptyState">
|
|
1484
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
|
1485
|
+
<p>Select a contact to view conversation</p>
|
|
1486
|
+
</div>
|
|
1487
|
+
|
|
1488
|
+
<!-- Contact Detail View -->
|
|
1489
|
+
<div id="contactDetail" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
|
|
1490
|
+
<div class="detail-header" id="detailHeader"></div>
|
|
1491
|
+
<div class="tabs-bar" id="tabsBar"></div>
|
|
1492
|
+
|
|
1493
|
+
<!-- Conversation Tab -->
|
|
1494
|
+
<div class="tab-content active" id="tabConversation">
|
|
1495
|
+
<div class="messages-container" id="messagesContainer"></div>
|
|
1496
|
+
<div class="message-input-bar">
|
|
1497
|
+
<input type="text" id="messageInput" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendMessage()">
|
|
1498
|
+
<button class="btn-send" onclick="sendMessage()">Send</button>
|
|
1499
|
+
</div>
|
|
1500
|
+
</div>
|
|
1501
|
+
|
|
1502
|
+
<!-- Settings Tab -->
|
|
1503
|
+
<div class="tab-content" id="tabSettings">
|
|
1504
|
+
<div class="settings-form" id="settingsForm"></div>
|
|
1505
|
+
</div>
|
|
1506
|
+
|
|
1507
|
+
<!-- Permissions Tab -->
|
|
1508
|
+
<div class="tab-content" id="tabPermissions">
|
|
1509
|
+
<div class="permissions-grid" id="permissionsGrid"></div>
|
|
1510
|
+
</div>
|
|
1511
|
+
|
|
1512
|
+
<!-- Qualification Tab -->
|
|
1513
|
+
<div class="tab-content" id="tabQualification">
|
|
1514
|
+
<div class="qual-content" id="qualContent"></div>
|
|
1515
|
+
</div>
|
|
1516
|
+
|
|
1517
|
+
<!-- Context Tab -->
|
|
1518
|
+
<div class="tab-content" id="tabContext">
|
|
1519
|
+
<div class="context-content" id="contextContent"></div>
|
|
1520
|
+
</div>
|
|
1521
|
+
</div>
|
|
1522
|
+
|
|
1523
|
+
<!-- Escalation Queue Page -->
|
|
1524
|
+
<div class="page-panel" id="pageEscalations">
|
|
1525
|
+
<div class="page-header">
|
|
1526
|
+
<h2>Escalation Queue</h2>
|
|
1527
|
+
<p>Pending escalations that need your attention</p>
|
|
1528
|
+
</div>
|
|
1529
|
+
<div class="escalation-list" id="escalationList">
|
|
1530
|
+
<div class="loading-spinner"></div>
|
|
1531
|
+
</div>
|
|
1532
|
+
</div>
|
|
1533
|
+
|
|
1534
|
+
<!-- FAQ Page -->
|
|
1535
|
+
<div class="page-panel" id="pageFaq">
|
|
1536
|
+
<div class="page-header">
|
|
1537
|
+
<h2>Knowledge Base</h2>
|
|
1538
|
+
<p>Manage FAQ entries that the router uses to answer common questions</p>
|
|
1539
|
+
</div>
|
|
1540
|
+
<div class="page-body">
|
|
1541
|
+
<div class="faq-toolbar">
|
|
1542
|
+
<input type="text" id="faqSearch" placeholder="Search FAQ..." oninput="filterFAQ()">
|
|
1543
|
+
<select class="filter-select" id="faqCategoryFilter" onchange="filterFAQ()" style="flex:0 0 auto;width:130px;">
|
|
1544
|
+
<option value="">All Categories</option>
|
|
1545
|
+
<option value="pricing">Pricing</option>
|
|
1546
|
+
<option value="hours">Hours</option>
|
|
1547
|
+
<option value="services">Services</option>
|
|
1548
|
+
<option value="general">General</option>
|
|
1549
|
+
</select>
|
|
1550
|
+
<button class="btn-primary" onclick="openFaqModal()">+ Add Entry</button>
|
|
1551
|
+
</div>
|
|
1552
|
+
<div id="faqList"></div>
|
|
1553
|
+
</div>
|
|
1554
|
+
</div>
|
|
1555
|
+
|
|
1556
|
+
<!-- Playbook Page -->
|
|
1557
|
+
<div class="page-panel" id="pagePlaybook">
|
|
1558
|
+
<div class="page-header">
|
|
1559
|
+
<h2>Business Playbook</h2>
|
|
1560
|
+
<p>Train the router on your brand voice, services, and conversation strategy</p>
|
|
1561
|
+
</div>
|
|
1562
|
+
<div class="page-body" id="playbookBody"></div>
|
|
1563
|
+
</div>
|
|
1564
|
+
|
|
1565
|
+
<!-- Settings Page -->
|
|
1566
|
+
<div class="page-panel" id="pageSettings">
|
|
1567
|
+
<div class="page-header">
|
|
1568
|
+
<h2>Global Settings</h2>
|
|
1569
|
+
<p>Configure router behavior, follow-ups, and response defaults</p>
|
|
1570
|
+
</div>
|
|
1571
|
+
<div class="page-body">
|
|
1572
|
+
<div class="settings-grid" id="globalSettingsGrid"></div>
|
|
1573
|
+
<button class="btn-save" onclick="saveGlobalSettings()" style="margin-top:8px;">Save Settings</button>
|
|
1574
|
+
</div>
|
|
1575
|
+
</div>
|
|
1576
|
+
</div>
|
|
1577
|
+
</div>
|
|
1578
|
+
|
|
1579
|
+
<!-- FAQ Modal -->
|
|
1580
|
+
<div class="modal-backdrop" id="faqModal">
|
|
1581
|
+
<div class="modal">
|
|
1582
|
+
<h3 id="faqModalTitle">Add FAQ Entry</h3>
|
|
1583
|
+
<div class="form-group">
|
|
1584
|
+
<label>Question</label>
|
|
1585
|
+
<input type="text" id="faqQ" placeholder="e.g., What are your business hours?">
|
|
1586
|
+
</div>
|
|
1587
|
+
<div class="form-group" style="margin-top:12px;">
|
|
1588
|
+
<label>Answer</label>
|
|
1589
|
+
<textarea id="faqA" placeholder="The answer that the router will provide..."></textarea>
|
|
1590
|
+
</div>
|
|
1591
|
+
<div class="form-group" style="margin-top:12px;">
|
|
1592
|
+
<label>Category</label>
|
|
1593
|
+
<select id="faqCat">
|
|
1594
|
+
<option value="general">General</option>
|
|
1595
|
+
<option value="pricing">Pricing</option>
|
|
1596
|
+
<option value="hours">Hours</option>
|
|
1597
|
+
<option value="services">Services</option>
|
|
1598
|
+
</select>
|
|
1599
|
+
</div>
|
|
1600
|
+
<div class="modal-actions">
|
|
1601
|
+
<button class="btn-ghost" onclick="closeFaqModal()">Cancel</button>
|
|
1602
|
+
<button class="btn-primary" onclick="saveFaqEntry()">Save</button>
|
|
1603
|
+
</div>
|
|
1604
|
+
</div>
|
|
1605
|
+
</div>
|
|
1606
|
+
|
|
1607
|
+
<script>
|
|
1608
|
+
/* =============================================
|
|
1609
|
+
AIVA Messages v2 - Application Logic
|
|
1610
|
+
============================================= */
|
|
1611
|
+
|
|
1612
|
+
// ---- State ----
|
|
1613
|
+
let contacts = [];
|
|
1614
|
+
let selectedPhone = null;
|
|
1615
|
+
let currentPage = 'contacts';
|
|
1616
|
+
let editingFaqId = null;
|
|
1617
|
+
|
|
1618
|
+
// ---- Placeholder Data ----
|
|
1619
|
+
const PLACEHOLDER_CONTACTS = [
|
|
1620
|
+
{
|
|
1621
|
+
phone: '+15091234567',
|
|
1622
|
+
name: 'Sarah Mitchell',
|
|
1623
|
+
category: 'lead',
|
|
1624
|
+
responseMode: 'auto',
|
|
1625
|
+
style: 'professional',
|
|
1626
|
+
instructions: '',
|
|
1627
|
+
source: 'imessage',
|
|
1628
|
+
qualificationScore: 72,
|
|
1629
|
+
pipelineStage: 'hot',
|
|
1630
|
+
lastMessage: 'I was looking at your website and wanted to learn more about your SEO services.',
|
|
1631
|
+
lastInteraction: new Date(Date.now() - 1800000).toISOString(),
|
|
1632
|
+
context: {
|
|
1633
|
+
relationship: 'Inbound lead from website. Owns a dental practice in Spokane.',
|
|
1634
|
+
lastTopic: 'SEO services inquiry',
|
|
1635
|
+
pendingItems: ['Send case study', 'Schedule discovery call'],
|
|
1636
|
+
conversationSummary: 'Sarah reached out about SEO for her dental practice. Interested in local search optimization. Has budget concerns but seems serious.',
|
|
1637
|
+
preferences: { preferredContact: 'text', timezone: 'PST' }
|
|
1638
|
+
},
|
|
1639
|
+
scopes: ['calendar.view', 'calendar.book', 'info.business', 'messages.auto-reply'],
|
|
1640
|
+
messages: [
|
|
1641
|
+
{ direction: 'inbound', text: 'Hi, I found your company online. Do you do SEO for dental practices?', timestamp: new Date(Date.now() - 7200000).toISOString() },
|
|
1642
|
+
{ direction: 'outbound', text: 'Hey Sarah! Yes, we specialize in local SEO for healthcare and dental practices. We have several dental clients in the Spokane area. What are you looking to improve?', timestamp: new Date(Date.now() - 6800000).toISOString() },
|
|
1643
|
+
{ direction: 'inbound', text: 'Mostly Google Maps rankings. We are on page 2 for most searches.', timestamp: new Date(Date.now() - 3600000).toISOString() },
|
|
1644
|
+
{ direction: 'outbound', text: 'Page 2 is fixable. We typically see dental practices move to the top 3 within 90 days with our local SEO package. Would you be open to a quick 15-minute call to look at your current setup?', timestamp: new Date(Date.now() - 3200000).toISOString() },
|
|
1645
|
+
{ direction: 'inbound', text: 'I was looking at your website and wanted to learn more about your SEO services.', timestamp: new Date(Date.now() - 1800000).toISOString() },
|
|
1646
|
+
],
|
|
1647
|
+
qualification: {
|
|
1648
|
+
score: 72,
|
|
1649
|
+
signals: [
|
|
1650
|
+
{ signal: 'Expressed specific need (local SEO)', points: 20 },
|
|
1651
|
+
{ signal: 'Has existing business (dental practice)', points: 15 },
|
|
1652
|
+
{ signal: 'Mentioned competitor awareness', points: 10 },
|
|
1653
|
+
{ signal: 'Engaged in multi-turn conversation', points: 12 },
|
|
1654
|
+
{ signal: 'Asked about pricing/services', points: 15 },
|
|
1655
|
+
]
|
|
1656
|
+
}
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
phone: '+15099876543',
|
|
1660
|
+
name: 'Jake Torres',
|
|
1661
|
+
category: 'client',
|
|
1662
|
+
responseMode: 'auto',
|
|
1663
|
+
style: 'casual',
|
|
1664
|
+
instructions: 'Jake prefers brief, casual messages. He is on the monthly retainer plan.',
|
|
1665
|
+
source: 'imessage',
|
|
1666
|
+
qualificationScore: 95,
|
|
1667
|
+
pipelineStage: 'client',
|
|
1668
|
+
lastMessage: 'Can you check on the Google Ads performance from last week?',
|
|
1669
|
+
lastInteraction: new Date(Date.now() - 3600000).toISOString(),
|
|
1670
|
+
context: {
|
|
1671
|
+
relationship: 'Active client since Nov 2025. Runs a roofing company.',
|
|
1672
|
+
lastTopic: 'Google Ads performance review',
|
|
1673
|
+
pendingItems: ['Pull weekly ads report'],
|
|
1674
|
+
conversationSummary: 'Long-term client. Monthly retainer for SEO + Google Ads. Generally happy with results.',
|
|
1675
|
+
preferences: { preferredContact: 'text', billing: 'monthly' }
|
|
1676
|
+
},
|
|
1677
|
+
scopes: ['calendar.view', 'calendar.book', 'calendar.modify', 'messages.send', 'messages.auto-reply', 'tasks.create', 'info.business', 'info.personal', 'reminders.create', 'files.send', 'support.technical'],
|
|
1678
|
+
messages: [
|
|
1679
|
+
{ direction: 'inbound', text: 'Can you check on the Google Ads performance from last week?', timestamp: new Date(Date.now() - 3600000).toISOString() },
|
|
1680
|
+
],
|
|
1681
|
+
qualification: { score: 95, signals: [] }
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
phone: '+15095551234',
|
|
1685
|
+
name: 'Mom',
|
|
1686
|
+
category: 'family',
|
|
1687
|
+
responseMode: 'auto',
|
|
1688
|
+
style: 'casual',
|
|
1689
|
+
instructions: '',
|
|
1690
|
+
source: 'imessage',
|
|
1691
|
+
qualificationScore: 0,
|
|
1692
|
+
pipelineStage: 'none',
|
|
1693
|
+
lastMessage: 'Are you coming to dinner Sunday?',
|
|
1694
|
+
lastInteraction: new Date(Date.now() - 86400000).toISOString(),
|
|
1695
|
+
context: {
|
|
1696
|
+
relationship: 'Brandon\'s mother.',
|
|
1697
|
+
lastTopic: 'Sunday dinner plans',
|
|
1698
|
+
pendingItems: [],
|
|
1699
|
+
conversationSummary: 'Regular family check-ins. Usually about weekend plans.',
|
|
1700
|
+
preferences: {}
|
|
1701
|
+
},
|
|
1702
|
+
scopes: ['calendar.view', 'messages.send', 'messages.auto-reply'],
|
|
1703
|
+
messages: [
|
|
1704
|
+
{ direction: 'inbound', text: 'Are you coming to dinner Sunday?', timestamp: new Date(Date.now() - 86400000).toISOString() },
|
|
1705
|
+
],
|
|
1706
|
+
qualification: { score: 0, signals: [] }
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
phone: '+15093217654',
|
|
1710
|
+
name: 'Unknown Caller',
|
|
1711
|
+
category: 'unknown',
|
|
1712
|
+
responseMode: 'escalate',
|
|
1713
|
+
style: 'professional',
|
|
1714
|
+
instructions: '',
|
|
1715
|
+
source: 'whatsapp',
|
|
1716
|
+
qualificationScore: 15,
|
|
1717
|
+
pipelineStage: 'cold',
|
|
1718
|
+
lastMessage: 'Hey is this the marketing company?',
|
|
1719
|
+
lastInteraction: new Date(Date.now() - 172800000).toISOString(),
|
|
1720
|
+
context: {
|
|
1721
|
+
relationship: 'Unknown. First contact via WhatsApp.',
|
|
1722
|
+
lastTopic: 'Initial inquiry',
|
|
1723
|
+
pendingItems: [],
|
|
1724
|
+
conversationSummary: 'Single message asking if this is a marketing company. No follow-up yet.',
|
|
1725
|
+
preferences: {}
|
|
1726
|
+
},
|
|
1727
|
+
scopes: ['info.business', 'messages.auto-reply'],
|
|
1728
|
+
messages: [
|
|
1729
|
+
{ direction: 'inbound', text: 'Hey is this the marketing company?', timestamp: new Date(Date.now() - 172800000).toISOString() },
|
|
1730
|
+
],
|
|
1731
|
+
qualification: {
|
|
1732
|
+
score: 15,
|
|
1733
|
+
signals: [
|
|
1734
|
+
{ signal: 'Initial inquiry received', points: 10 },
|
|
1735
|
+
{ signal: 'Asked about services', points: 5 },
|
|
1736
|
+
]
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
phone: '+15098887777',
|
|
1741
|
+
name: 'Nate Yoder',
|
|
1742
|
+
category: 'team',
|
|
1743
|
+
responseMode: 'monitor',
|
|
1744
|
+
style: 'casual',
|
|
1745
|
+
instructions: 'Business partner. Full access.',
|
|
1746
|
+
source: 'imessage',
|
|
1747
|
+
qualificationScore: 0,
|
|
1748
|
+
pipelineStage: 'none',
|
|
1749
|
+
lastMessage: 'Pushed the new deploy. Check staging when you get a chance.',
|
|
1750
|
+
lastInteraction: new Date(Date.now() - 7200000).toISOString(),
|
|
1751
|
+
context: {
|
|
1752
|
+
relationship: 'Business partner at CMP and YDR Capital.',
|
|
1753
|
+
lastTopic: 'Deployment update',
|
|
1754
|
+
pendingItems: ['Review staging'],
|
|
1755
|
+
conversationSummary: 'Ongoing technical collaboration. Regular deployment and project updates.',
|
|
1756
|
+
preferences: {}
|
|
1757
|
+
},
|
|
1758
|
+
scopes: ['calendar.view', 'calendar.book', 'calendar.modify', 'messages.send', 'messages.auto-reply', 'tasks.create', 'info.business', 'info.personal', 'reminders.create', 'files.send', 'support.technical'],
|
|
1759
|
+
messages: [
|
|
1760
|
+
{ direction: 'inbound', text: 'Pushed the new deploy. Check staging when you get a chance.', timestamp: new Date(Date.now() - 7200000).toISOString() },
|
|
1761
|
+
],
|
|
1762
|
+
qualification: { score: 0, signals: [] }
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
phone: '+15556667777',
|
|
1766
|
+
name: 'Rachel Kim',
|
|
1767
|
+
category: 'qualified-lead',
|
|
1768
|
+
responseMode: 'auto',
|
|
1769
|
+
style: 'professional',
|
|
1770
|
+
instructions: 'High-value prospect. Restaurant chain owner, 4 locations.',
|
|
1771
|
+
source: 'imessage',
|
|
1772
|
+
qualificationScore: 85,
|
|
1773
|
+
pipelineStage: 'qualified',
|
|
1774
|
+
lastMessage: 'Let me talk to my partner and I will get back to you by Friday.',
|
|
1775
|
+
lastInteraction: new Date(Date.now() - 43200000).toISOString(),
|
|
1776
|
+
context: {
|
|
1777
|
+
relationship: 'Qualified lead. Owns 4-location restaurant chain. Met through referral.',
|
|
1778
|
+
lastTopic: 'Proposal review and partner discussion',
|
|
1779
|
+
pendingItems: ['Follow up Friday if no response', 'Send proposal PDF'],
|
|
1780
|
+
conversationSummary: 'Strong prospect. Had discovery call. Sent proposal. Waiting on partner approval. Budget confirmed at $3k/mo.',
|
|
1781
|
+
preferences: { budget: '$3,000/mo', referredBy: 'Jake Torres' }
|
|
1782
|
+
},
|
|
1783
|
+
scopes: ['calendar.view', 'calendar.book', 'info.business', 'messages.auto-reply', 'files.send'],
|
|
1784
|
+
messages: [
|
|
1785
|
+
{ direction: 'outbound', text: 'Hi Rachel, following up on the proposal I sent over. Any questions I can answer?', timestamp: new Date(Date.now() - 50000000).toISOString() },
|
|
1786
|
+
{ direction: 'inbound', text: 'Let me talk to my partner and I will get back to you by Friday.', timestamp: new Date(Date.now() - 43200000).toISOString() },
|
|
1787
|
+
],
|
|
1788
|
+
qualification: {
|
|
1789
|
+
score: 85,
|
|
1790
|
+
signals: [
|
|
1791
|
+
{ signal: 'Budget confirmed ($3k/mo)', points: 25 },
|
|
1792
|
+
{ signal: 'Discovery call completed', points: 20 },
|
|
1793
|
+
{ signal: 'Multiple locations (scale potential)', points: 15 },
|
|
1794
|
+
{ signal: 'Referral from existing client', points: 15 },
|
|
1795
|
+
{ signal: 'Requested proposal', points: 10 },
|
|
1796
|
+
]
|
|
1797
|
+
}
|
|
1798
|
+
},
|
|
1799
|
+
];
|
|
1800
|
+
|
|
1801
|
+
const PLACEHOLDER_FAQ = [
|
|
1802
|
+
{ id: 1, question: 'What are your business hours?', answer: 'We are available Monday through Friday, 9 AM to 5 PM Pacific Time. We often respond to messages outside these hours as well.', category: 'hours' },
|
|
1803
|
+
{ id: 2, question: 'How much does SEO cost?', answer: 'Our SEO packages start at $1,500/month for local SEO and go up to $5,000/month for comprehensive national campaigns. We will put together a custom quote after a discovery call.', category: 'pricing' },
|
|
1804
|
+
{ id: 3, question: 'What services do you offer?', answer: 'We offer SEO (local and national), Google Ads management, Meta Ads, website design, and conversion rate optimization.', category: 'services' },
|
|
1805
|
+
{ id: 4, question: 'Do you offer contracts?', answer: 'We work on month-to-month agreements. No long-term contracts required, though we recommend a minimum 3-month commitment for SEO to see meaningful results.', category: 'general' },
|
|
1806
|
+
];
|
|
1807
|
+
|
|
1808
|
+
const PLACEHOLDER_PLAYBOOK = {
|
|
1809
|
+
brandVoice: 'Professional but approachable. We speak like a trusted advisor, not a salesman. Use clear, jargon-free language. Be direct about what we can and cannot do. Never make promises we cannot keep.',
|
|
1810
|
+
services: 'Core services: Local SEO, National SEO, Google Ads Management, Meta Ads, Website Design, CRO. We specialize in service-based businesses: dental, legal, HVAC, roofing, restaurants. Average client ROI is 3-5x within 6 months.',
|
|
1811
|
+
qualifyingStrategies: 'Ask about their current marketing spend. Understand their timeline and urgency. Identify decision-makers early. Look for budget signals ($1,500+ monthly is our minimum). Prioritize businesses with existing revenue over startups. Referrals from current clients get priority treatment.',
|
|
1812
|
+
guardrails: 'Never discuss competitor pricing or badmouth competitors. Never guarantee specific rankings or timelines. Do not discuss internal processes or tools. Do not share other client data. Always offer to schedule a call for complex pricing discussions rather than quoting over text.',
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
const PLACEHOLDER_SETTINGS = {
|
|
1816
|
+
awayMessage: '',
|
|
1817
|
+
followUpEnabled: true,
|
|
1818
|
+
followUpHoursStart: '09:00',
|
|
1819
|
+
followUpHoursEnd: '17:00',
|
|
1820
|
+
maxDailyAutoResponses: 50,
|
|
1821
|
+
maxDailyFollowUps: 20,
|
|
1822
|
+
debounceDelay: 3,
|
|
1823
|
+
defaultResponseMode: 'auto',
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
const ALL_SCOPES = [
|
|
1827
|
+
{ key: 'calendar.view', name: 'View Calendar', desc: 'See availability and scheduled events' },
|
|
1828
|
+
{ key: 'calendar.book', name: 'Book Appointments', desc: 'Schedule new calendar events' },
|
|
1829
|
+
{ key: 'calendar.modify', name: 'Modify Calendar', desc: 'Reschedule or cancel events' },
|
|
1830
|
+
{ key: 'messages.send', name: 'Send Messages', desc: 'Send messages on your behalf' },
|
|
1831
|
+
{ key: 'messages.auto-reply', name: 'Auto-Reply', desc: 'Automatically respond to messages' },
|
|
1832
|
+
{ key: 'tasks.create', name: 'Create Tasks', desc: 'Add tasks to your task manager' },
|
|
1833
|
+
{ key: 'info.business', name: 'Business Info', desc: 'Share business information (hours, services)' },
|
|
1834
|
+
{ key: 'info.personal', name: 'Personal Info', desc: 'Share personal details when relevant' },
|
|
1835
|
+
{ key: 'reminders.create', name: 'Create Reminders', desc: 'Set reminders for follow-ups' },
|
|
1836
|
+
{ key: 'files.send', name: 'Send Files', desc: 'Share documents and files' },
|
|
1837
|
+
{ key: 'support.technical', name: 'Technical Support', desc: 'Provide technical assistance' },
|
|
1838
|
+
];
|
|
1839
|
+
|
|
1840
|
+
// ---- API Layer ----
|
|
1841
|
+
const API_BASE = '/api/v2';
|
|
1842
|
+
|
|
1843
|
+
async function apiGet(path) {
|
|
1844
|
+
try {
|
|
1845
|
+
const res = await fetch(API_BASE + path);
|
|
1846
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
1847
|
+
return await res.json();
|
|
1848
|
+
} catch (e) {
|
|
1849
|
+
console.warn('API unavailable, using placeholder data:', path);
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
async function apiPut(path, body) {
|
|
1855
|
+
try {
|
|
1856
|
+
const res = await fetch(API_BASE + path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
1857
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
1858
|
+
return await res.json();
|
|
1859
|
+
} catch (e) {
|
|
1860
|
+
console.warn('API unavailable:', path);
|
|
1861
|
+
showToast('Saved locally (API unavailable)', false);
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
async function apiPost(path, body) {
|
|
1867
|
+
try {
|
|
1868
|
+
const res = await fetch(API_BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
1869
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
1870
|
+
return await res.json();
|
|
1871
|
+
} catch (e) {
|
|
1872
|
+
console.warn('API unavailable:', path);
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
async function apiDelete(path) {
|
|
1878
|
+
try {
|
|
1879
|
+
const res = await fetch(API_BASE + path, { method: 'DELETE' });
|
|
1880
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
1881
|
+
return await res.json();
|
|
1882
|
+
} catch (e) {
|
|
1883
|
+
console.warn('API unavailable:', path);
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// ---- Init ----
|
|
1889
|
+
async function init() {
|
|
1890
|
+
const data = await apiGet('/contacts');
|
|
1891
|
+
contacts = data || PLACEHOLDER_CONTACTS;
|
|
1892
|
+
renderContactList();
|
|
1893
|
+
updateStats();
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// ---- Navigation ----
|
|
1897
|
+
function showPage(page) {
|
|
1898
|
+
currentPage = page;
|
|
1899
|
+
document.querySelectorAll('.sidebar-nav .nav-btn').forEach((btn, i) => {
|
|
1900
|
+
btn.classList.toggle('active', ['contacts', 'escalations', 'faq', 'playbook', 'settings'][i] === page);
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
const detail = document.getElementById('contactDetail');
|
|
1904
|
+
const empty = document.getElementById('emptyState');
|
|
1905
|
+
const contactPanel = document.getElementById('contactPanel');
|
|
1906
|
+
|
|
1907
|
+
document.querySelectorAll('.page-panel').forEach(p => p.classList.remove('active'));
|
|
1908
|
+
|
|
1909
|
+
if (page === 'contacts') {
|
|
1910
|
+
contactPanel.style.display = 'flex';
|
|
1911
|
+
if (selectedPhone) {
|
|
1912
|
+
detail.style.display = 'flex';
|
|
1913
|
+
empty.style.display = 'none';
|
|
1914
|
+
} else {
|
|
1915
|
+
detail.style.display = 'none';
|
|
1916
|
+
empty.style.display = 'flex';
|
|
1917
|
+
}
|
|
1918
|
+
} else {
|
|
1919
|
+
contactPanel.style.display = 'none';
|
|
1920
|
+
detail.style.display = 'none';
|
|
1921
|
+
empty.style.display = 'none';
|
|
1922
|
+
const pageId = page === 'escalations' ? 'pageEscalations' : 'page' + page.charAt(0).toUpperCase() + page.slice(1);
|
|
1923
|
+
const pageEl = document.getElementById(pageId);
|
|
1924
|
+
if (pageEl) {
|
|
1925
|
+
pageEl.classList.add('active');
|
|
1926
|
+
if (page === 'faq') loadFAQ();
|
|
1927
|
+
if (page === 'playbook') loadPlaybook();
|
|
1928
|
+
if (page === 'settings') loadGlobalSettings();
|
|
1929
|
+
if (page === 'escalations') loadEscalations();
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// ---- Contact List ----
|
|
1935
|
+
function renderContactList() {
|
|
1936
|
+
const list = document.getElementById('contactList');
|
|
1937
|
+
const search = document.getElementById('searchInput').value.toLowerCase();
|
|
1938
|
+
const catFilter = document.getElementById('filterCategory').value;
|
|
1939
|
+
const pipeFilter = document.getElementById('filterPipeline').value;
|
|
1940
|
+
const sortBy = document.getElementById('sortBy').value;
|
|
1941
|
+
|
|
1942
|
+
let filtered = contacts.filter(c => {
|
|
1943
|
+
if (search && !c.name.toLowerCase().includes(search) && !c.phone.includes(search)) return false;
|
|
1944
|
+
if (catFilter && c.category !== catFilter) return false;
|
|
1945
|
+
if (pipeFilter && c.pipelineStage !== pipeFilter) return false;
|
|
1946
|
+
return true;
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
filtered.sort((a, b) => {
|
|
1950
|
+
if (sortBy === 'name') return a.name.localeCompare(b.name);
|
|
1951
|
+
if (sortBy === 'score') return (b.qualificationScore || 0) - (a.qualificationScore || 0);
|
|
1952
|
+
return new Date(b.lastInteraction) - new Date(a.lastInteraction);
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
if (filtered.length === 0) {
|
|
1956
|
+
list.innerHTML = '<div class="empty-state" style="padding:40px 0;"><p>No contacts found</p></div>';
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
list.innerHTML = filtered.map(c => {
|
|
1961
|
+
const initials = c.name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
|
1962
|
+
const time = formatTime(c.lastInteraction);
|
|
1963
|
+
const isActive = c.phone === selectedPhone;
|
|
1964
|
+
const stageClass = c.pipelineStage || 'none';
|
|
1965
|
+
|
|
1966
|
+
return `<div class="contact-item ${isActive ? 'active' : ''}" onclick="selectContact('${c.phone}')">
|
|
1967
|
+
<div class="contact-avatar">
|
|
1968
|
+
${initials}
|
|
1969
|
+
<div class="mode-dot ${c.responseMode}"></div>
|
|
1970
|
+
</div>
|
|
1971
|
+
<div class="contact-info">
|
|
1972
|
+
<div class="contact-name-row">
|
|
1973
|
+
<span class="contact-name">${esc(c.name)}</span>
|
|
1974
|
+
<span class="pipeline-badge ${stageClass}">${stageClass !== 'none' ? stageClass : ''}</span>
|
|
1975
|
+
</div>
|
|
1976
|
+
<span class="contact-preview">${esc(c.lastMessage || '')}</span>
|
|
1977
|
+
<span class="category-tag cat-${c.category}">${c.category.replace('-', ' ')}</span>
|
|
1978
|
+
</div>
|
|
1979
|
+
<div class="contact-meta">
|
|
1980
|
+
<span class="contact-time">${time}</span>
|
|
1981
|
+
${c.qualificationScore > 0 ? `<span class="contact-score">${c.qualificationScore}</span>` : ''}
|
|
1982
|
+
</div>
|
|
1983
|
+
</div>`;
|
|
1984
|
+
}).join('');
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function filterContacts() {
|
|
1988
|
+
renderContactList();
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function updateStats() {
|
|
1992
|
+
document.getElementById('statTotal').textContent = contacts.length;
|
|
1993
|
+
document.getElementById('statLeads').textContent = contacts.filter(c => c.category === 'lead' || c.category === 'qualified-lead').length;
|
|
1994
|
+
document.getElementById('statClients').textContent = contacts.filter(c => c.category === 'client').length;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// ---- Select Contact ----
|
|
1998
|
+
async function selectContact(phone) {
|
|
1999
|
+
selectedPhone = phone;
|
|
2000
|
+
const contact = contacts.find(c => c.phone === phone);
|
|
2001
|
+
if (!contact) return;
|
|
2002
|
+
|
|
2003
|
+
document.getElementById('emptyState').style.display = 'none';
|
|
2004
|
+
const detail = document.getElementById('contactDetail');
|
|
2005
|
+
detail.style.display = 'flex';
|
|
2006
|
+
|
|
2007
|
+
// Mobile: show detail panel as overlay
|
|
2008
|
+
const detailPanel = document.getElementById('detailPanel');
|
|
2009
|
+
if (window.innerWidth <= 768) {
|
|
2010
|
+
detailPanel.classList.add('mobile-show');
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
renderDetailHeader(contact);
|
|
2014
|
+
renderTabs(contact);
|
|
2015
|
+
switchTab('Conversation');
|
|
2016
|
+
renderConversation(contact);
|
|
2017
|
+
renderContactList(); // update active state
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function renderDetailHeader(c) {
|
|
2021
|
+
const initials = c.name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
|
2022
|
+
const catClass = c.category.replace('-', '');
|
|
2023
|
+
const catDisplay = c.category === 'qualified-lead' ? 'qualified lead' : c.category;
|
|
2024
|
+
|
|
2025
|
+
document.getElementById('detailHeader').innerHTML = `
|
|
2026
|
+
<button class="mobile-back-btn" onclick="mobileBack()" title="Back">
|
|
2027
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
|
|
2028
|
+
</button>
|
|
2029
|
+
<div class="detail-avatar">${initials}</div>
|
|
2030
|
+
<div class="detail-info">
|
|
2031
|
+
<div class="name">${esc(c.name)}</div>
|
|
2032
|
+
<div class="phone">${c.phone} · ${c.source}</div>
|
|
2033
|
+
</div>
|
|
2034
|
+
<div class="detail-badges">
|
|
2035
|
+
<span class="detail-category ${catClass}">${catDisplay}</span>
|
|
2036
|
+
${c.qualificationScore > 0 ? `
|
|
2037
|
+
<div class="score-mini">
|
|
2038
|
+
<svg class="score-ring" viewBox="0 0 36 36">
|
|
2039
|
+
<path d="M18 2.0845a15.9155 15.9155 0 010 31.831 15.9155 15.9155 0 010-31.831" fill="none" stroke="${getScoreColor(c.qualificationScore)}" stroke-width="2.5" stroke-dasharray="${c.qualificationScore}, 100" stroke-linecap="round"/>
|
|
2040
|
+
<text x="18" y="21" text-anchor="middle" fill="${getScoreColor(c.qualificationScore)}" font-size="10" font-weight="600">${c.qualificationScore}</text>
|
|
2041
|
+
</svg>
|
|
2042
|
+
</div>` : ''}
|
|
2043
|
+
</div>`;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
function renderTabs(c) {
|
|
2047
|
+
const tabs = ['Conversation', 'Settings', 'Permissions'];
|
|
2048
|
+
if (c.category === 'lead' || c.category === 'qualified-lead') tabs.push('Qualification');
|
|
2049
|
+
tabs.push('Context');
|
|
2050
|
+
|
|
2051
|
+
document.getElementById('tabsBar').innerHTML = tabs.map((t, i) =>
|
|
2052
|
+
`<button class="tab-btn ${i === 0 ? 'active' : ''}" onclick="switchTab('${t}')">${t}</button>`
|
|
2053
|
+
).join('');
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function switchTab(name) {
|
|
2057
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.textContent === name));
|
|
2058
|
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
2059
|
+
const tabEl = document.getElementById('tab' + name);
|
|
2060
|
+
if (tabEl) tabEl.classList.add('active');
|
|
2061
|
+
|
|
2062
|
+
const contact = contacts.find(c => c.phone === selectedPhone);
|
|
2063
|
+
if (!contact) return;
|
|
2064
|
+
|
|
2065
|
+
if (name === 'Conversation') renderConversation(contact);
|
|
2066
|
+
else if (name === 'Settings') renderSettings(contact);
|
|
2067
|
+
else if (name === 'Permissions') renderPermissions(contact);
|
|
2068
|
+
else if (name === 'Qualification') renderQualification(contact);
|
|
2069
|
+
else if (name === 'Context') renderContext(contact);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// ---- Conversation ----
|
|
2073
|
+
function renderConversation(c) {
|
|
2074
|
+
const msgs = c.messages || [];
|
|
2075
|
+
const container = document.getElementById('messagesContainer');
|
|
2076
|
+
|
|
2077
|
+
if (msgs.length === 0) {
|
|
2078
|
+
container.innerHTML = '<div class="empty-state" style="flex:1;"><p>No messages yet</p></div>';
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
container.innerHTML = msgs.map(m => `
|
|
2083
|
+
<div class="message-bubble ${m.direction === 'inbound' ? 'inbound' : 'outbound'}">
|
|
2084
|
+
${esc(m.text)}
|
|
2085
|
+
<div class="msg-time">${formatDateTime(m.timestamp)}</div>
|
|
2086
|
+
</div>
|
|
2087
|
+
`).join('');
|
|
2088
|
+
|
|
2089
|
+
container.scrollTop = container.scrollHeight;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
async function sendMessage() {
|
|
2093
|
+
const input = document.getElementById('messageInput');
|
|
2094
|
+
const text = input.value.trim();
|
|
2095
|
+
if (!text || !selectedPhone) return;
|
|
2096
|
+
|
|
2097
|
+
const contact = contacts.find(c => c.phone === selectedPhone);
|
|
2098
|
+
if (!contact) return;
|
|
2099
|
+
|
|
2100
|
+
if (!contact.messages) contact.messages = [];
|
|
2101
|
+
const msg = { direction: 'outbound', text, timestamp: new Date().toISOString() };
|
|
2102
|
+
const prevLastMessage = contact.lastMessage;
|
|
2103
|
+
const prevLastInteraction = contact.lastInteraction;
|
|
2104
|
+
|
|
2105
|
+
contact.messages.push(msg);
|
|
2106
|
+
contact.lastMessage = text;
|
|
2107
|
+
contact.lastInteraction = new Date().toISOString();
|
|
2108
|
+
|
|
2109
|
+
input.value = '';
|
|
2110
|
+
renderConversation(contact);
|
|
2111
|
+
renderContactList();
|
|
2112
|
+
|
|
2113
|
+
// Fire API call with rollback on failure
|
|
2114
|
+
const result = await apiPost(`/send`, { phone: selectedPhone, text });
|
|
2115
|
+
if (result === null) {
|
|
2116
|
+
// Rollback optimistic update
|
|
2117
|
+
const idx = contact.messages.indexOf(msg);
|
|
2118
|
+
if (idx !== -1) contact.messages.splice(idx, 1);
|
|
2119
|
+
contact.lastMessage = prevLastMessage;
|
|
2120
|
+
contact.lastInteraction = prevLastInteraction;
|
|
2121
|
+
renderConversation(contact);
|
|
2122
|
+
renderContactList();
|
|
2123
|
+
showToast('Failed to send message', true);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// ---- Settings ----
|
|
2128
|
+
function renderSettings(c) {
|
|
2129
|
+
document.getElementById('settingsForm').innerHTML = `
|
|
2130
|
+
<div class="form-row">
|
|
2131
|
+
<div class="form-group">
|
|
2132
|
+
<label>Category</label>
|
|
2133
|
+
<select id="setCategory" value="${c.category}">
|
|
2134
|
+
${['unknown','lead','qualified-lead','client','family','friend','team','blocked'].map(v =>
|
|
2135
|
+
`<option value="${v}" ${c.category === v ? 'selected' : ''}>${v}</option>`).join('')}
|
|
2136
|
+
</select>
|
|
2137
|
+
</div>
|
|
2138
|
+
<div class="form-group">
|
|
2139
|
+
<label>Response Mode</label>
|
|
2140
|
+
<select id="setMode">
|
|
2141
|
+
${['auto','escalate','monitor','block'].map(v =>
|
|
2142
|
+
`<option value="${v}" ${c.responseMode === v ? 'selected' : ''}>${v}</option>`).join('')}
|
|
2143
|
+
</select>
|
|
2144
|
+
</div>
|
|
2145
|
+
</div>
|
|
2146
|
+
<div class="form-row">
|
|
2147
|
+
<div class="form-group">
|
|
2148
|
+
<label>Conversation Style</label>
|
|
2149
|
+
<select id="setStyle">
|
|
2150
|
+
${['casual','professional','friendly'].map(v =>
|
|
2151
|
+
`<option value="${v}" ${c.style === v ? 'selected' : ''}>${v}</option>`).join('')}
|
|
2152
|
+
</select>
|
|
2153
|
+
</div>
|
|
2154
|
+
<div class="form-group">
|
|
2155
|
+
<label>Source</label>
|
|
2156
|
+
<select disabled>
|
|
2157
|
+
<option>${c.source}</option>
|
|
2158
|
+
</select>
|
|
2159
|
+
</div>
|
|
2160
|
+
</div>
|
|
2161
|
+
<div class="form-group">
|
|
2162
|
+
<label>Custom Instructions</label>
|
|
2163
|
+
<textarea id="setInstructions" placeholder="Special instructions for how the router should handle this contact...">${esc(c.instructions || '')}</textarea>
|
|
2164
|
+
</div>
|
|
2165
|
+
<button class="btn-save" onclick="saveContactSettings()">Save Changes</button>
|
|
2166
|
+
`;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
async function saveContactSettings() {
|
|
2170
|
+
const contact = contacts.find(c => c.phone === selectedPhone);
|
|
2171
|
+
if (!contact) return;
|
|
2172
|
+
|
|
2173
|
+
contact.category = document.getElementById('setCategory').value;
|
|
2174
|
+
contact.responseMode = document.getElementById('setMode').value;
|
|
2175
|
+
contact.style = document.getElementById('setStyle').value;
|
|
2176
|
+
contact.instructions = document.getElementById('setInstructions').value;
|
|
2177
|
+
|
|
2178
|
+
await apiPut(`/contacts/${encodeURIComponent(selectedPhone)}`, {
|
|
2179
|
+
category: contact.category,
|
|
2180
|
+
responseMode: contact.responseMode,
|
|
2181
|
+
style: contact.style,
|
|
2182
|
+
instructions: contact.instructions,
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
renderDetailHeader(contact);
|
|
2186
|
+
renderTabs(contact);
|
|
2187
|
+
renderContactList();
|
|
2188
|
+
showToast('Contact settings saved');
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// ---- Permissions ----
|
|
2192
|
+
function renderPermissions(c) {
|
|
2193
|
+
const scopes = c.scopes || [];
|
|
2194
|
+
document.getElementById('permissionsGrid').innerHTML = `
|
|
2195
|
+
<div class="perm-actions">
|
|
2196
|
+
<button class="btn-outline" onclick="permAction('defaults')">Reset to Defaults</button>
|
|
2197
|
+
<button class="btn-outline" onclick="permAction('all')">Grant All</button>
|
|
2198
|
+
<button class="btn-outline" onclick="permAction('none')">Revoke All</button>
|
|
2199
|
+
</div>
|
|
2200
|
+
${ALL_SCOPES.map(s => `
|
|
2201
|
+
<div class="perm-item">
|
|
2202
|
+
<div class="perm-label">
|
|
2203
|
+
<div class="scope-name">${s.name}</div>
|
|
2204
|
+
<div class="scope-desc">${s.desc}</div>
|
|
2205
|
+
</div>
|
|
2206
|
+
<label class="toggle">
|
|
2207
|
+
<input type="checkbox" data-scope="${s.key}" ${scopes.includes(s.key) ? 'checked' : ''} onchange="toggleScope('${s.key}', this.checked)">
|
|
2208
|
+
<span class="toggle-slider"></span>
|
|
2209
|
+
</label>
|
|
2210
|
+
</div>
|
|
2211
|
+
`).join('')}
|
|
2212
|
+
`;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
async function toggleScope(scope, granted) {
|
|
2216
|
+
const contact = contacts.find(c => c.phone === selectedPhone);
|
|
2217
|
+
if (!contact) return;
|
|
2218
|
+
if (!contact.scopes) contact.scopes = [];
|
|
2219
|
+
|
|
2220
|
+
if (granted && !contact.scopes.includes(scope)) contact.scopes.push(scope);
|
|
2221
|
+
else if (!granted) contact.scopes = contact.scopes.filter(s => s !== scope);
|
|
2222
|
+
|
|
2223
|
+
await apiPut(`/contacts/${encodeURIComponent(selectedPhone)}/scopes`, { scopes: contact.scopes });
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function permAction(action) {
|
|
2227
|
+
const contact = contacts.find(c => c.phone === selectedPhone);
|
|
2228
|
+
if (!contact) return;
|
|
2229
|
+
|
|
2230
|
+
if (action === 'all') {
|
|
2231
|
+
contact.scopes = ALL_SCOPES.map(s => s.key);
|
|
2232
|
+
} else if (action === 'none') {
|
|
2233
|
+
contact.scopes = [];
|
|
2234
|
+
} else if (action === 'defaults') {
|
|
2235
|
+
const defaults = {
|
|
2236
|
+
unknown: ['info.business', 'messages.auto-reply'],
|
|
2237
|
+
lead: ['calendar.view', 'calendar.book', 'info.business', 'messages.auto-reply'],
|
|
2238
|
+
'qualified-lead': ['calendar.view', 'calendar.book', 'info.business', 'messages.auto-reply', 'files.send'],
|
|
2239
|
+
client: ['calendar.view', 'calendar.book', 'calendar.modify', 'messages.send', 'messages.auto-reply', 'tasks.create', 'info.business', 'reminders.create', 'files.send', 'support.technical'],
|
|
2240
|
+
family: ['calendar.view', 'messages.send', 'messages.auto-reply', 'info.personal'],
|
|
2241
|
+
friend: ['calendar.view', 'messages.send', 'messages.auto-reply'],
|
|
2242
|
+
team: ALL_SCOPES.map(s => s.key),
|
|
2243
|
+
blocked: [],
|
|
2244
|
+
};
|
|
2245
|
+
contact.scopes = defaults[contact.category] || ['messages.auto-reply'];
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
renderPermissions(contact);
|
|
2249
|
+
apiPut(`/contacts/${encodeURIComponent(selectedPhone)}/scopes`, { scopes: contact.scopes });
|
|
2250
|
+
showToast('Permissions updated');
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// ---- Qualification ----
|
|
2254
|
+
function renderQualification(c) {
|
|
2255
|
+
const score = c.qualificationScore || 0;
|
|
2256
|
+
const stage = c.pipelineStage || 'cold';
|
|
2257
|
+
const qual = c.qualification || { signals: [] };
|
|
2258
|
+
const stages = ['cold', 'warm', 'hot', 'qualified', 'client'];
|
|
2259
|
+
const stageIdx = stages.indexOf(stage);
|
|
2260
|
+
const stageColors = { cold: '#6b7280', warm: '#eab308', hot: '#f97316', qualified: '#22c55e', client: '#3b82f6' };
|
|
2261
|
+
|
|
2262
|
+
const gaugeColor = getScoreColor(score);
|
|
2263
|
+
const angle = (score / 100) * 180;
|
|
2264
|
+
|
|
2265
|
+
document.getElementById('qualContent').innerHTML = `
|
|
2266
|
+
<div class="score-gauge">
|
|
2267
|
+
<svg class="gauge-svg" viewBox="0 0 200 110">
|
|
2268
|
+
<path d="M20 100 A80 80 0 0 1 180 100" fill="none" stroke="#252525" stroke-width="12" stroke-linecap="round"/>
|
|
2269
|
+
<path d="M20 100 A80 80 0 0 1 180 100" fill="none" stroke="${gaugeColor}" stroke-width="12" stroke-linecap="round"
|
|
2270
|
+
stroke-dasharray="${(score / 100) * 251.2} 251.2"/>
|
|
2271
|
+
</svg>
|
|
2272
|
+
<div class="gauge-label" style="color:${gaugeColor}">${score}</div>
|
|
2273
|
+
<div class="gauge-sub">Qualification Score</div>
|
|
2274
|
+
</div>
|
|
2275
|
+
|
|
2276
|
+
<div>
|
|
2277
|
+
<h4 style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:12px;">Pipeline Stage</h4>
|
|
2278
|
+
<div class="pipeline-vis">
|
|
2279
|
+
${stages.map((s, i) => `
|
|
2280
|
+
<div class="pipeline-stage">
|
|
2281
|
+
${i > 0 ? '' : ''}
|
|
2282
|
+
<div class="pipeline-dot ${i <= stageIdx ? 'reached' : ''} ${i === stageIdx ? 'current' : ''}"
|
|
2283
|
+
style="${i <= stageIdx ? `background:${stageColors[s]};border-color:${stageColors[s]};` : ''}"></div>
|
|
2284
|
+
<span class="pipeline-stage-label ${i === stageIdx ? 'current' : ''}">${s}</span>
|
|
2285
|
+
</div>
|
|
2286
|
+
`).join('')}
|
|
2287
|
+
</div>
|
|
2288
|
+
</div>
|
|
2289
|
+
|
|
2290
|
+
${qual.signals.length > 0 ? `
|
|
2291
|
+
<div>
|
|
2292
|
+
<h4 style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:12px;">Qualification Signals</h4>
|
|
2293
|
+
<div class="signals-list">
|
|
2294
|
+
${qual.signals.map(s => `
|
|
2295
|
+
<div class="signal-item">
|
|
2296
|
+
<span class="signal-dot"></span>
|
|
2297
|
+
${esc(s.signal)}
|
|
2298
|
+
<span class="signal-points">+${s.points}</span>
|
|
2299
|
+
</div>
|
|
2300
|
+
`).join('')}
|
|
2301
|
+
</div>
|
|
2302
|
+
</div>` : ''}
|
|
2303
|
+
|
|
2304
|
+
<div class="form-group" style="margin-top:8px;">
|
|
2305
|
+
<label>Manual Score Override</label>
|
|
2306
|
+
<div style="display:flex;gap:10px;align-items:center;">
|
|
2307
|
+
<input type="number" id="scoreOverride" min="0" max="100" value="${score}" style="width:80px;">
|
|
2308
|
+
<button class="btn-outline" onclick="overrideScore()">Update</button>
|
|
2309
|
+
</div>
|
|
2310
|
+
</div>
|
|
2311
|
+
`;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
async function overrideScore() {
|
|
2315
|
+
const contact = contacts.find(c => c.phone === selectedPhone);
|
|
2316
|
+
if (!contact) return;
|
|
2317
|
+
const val = parseInt(document.getElementById('scoreOverride').value) || 0;
|
|
2318
|
+
contact.qualificationScore = Math.max(0, Math.min(100, val));
|
|
2319
|
+
if (contact.qualification) contact.qualification.score = contact.qualificationScore;
|
|
2320
|
+
renderQualification(contact);
|
|
2321
|
+
await apiPut(`/contacts/${encodeURIComponent(selectedPhone)}`, { qualificationScore: contact.qualificationScore });
|
|
2322
|
+
renderContactList();
|
|
2323
|
+
showToast('Score updated');
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// ---- Context ----
|
|
2327
|
+
function renderContext(c) {
|
|
2328
|
+
const ctx = c.context || {};
|
|
2329
|
+
const prefs = ctx.preferences || {};
|
|
2330
|
+
document.getElementById('contextContent').innerHTML = `
|
|
2331
|
+
<div class="context-card">
|
|
2332
|
+
<h4>Relationship</h4>
|
|
2333
|
+
<p>${esc(ctx.relationship || 'No relationship summary yet.')}</p>
|
|
2334
|
+
</div>
|
|
2335
|
+
<div class="context-card">
|
|
2336
|
+
<h4>Last Topic</h4>
|
|
2337
|
+
<p>${esc(ctx.lastTopic || 'None')}</p>
|
|
2338
|
+
</div>
|
|
2339
|
+
<div class="context-card">
|
|
2340
|
+
<h4>Pending Items</h4>
|
|
2341
|
+
${(ctx.pendingItems && ctx.pendingItems.length > 0)
|
|
2342
|
+
? `<ul class="items-list">${ctx.pendingItems.map(i => `<li>${esc(i)}</li>`).join('')}</ul>`
|
|
2343
|
+
: '<p>No pending items</p>'}
|
|
2344
|
+
</div>
|
|
2345
|
+
<div class="context-card">
|
|
2346
|
+
<h4>Conversation Summary</h4>
|
|
2347
|
+
<p>${esc(ctx.conversationSummary || 'No summary available.')}</p>
|
|
2348
|
+
</div>
|
|
2349
|
+
${Object.keys(prefs).length > 0 ? `
|
|
2350
|
+
<div class="context-card">
|
|
2351
|
+
<h4>Learned Preferences</h4>
|
|
2352
|
+
<ul class="items-list">
|
|
2353
|
+
${Object.entries(prefs).map(([k, v]) => `<li><strong>${esc(k)}:</strong> ${esc(String(v))}</li>`).join('')}
|
|
2354
|
+
</ul>
|
|
2355
|
+
</div>` : ''}
|
|
2356
|
+
`;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// ---- FAQ ----
|
|
2360
|
+
let faqData = [];
|
|
2361
|
+
|
|
2362
|
+
async function loadFAQ() {
|
|
2363
|
+
const data = await apiGet('/faq');
|
|
2364
|
+
faqData = data || PLACEHOLDER_FAQ;
|
|
2365
|
+
renderFAQ();
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function renderFAQ() {
|
|
2369
|
+
const search = (document.getElementById('faqSearch')?.value || '').toLowerCase();
|
|
2370
|
+
const catFilter = document.getElementById('faqCategoryFilter')?.value || '';
|
|
2371
|
+
|
|
2372
|
+
let filtered = faqData.filter(f => {
|
|
2373
|
+
if (search && !f.question.toLowerCase().includes(search) && !f.answer.toLowerCase().includes(search)) return false;
|
|
2374
|
+
if (catFilter && f.category !== catFilter) return false;
|
|
2375
|
+
return true;
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
const list = document.getElementById('faqList');
|
|
2379
|
+
if (filtered.length === 0) {
|
|
2380
|
+
list.innerHTML = '<div class="empty-state" style="padding:40px 0;"><p>No FAQ entries found</p></div>';
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
list.innerHTML = filtered.map(f => `
|
|
2385
|
+
<div class="faq-item">
|
|
2386
|
+
<div class="faq-q">${esc(f.question)}</div>
|
|
2387
|
+
<div class="faq-a">${esc(f.answer)}</div>
|
|
2388
|
+
<div class="faq-meta">
|
|
2389
|
+
<span class="faq-cat">${f.category}</span>
|
|
2390
|
+
<div class="faq-actions">
|
|
2391
|
+
<button class="btn-icon" onclick="editFaq(${f.id})" title="Edit">
|
|
2392
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
|
2393
|
+
</button>
|
|
2394
|
+
<button class="btn-icon danger" onclick="deleteFaq(${f.id})" title="Delete">
|
|
2395
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
2396
|
+
</button>
|
|
2397
|
+
</div>
|
|
2398
|
+
</div>
|
|
2399
|
+
</div>
|
|
2400
|
+
`).join('');
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function filterFAQ() { renderFAQ(); }
|
|
2404
|
+
|
|
2405
|
+
function openFaqModal(id) {
|
|
2406
|
+
editingFaqId = id || null;
|
|
2407
|
+
const modal = document.getElementById('faqModal');
|
|
2408
|
+
document.getElementById('faqModalTitle').textContent = id ? 'Edit FAQ Entry' : 'Add FAQ Entry';
|
|
2409
|
+
|
|
2410
|
+
if (id) {
|
|
2411
|
+
const f = faqData.find(x => x.id === id);
|
|
2412
|
+
if (f) {
|
|
2413
|
+
document.getElementById('faqQ').value = f.question;
|
|
2414
|
+
document.getElementById('faqA').value = f.answer;
|
|
2415
|
+
document.getElementById('faqCat').value = f.category;
|
|
2416
|
+
}
|
|
2417
|
+
} else {
|
|
2418
|
+
document.getElementById('faqQ').value = '';
|
|
2419
|
+
document.getElementById('faqA').value = '';
|
|
2420
|
+
document.getElementById('faqCat').value = 'general';
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
modal.classList.add('open');
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function closeFaqModal() {
|
|
2427
|
+
document.getElementById('faqModal').classList.remove('open');
|
|
2428
|
+
editingFaqId = null;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
async function saveFaqEntry() {
|
|
2432
|
+
const q = document.getElementById('faqQ').value.trim();
|
|
2433
|
+
const a = document.getElementById('faqA').value.trim();
|
|
2434
|
+
const cat = document.getElementById('faqCat').value;
|
|
2435
|
+
if (!q || !a) return;
|
|
2436
|
+
|
|
2437
|
+
if (editingFaqId) {
|
|
2438
|
+
const entry = faqData.find(f => f.id === editingFaqId);
|
|
2439
|
+
if (entry) { entry.question = q; entry.answer = a; entry.category = cat; }
|
|
2440
|
+
await apiPut(`/faq/${editingFaqId}`, { question: q, answer: a, category: cat });
|
|
2441
|
+
} else {
|
|
2442
|
+
const newId = Math.max(0, ...faqData.map(f => f.id)) + 1;
|
|
2443
|
+
faqData.push({ id: newId, question: q, answer: a, category: cat });
|
|
2444
|
+
await apiPost('/faq', { question: q, answer: a, category: cat });
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
closeFaqModal();
|
|
2448
|
+
renderFAQ();
|
|
2449
|
+
showToast(editingFaqId ? 'FAQ entry updated' : 'FAQ entry created');
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
function editFaq(id) { openFaqModal(id); }
|
|
2453
|
+
|
|
2454
|
+
async function deleteFaq(id) {
|
|
2455
|
+
if (!confirm('Delete this FAQ entry?')) return;
|
|
2456
|
+
faqData = faqData.filter(f => f.id !== id);
|
|
2457
|
+
await apiDelete(`/faq/${id}`);
|
|
2458
|
+
renderFAQ();
|
|
2459
|
+
showToast('FAQ entry deleted');
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// ---- Playbook ----
|
|
2463
|
+
async function loadPlaybook() {
|
|
2464
|
+
const data = await apiGet('/playbook');
|
|
2465
|
+
const pb = data || PLACEHOLDER_PLAYBOOK;
|
|
2466
|
+
|
|
2467
|
+
document.getElementById('playbookBody').innerHTML = `
|
|
2468
|
+
<div class="playbook-section">
|
|
2469
|
+
<h3>Brand Voice</h3>
|
|
2470
|
+
<textarea id="pbVoice">${esc(pb.brandVoice || '')}</textarea>
|
|
2471
|
+
</div>
|
|
2472
|
+
<div class="playbook-section">
|
|
2473
|
+
<h3>Services</h3>
|
|
2474
|
+
<textarea id="pbServices">${esc(pb.services || '')}</textarea>
|
|
2475
|
+
</div>
|
|
2476
|
+
<div class="playbook-section">
|
|
2477
|
+
<h3>Qualifying Strategies</h3>
|
|
2478
|
+
<textarea id="pbQualify">${esc(pb.qualifyingStrategies || '')}</textarea>
|
|
2479
|
+
</div>
|
|
2480
|
+
<div class="playbook-section">
|
|
2481
|
+
<h3>Guardrails</h3>
|
|
2482
|
+
<textarea id="pbGuardrails">${esc(pb.guardrails || '')}</textarea>
|
|
2483
|
+
</div>
|
|
2484
|
+
<button class="btn-save" onclick="savePlaybook()">Save Playbook</button>
|
|
2485
|
+
`;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
async function savePlaybook() {
|
|
2489
|
+
const playbook = {
|
|
2490
|
+
brandVoice: document.getElementById('pbVoice').value,
|
|
2491
|
+
services: document.getElementById('pbServices').value,
|
|
2492
|
+
qualifyingStrategies: document.getElementById('pbQualify').value,
|
|
2493
|
+
guardrails: document.getElementById('pbGuardrails').value,
|
|
2494
|
+
};
|
|
2495
|
+
await apiPut('/playbook', playbook);
|
|
2496
|
+
showToast('Playbook saved');
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// ---- Global Settings ----
|
|
2500
|
+
async function loadGlobalSettings() {
|
|
2501
|
+
const data = await apiGet('/settings');
|
|
2502
|
+
const s = data || PLACEHOLDER_SETTINGS;
|
|
2503
|
+
|
|
2504
|
+
document.getElementById('globalSettingsGrid').innerHTML = `
|
|
2505
|
+
<div class="settings-card">
|
|
2506
|
+
<h3>Away Message</h3>
|
|
2507
|
+
<div class="form-group">
|
|
2508
|
+
<label>Message Text</label>
|
|
2509
|
+
<textarea id="gsAway" placeholder="Leave empty to disable">${esc(s.awayMessage || '')}</textarea>
|
|
2510
|
+
</div>
|
|
2511
|
+
</div>
|
|
2512
|
+
<div class="settings-card">
|
|
2513
|
+
<h3>Follow-Ups</h3>
|
|
2514
|
+
<div class="form-group">
|
|
2515
|
+
<label>Enabled</label>
|
|
2516
|
+
<label class="toggle">
|
|
2517
|
+
<input type="checkbox" id="gsFollowUp" ${s.followUpEnabled ? 'checked' : ''}>
|
|
2518
|
+
<span class="toggle-slider"></span>
|
|
2519
|
+
</label>
|
|
2520
|
+
</div>
|
|
2521
|
+
<div class="form-row">
|
|
2522
|
+
<div class="form-group">
|
|
2523
|
+
<label>Start Hour</label>
|
|
2524
|
+
<input type="time" id="gsStart" value="${s.followUpHoursStart || '09:00'}">
|
|
2525
|
+
</div>
|
|
2526
|
+
<div class="form-group">
|
|
2527
|
+
<label>End Hour</label>
|
|
2528
|
+
<input type="time" id="gsEnd" value="${s.followUpHoursEnd || '17:00'}">
|
|
2529
|
+
</div>
|
|
2530
|
+
</div>
|
|
2531
|
+
</div>
|
|
2532
|
+
<div class="settings-card">
|
|
2533
|
+
<h3>Rate Limits</h3>
|
|
2534
|
+
<div class="form-group">
|
|
2535
|
+
<label>Max Daily Auto-Responses</label>
|
|
2536
|
+
<input type="number" id="gsMaxAuto" value="${s.maxDailyAutoResponses || 50}">
|
|
2537
|
+
</div>
|
|
2538
|
+
<div class="form-group">
|
|
2539
|
+
<label>Max Daily Follow-Ups</label>
|
|
2540
|
+
<input type="number" id="gsMaxFollow" value="${s.maxDailyFollowUps || 20}">
|
|
2541
|
+
</div>
|
|
2542
|
+
</div>
|
|
2543
|
+
<div class="settings-card">
|
|
2544
|
+
<h3>Defaults</h3>
|
|
2545
|
+
<div class="form-group">
|
|
2546
|
+
<label>Debounce Delay (seconds)</label>
|
|
2547
|
+
<input type="number" id="gsDebounce" value="${s.debounceDelay || 3}">
|
|
2548
|
+
</div>
|
|
2549
|
+
<div class="form-group">
|
|
2550
|
+
<label>Default Response Mode</label>
|
|
2551
|
+
<select id="gsDefaultMode">
|
|
2552
|
+
${['auto','escalate','monitor','block'].map(v =>
|
|
2553
|
+
`<option value="${v}" ${s.defaultResponseMode === v ? 'selected' : ''}>${v}</option>`).join('')}
|
|
2554
|
+
</select>
|
|
2555
|
+
</div>
|
|
2556
|
+
</div>
|
|
2557
|
+
`;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
async function saveGlobalSettings() {
|
|
2561
|
+
const settings = {
|
|
2562
|
+
awayMessage: document.getElementById('gsAway').value,
|
|
2563
|
+
followUpEnabled: document.getElementById('gsFollowUp').checked,
|
|
2564
|
+
followUpHoursStart: document.getElementById('gsStart').value,
|
|
2565
|
+
followUpHoursEnd: document.getElementById('gsEnd').value,
|
|
2566
|
+
maxDailyAutoResponses: parseInt(document.getElementById('gsMaxAuto').value) || 50,
|
|
2567
|
+
maxDailyFollowUps: parseInt(document.getElementById('gsMaxFollow').value) || 20,
|
|
2568
|
+
debounceDelay: parseInt(document.getElementById('gsDebounce').value) || 3,
|
|
2569
|
+
defaultResponseMode: document.getElementById('gsDefaultMode').value,
|
|
2570
|
+
};
|
|
2571
|
+
await apiPut('/settings', settings);
|
|
2572
|
+
showToast('Settings saved');
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// ---- Utilities ----
|
|
2576
|
+
function esc(s) {
|
|
2577
|
+
const d = document.createElement('div');
|
|
2578
|
+
d.textContent = s;
|
|
2579
|
+
return d.innerHTML;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
function formatTime(iso) {
|
|
2583
|
+
if (!iso) return '';
|
|
2584
|
+
const d = new Date(iso);
|
|
2585
|
+
const now = new Date();
|
|
2586
|
+
const diff = now - d;
|
|
2587
|
+
if (diff < 60000) return 'now';
|
|
2588
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + 'm';
|
|
2589
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h';
|
|
2590
|
+
if (diff < 604800000) return Math.floor(diff / 86400000) + 'd';
|
|
2591
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
function formatDateTime(iso) {
|
|
2595
|
+
if (!iso) return '';
|
|
2596
|
+
return new Date(iso).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function getScoreColor(score) {
|
|
2600
|
+
if (score >= 80) return '#22c55e';
|
|
2601
|
+
if (score >= 60) return '#eab308';
|
|
2602
|
+
if (score >= 40) return '#f97316';
|
|
2603
|
+
return '#6b7280';
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function showToast(msg, isError = false) {
|
|
2607
|
+
const existing = document.querySelector('.toast');
|
|
2608
|
+
if (existing) existing.remove();
|
|
2609
|
+
const toast = document.createElement('div');
|
|
2610
|
+
toast.className = 'toast' + (isError ? ' error' : '');
|
|
2611
|
+
toast.textContent = msg;
|
|
2612
|
+
document.body.appendChild(toast);
|
|
2613
|
+
setTimeout(() => toast.remove(), 3000);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// ---- Mobile Back ----
|
|
2617
|
+
function mobileBack() {
|
|
2618
|
+
const detail = document.getElementById('detailPanel');
|
|
2619
|
+
detail.classList.remove('mobile-show');
|
|
2620
|
+
selectedPhone = null;
|
|
2621
|
+
renderContactList();
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// ---- Escalation Queue ----
|
|
2625
|
+
let escalations = [];
|
|
2626
|
+
let escPollTimer = null;
|
|
2627
|
+
|
|
2628
|
+
async function loadEscalations() {
|
|
2629
|
+
try {
|
|
2630
|
+
const res = await fetch('/api/v2/escalations?limit=50');
|
|
2631
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
2632
|
+
const data = await res.json();
|
|
2633
|
+
escalations = (data || []).filter(e => e.status === 'pending');
|
|
2634
|
+
} catch (e) {
|
|
2635
|
+
console.warn('Escalation API unavailable:', e.message);
|
|
2636
|
+
escalations = [];
|
|
2637
|
+
}
|
|
2638
|
+
renderEscalations();
|
|
2639
|
+
updateEscBadge();
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
function getEscUrgency(timeoutAt) {
|
|
2643
|
+
if (!timeoutAt) return 'low';
|
|
2644
|
+
const remaining = new Date(timeoutAt.replace(' ', 'T') + 'Z') - Date.now();
|
|
2645
|
+
if (remaining <= 0) return 'critical';
|
|
2646
|
+
if (remaining < 2 * 60000) return 'critical';
|
|
2647
|
+
if (remaining < 5 * 60000) return 'high';
|
|
2648
|
+
if (remaining < 10 * 60000) return 'medium';
|
|
2649
|
+
return 'low';
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
function formatRemaining(timeoutAt) {
|
|
2653
|
+
if (!timeoutAt) return '';
|
|
2654
|
+
const remaining = new Date(timeoutAt.replace(' ', 'T') + 'Z') - Date.now();
|
|
2655
|
+
if (remaining <= 0) return 'EXPIRED';
|
|
2656
|
+
const mins = Math.floor(remaining / 60000);
|
|
2657
|
+
const secs = Math.floor((remaining % 60000) / 1000);
|
|
2658
|
+
if (mins > 0) return `${mins}m ${secs}s`;
|
|
2659
|
+
return `${secs}s`;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function renderEscalations() {
|
|
2663
|
+
const list = document.getElementById('escalationList');
|
|
2664
|
+
if (!list) return;
|
|
2665
|
+
|
|
2666
|
+
if (escalations.length === 0) {
|
|
2667
|
+
list.innerHTML = `<div class="escalation-empty">
|
|
2668
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
2669
|
+
<p>No pending escalations</p>
|
|
2670
|
+
</div>`;
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
list.innerHTML = escalations.map(e => {
|
|
2675
|
+
const ctx = JSON.parse(e.context_sent || '{}');
|
|
2676
|
+
const urgency = getEscUrgency(e.timeout_at);
|
|
2677
|
+
const remaining = formatRemaining(e.timeout_at);
|
|
2678
|
+
const name = ctx.contactName || e.phone;
|
|
2679
|
+
const category = ctx.category || 'unknown';
|
|
2680
|
+
const trigger = e.trigger_message || ctx.triggerMessage || '';
|
|
2681
|
+
|
|
2682
|
+
return `<div class="escalation-card urgency-${urgency}" data-esc-id="${esc(e.escalation_id || e.id)}">
|
|
2683
|
+
<div class="esc-header">
|
|
2684
|
+
<span class="esc-name">${esc(name)}</span>
|
|
2685
|
+
<span class="esc-category">${esc(category)}</span>
|
|
2686
|
+
<span class="esc-timer urgency-${urgency}" data-timeout="${esc(e.timeout_at || '')}">${remaining}</span>
|
|
2687
|
+
</div>
|
|
2688
|
+
<div class="esc-message">${esc(trigger)}</div>
|
|
2689
|
+
<div class="esc-actions">
|
|
2690
|
+
<input class="esc-reply-input" placeholder="Type a response..." id="escReply-${esc(e.escalation_id || e.id)}" onkeydown="if(event.key==='Enter')respondEscalation('${esc(e.escalation_id || e.id)}')">
|
|
2691
|
+
<button class="btn-primary" onclick="respondEscalation('${esc(e.escalation_id || e.id)}')" style="padding:7px 14px;font-size:12px;">Reply</button>
|
|
2692
|
+
<button class="btn-deny" onclick="denyEscalation('${esc(e.escalation_id || e.id)}')">Deny</button>
|
|
2693
|
+
</div>
|
|
2694
|
+
</div>`;
|
|
2695
|
+
}).join('');
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
function updateEscBadge() {
|
|
2699
|
+
const badge = document.getElementById('escBadge');
|
|
2700
|
+
if (!badge) return;
|
|
2701
|
+
const count = escalations.length;
|
|
2702
|
+
if (count > 0) {
|
|
2703
|
+
badge.textContent = count;
|
|
2704
|
+
badge.style.display = 'flex';
|
|
2705
|
+
} else {
|
|
2706
|
+
badge.style.display = 'none';
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// Update timers every second when on escalation page
|
|
2711
|
+
function startEscTimers() {
|
|
2712
|
+
if (escPollTimer) clearInterval(escPollTimer);
|
|
2713
|
+
escPollTimer = setInterval(() => {
|
|
2714
|
+
document.querySelectorAll('.esc-timer[data-timeout]').forEach(el => {
|
|
2715
|
+
const timeout = el.getAttribute('data-timeout');
|
|
2716
|
+
if (timeout) {
|
|
2717
|
+
const urgency = getEscUrgency(timeout);
|
|
2718
|
+
el.textContent = formatRemaining(timeout);
|
|
2719
|
+
el.className = `esc-timer urgency-${urgency}`;
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
}, 1000);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
async function respondEscalation(escalationId) {
|
|
2726
|
+
const input = document.getElementById('escReply-' + escalationId);
|
|
2727
|
+
const text = input?.value?.trim();
|
|
2728
|
+
if (!text) return;
|
|
2729
|
+
|
|
2730
|
+
try {
|
|
2731
|
+
const res = await fetch('/api/router/escalation-reply', {
|
|
2732
|
+
method: 'POST',
|
|
2733
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2734
|
+
body: JSON.stringify({ escalationId, response: text }),
|
|
2735
|
+
});
|
|
2736
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
2737
|
+
const result = await res.json();
|
|
2738
|
+
if (result.success === false) throw new Error(result.error || 'Failed');
|
|
2739
|
+
|
|
2740
|
+
escalations = escalations.filter(e => (e.escalation_id || e.id) !== escalationId);
|
|
2741
|
+
renderEscalations();
|
|
2742
|
+
updateEscBadge();
|
|
2743
|
+
showToast('Response sent');
|
|
2744
|
+
} catch (e) {
|
|
2745
|
+
showToast('Failed to send response: ' + e.message, true);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
async function denyEscalation(escalationId) {
|
|
2750
|
+
if (!confirm('Deny this escalation? The contact will receive a timeout/fallback message.')) return;
|
|
2751
|
+
|
|
2752
|
+
try {
|
|
2753
|
+
const res = await fetch('/api/router/escalation-reply', {
|
|
2754
|
+
method: 'POST',
|
|
2755
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2756
|
+
body: JSON.stringify({ escalationId, response: '__DENY__' }),
|
|
2757
|
+
});
|
|
2758
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
2759
|
+
|
|
2760
|
+
escalations = escalations.filter(e => (e.escalation_id || e.id) !== escalationId);
|
|
2761
|
+
renderEscalations();
|
|
2762
|
+
updateEscBadge();
|
|
2763
|
+
showToast('Escalation denied');
|
|
2764
|
+
} catch (e) {
|
|
2765
|
+
showToast('Failed to deny escalation: ' + e.message, true);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// Poll for escalation badge count every 30s
|
|
2770
|
+
setInterval(async () => {
|
|
2771
|
+
try {
|
|
2772
|
+
const res = await fetch('/api/v2/escalations?limit=50');
|
|
2773
|
+
if (res.ok) {
|
|
2774
|
+
const data = await res.json();
|
|
2775
|
+
escalations = (data || []).filter(e => e.status === 'pending');
|
|
2776
|
+
updateEscBadge();
|
|
2777
|
+
if (currentPage === 'escalations') renderEscalations();
|
|
2778
|
+
}
|
|
2779
|
+
} catch (e) { /* silent */ }
|
|
2780
|
+
}, 30000);
|
|
2781
|
+
|
|
2782
|
+
// ---- Boot ----
|
|
2783
|
+
init();
|
|
2784
|
+
startEscTimers();
|
|
2785
|
+
// Initial escalation badge load
|
|
2786
|
+
loadEscalations();
|
|
2787
|
+
|
|
2788
|
+
// Embedded mode detection - if inside an iframe, switch to embedded layout
|
|
2789
|
+
if (window.self !== window.top) {
|
|
2790
|
+
document.querySelector('.app').classList.add('embedded');
|
|
2791
|
+
|
|
2792
|
+
// Create a bottom tab bar to replace the hidden sidebar
|
|
2793
|
+
const tabBar = document.createElement('div');
|
|
2794
|
+
tabBar.id = 'embeddedTabBar';
|
|
2795
|
+
tabBar.style.cssText = 'display:flex;background:var(--void);border-top:1px solid var(--border);padding:6px 0 env(safe-area-inset-bottom, 6px);position:fixed;bottom:0;left:0;right:0;z-index:200;';
|
|
2796
|
+
|
|
2797
|
+
const tabs = [
|
|
2798
|
+
{ page: 'contacts', label: 'Contacts', icon: '<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>' },
|
|
2799
|
+
{ page: 'escalations', label: 'Alerts', icon: '<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>' },
|
|
2800
|
+
{ page: 'faq', label: 'FAQ', icon: '<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3m.08 4h.01"/></svg>' },
|
|
2801
|
+
{ page: 'playbook', label: 'Playbook', icon: '<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>' },
|
|
2802
|
+
{ page: 'settings', label: 'Settings', icon: '<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg>' }
|
|
2803
|
+
];
|
|
2804
|
+
|
|
2805
|
+
tabs.forEach(t => {
|
|
2806
|
+
const btn = document.createElement('button');
|
|
2807
|
+
btn.style.cssText = 'flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;background:none;border:none;color:' + (t.page === 'contacts' ? 'var(--red)' : 'var(--text-muted)') + ';font-size:10px;font-family:inherit;cursor:pointer;padding:4px 0;';
|
|
2808
|
+
btn.innerHTML = t.icon + '<span>' + t.label + '</span>';
|
|
2809
|
+
btn.onclick = () => {
|
|
2810
|
+
showPage(t.page);
|
|
2811
|
+
tabBar.querySelectorAll('button').forEach(b => b.style.color = 'var(--text-muted)');
|
|
2812
|
+
btn.style.color = 'var(--red)';
|
|
2813
|
+
};
|
|
2814
|
+
tabBar.appendChild(btn);
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
document.body.appendChild(tabBar);
|
|
2818
|
+
|
|
2819
|
+
// Add bottom padding so content isn't hidden behind tab bar
|
|
2820
|
+
document.querySelector('.app').style.paddingBottom = '60px';
|
|
2821
|
+
}
|
|
2822
|
+
</script>
|
|
2823
|
+
</body>
|
|
2824
|
+
</html>
|