crewswarm 0.9.2 → 0.9.3

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.
Files changed (207) hide show
  1. package/README.md +22 -9
  2. package/apps/dashboard/dist/assets/{chat-core-Cx4sTxDd.js → chat-core-3KirthZA.js} +1 -1
  3. package/apps/dashboard/dist/assets/index-GSWxxEPO.js +2 -0
  4. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
  5. package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
  6. package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +1 -0
  7. package/apps/dashboard/dist/index.html +82 -11
  8. package/apps/vibe/README.md +2 -2
  9. package/apps/vibe/package.json +1 -1
  10. package/apps/vibe/server.mjs +3 -3
  11. package/crew-lead.mjs +34 -4
  12. package/lib/bridges/gateway-ws.mjs +4 -0
  13. package/lib/crew-lead/chat-handler.mjs +34 -0
  14. package/lib/crew-lead/http-server.mjs +55 -14
  15. package/lib/crew-lead/llm-caller.mjs +24 -8
  16. package/lib/crew-lead/prompts.mjs +7 -0
  17. package/lib/crew-lead/wave-dispatcher.mjs +15 -3
  18. package/lib/crew-lead/ws-router.mjs +219 -27
  19. package/lib/engines/engine-registry.mjs +9 -0
  20. package/lib/engines/rt-envelope.mjs +1 -0
  21. package/lib/engines/runners.mjs +5 -2
  22. package/lib/runtime/paths.mjs +12 -8
  23. package/package.json +35 -15
  24. package/scripts/capture-build-flow.mjs +118 -0
  25. package/scripts/coverage-report.mjs +209 -0
  26. package/scripts/coverage-summary.mjs +47 -0
  27. package/scripts/dashboard-validation.mjs +74 -0
  28. package/scripts/dashboard.mjs +560 -70
  29. package/scripts/live-bridge-matrix.mjs +79 -0
  30. package/scripts/live-cli-matrix.mjs +166 -0
  31. package/scripts/live-crewchat-check.mjs +42 -0
  32. package/scripts/live-engine-matrix.mjs +50 -0
  33. package/scripts/live-provider-failover-matrix.mjs +107 -0
  34. package/scripts/live-provider-matrix.mjs +228 -0
  35. package/scripts/restart-all-from-repo.sh +4 -4
  36. package/scripts/smoke-dispatch.mjs +4 -1
  37. package/scripts/test-blast-radius.mjs +204 -0
  38. package/scripts/test-report-summary.mjs +88 -0
  39. package/scripts/test-reporter.mjs +651 -0
  40. package/scripts/test-rerun.mjs +136 -0
  41. package/scripts/tmux-bridge +130 -0
  42. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  43. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  44. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  45. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  46. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  47. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  48. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  49. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  50. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  51. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  52. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  53. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  54. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  56. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  60. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  62. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  64. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  65. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  66. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  67. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  68. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  69. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  70. package/apps/dashboard/dist/index.html.br +0 -0
  71. package/apps/dashboard/dist/index.html.gz +0 -0
  72. package/apps/dashboard/index.html +0 -6529
  73. package/apps/dashboard/package.json +0 -15
  74. package/apps/dashboard/src/app.js +0 -2828
  75. package/apps/dashboard/src/app.js.br +0 -0
  76. package/apps/dashboard/src/app.js.gz +0 -0
  77. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  78. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  79. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  80. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  81. package/apps/dashboard/src/cli-process.js +0 -208
  82. package/apps/dashboard/src/cli-process.js.br +0 -0
  83. package/apps/dashboard/src/cli-process.js.gz +0 -0
  84. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  85. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  86. package/apps/dashboard/src/core/api.js +0 -18
  87. package/apps/dashboard/src/core/api.js.br +0 -0
  88. package/apps/dashboard/src/core/dom.js +0 -228
  89. package/apps/dashboard/src/core/dom.js.br +0 -0
  90. package/apps/dashboard/src/core/state.js +0 -91
  91. package/apps/dashboard/src/core/state.js.br +0 -0
  92. package/apps/dashboard/src/core/task-manager.js +0 -134
  93. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  94. package/apps/dashboard/src/orchestration-status.js +0 -127
  95. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  96. package/apps/dashboard/src/setup-wizard.js +0 -562
  97. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  98. package/apps/dashboard/src/styles.css +0 -2085
  99. package/apps/dashboard/src/styles.css.br +0 -0
  100. package/apps/dashboard/src/styles.css.gz +0 -0
  101. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  102. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  103. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  104. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  105. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  106. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  107. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  108. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  109. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  110. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  111. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  112. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  113. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  114. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  115. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  116. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  117. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  118. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  119. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  120. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  121. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  122. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  123. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  124. package/apps/dashboard/src/tabs/settings-tab.js +0 -861
  125. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  126. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  127. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  128. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  129. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  130. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  131. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  132. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  133. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  134. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  135. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  136. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  137. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  138. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  139. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  140. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  141. package/apps/vibe/.crew/cost.json +0 -17
  142. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  143. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  144. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  145. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  146. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  147. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  148. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  149. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  150. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  151. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  152. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  153. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  154. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  155. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  156. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  157. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  158. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  159. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  160. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  161. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  162. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  163. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  164. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  172. package/apps/vibe/.crew/sandbox.json +0 -7
  173. package/apps/vibe/.crew/session.json +0 -330
  174. package/apps/vibe/.crew/training-data.jsonl +0 -0
  175. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  176. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  177. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  178. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  179. package/apps/vibe/ARCHITECTURE.md +0 -3393
  180. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  181. package/apps/vibe/ROADMAP.md +0 -41
  182. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  183. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  184. package/apps/vibe/capture-demo.mjs +0 -160
  185. package/apps/vibe/capture-full-demo.mjs +0 -255
  186. package/apps/vibe/capture-quickstart.mjs +0 -256
  187. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  188. package/apps/vibe/capture-vibe-video.mjs +0 -260
  189. package/apps/vibe/check-buttons.js +0 -41
  190. package/apps/vibe/diagnose.html +0 -106
  191. package/apps/vibe/fix-buttons.js +0 -103
  192. package/apps/vibe/index.html +0 -3404
  193. package/apps/vibe/package-lock.json +0 -920
  194. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  195. package/apps/vibe/src/main.js +0 -2940
  196. package/apps/vibe/src/register-all-languages.js +0 -98
  197. package/apps/vibe/start-studio.sh +0 -11
  198. package/apps/vibe/test/accessibility-tests.js +0 -77
  199. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  200. package/apps/vibe/test/performance-tests.js +0 -120
  201. package/apps/vibe/test/security-tests.js +0 -213
  202. package/apps/vibe/tests/e2e.local.mjs +0 -54
  203. package/apps/vibe/tests/server.smoke.mjs +0 -106
  204. package/apps/vibe/update_website.mjs +0 -74
  205. package/apps/vibe/vite.config.js +0 -19
  206. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  207. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -1,654 +0,0 @@
1
- /**
2
- * Contacts Tab — Unified contact management across WhatsApp, Telegram, etc.
3
- * Deps: getJSON, postJSON (core/api), escHtml, showNotification (core/dom), state (core/state)
4
- */
5
-
6
- import { getJSON, postJSON } from '../core/api.js';
7
- import { escHtml, showNotification } from '../core/dom.js';
8
- import { state } from '../core/state.js';
9
-
10
- let _contactsData = {};
11
- let _allContacts = [];
12
- let _currentFilters = {
13
- search: '',
14
- platform: '',
15
- sortBy: 'name',
16
- letter: 'all'
17
- };
18
-
19
- export function showContacts() {
20
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
21
- document.getElementById('contactsView').classList.add('active');
22
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
23
- const nav = document.getElementById('navContacts');
24
- if (nav) nav.classList.add('active');
25
- loadContacts();
26
- }
27
-
28
- // ── Load contacts ─────────────────────────────────────────────────────────────
29
-
30
- export async function loadContacts() {
31
- const list = document.getElementById('contactsList');
32
-
33
- list.innerHTML = '<div class="meta" style="padding:20px;">Loading contacts...</div>';
34
-
35
- try {
36
- const data = await getJSON('/api/contacts');
37
- const contacts = data.contacts || [];
38
-
39
- _contactsData = {};
40
- contacts.forEach(c => { _contactsData[c.contact_id] = c; });
41
- _allContacts = contacts;
42
-
43
- if (!contacts.length) {
44
- list.innerHTML = '<div class="meta" style="padding:20px;">No contacts yet. Click "+ New Contact" to add manually, or contacts are created automatically when someone messages the bot.</div>';
45
- document.getElementById('contactsCount').innerHTML = '';
46
- return;
47
- }
48
-
49
- applyFiltersAndRender();
50
-
51
- } catch(e) {
52
- list.innerHTML = '<div class="meta" style="padding:20px;color:var(--red-hi);">Failed to load contacts: ' + escHtml(e.message) + '</div>';
53
- }
54
- }
55
-
56
- // ── Filtering and Sorting ─────────────────────────────────────────────────────
57
-
58
- export function applyContactFilters() {
59
- const searchInput = document.getElementById('contactsSearch');
60
- const platformFilter = document.getElementById('contactsPlatformFilter');
61
- const sortBy = document.getElementById('contactsSortBy');
62
-
63
- _currentFilters.search = searchInput.value.toLowerCase().trim();
64
- _currentFilters.platform = platformFilter.value;
65
- _currentFilters.sortBy = sortBy.value;
66
-
67
- applyFiltersAndRender();
68
- }
69
-
70
- export function filterByLetter(letter) {
71
- _currentFilters.letter = letter;
72
-
73
- // Update active button
74
- document.querySelectorAll('.alpha-filter').forEach(btn => {
75
- btn.classList.remove('active');
76
- btn.style.background = 'var(--bg-1)';
77
- btn.style.color = 'var(--text-2)';
78
- });
79
- const activeBtn = document.querySelector(`[data-letter="${letter}"]`);
80
- if (activeBtn) {
81
- activeBtn.classList.add('active');
82
- activeBtn.style.background = 'var(--purple)';
83
- activeBtn.style.color = '#fff';
84
- }
85
-
86
- applyFiltersAndRender();
87
- }
88
-
89
- function applyFiltersAndRender() {
90
- let filtered = [..._allContacts];
91
-
92
- // Filter by search term
93
- if (_currentFilters.search) {
94
- filtered = filtered.filter(c => {
95
- const searchText = `${c.display_name} ${c.phone_number || ''} ${c.email || ''} ${c.notes || ''} ${JSON.stringify(c.preferences)} ${JSON.stringify(c.tags)}`.toLowerCase();
96
- return searchText.includes(_currentFilters.search);
97
- });
98
- }
99
-
100
- // Filter by platform
101
- if (_currentFilters.platform) {
102
- filtered = filtered.filter(c => {
103
- if (c.platform === _currentFilters.platform) return true;
104
- const links = c.platform_links || {};
105
- return !!links[_currentFilters.platform];
106
- });
107
- }
108
-
109
- // Filter by letter
110
- if (_currentFilters.letter !== 'all') {
111
- filtered = filtered.filter(c => {
112
- const firstChar = (c.display_name || '').charAt(0).toUpperCase();
113
- if (_currentFilters.letter === '#') {
114
- return !/[A-Z]/.test(firstChar);
115
- }
116
- return firstChar === _currentFilters.letter;
117
- });
118
- }
119
-
120
- // Sort
121
- if (_currentFilters.sortBy === 'name') {
122
- filtered.sort((a, b) => {
123
- const nameA = (a.display_name || a.phone_number || '').toLowerCase();
124
- const nameB = (b.display_name || b.phone_number || '').toLowerCase();
125
- return nameA.localeCompare(nameB);
126
- });
127
- } else if (_currentFilters.sortBy === 'recent') {
128
- filtered.sort((a, b) => b.last_seen - a.last_seen);
129
- } else if (_currentFilters.sortBy === 'messages') {
130
- filtered.sort((a, b) => b.message_count - a.message_count);
131
- }
132
-
133
- // Update count
134
- const countEl = document.getElementById('contactsCount');
135
- if (countEl) {
136
- const total = _allContacts.length;
137
- const showing = filtered.length;
138
- if (showing === total) {
139
- countEl.innerHTML = `Showing all ${total} contacts`;
140
- } else {
141
- countEl.innerHTML = `Showing ${showing} of ${total} contacts`;
142
- }
143
- }
144
-
145
- renderContactsList(filtered);
146
- }
147
-
148
- // ── Render ────────────────────────────────────────────────────────────────────
149
-
150
- function renderContactsList(contacts) {
151
- const list = document.getElementById('contactsList');
152
-
153
- if (!contacts.length) {
154
- list.innerHTML = '<div class="meta" style="padding:20px;">No contacts match your filters.</div>';
155
- return;
156
- }
157
-
158
- list.innerHTML = contacts.map(c => {
159
- const id = c.contact_id;
160
- const safeId = escHtml(id);
161
- const name = escHtml(c.display_name || c.phone_number || 'Unknown');
162
- const platformLinks = c.platform_links || {};
163
- const preferences = c.preferences || {};
164
- const tags = c.tags || [];
165
-
166
- // Platform badges
167
- const platforms = [];
168
- if (c.platform === 'whatsapp' || platformLinks.whatsapp) platforms.push('<span style="background:#25D366;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">WhatsApp</span>');
169
- if (c.platform === 'telegram' || platformLinks.telegram) platforms.push('<span style="background:#0088cc;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Telegram</span>');
170
- if (c.platform === 'twitter' || platformLinks.twitter) platforms.push('<span style="background:#1DA1F2;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Twitter</span>');
171
- if (c.platform === 'instagram' || platformLinks.instagram) platforms.push('<span style="background:#E4405F;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Instagram</span>');
172
- if (c.platform === 'tiktok' || platformLinks.tiktok) platforms.push('<span style="background:#000000;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">TikTok</span>');
173
- if (c.platform === 'slack' || platformLinks.slack) platforms.push('<span style="background:#4A154B;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Slack</span>');
174
- if (c.platform === 'web' || platformLinks.web) platforms.push('<span style="background:#666;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Web</span>');
175
- if (c.platform === 'website' || platformLinks.website) platforms.push('<span style="background:#666;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Website</span>');
176
-
177
- // Last seen
178
- const lastSeenText = formatRelativeTime(c.last_seen);
179
-
180
- // Preferences preview (compact)
181
- const prefPreview = [];
182
- if (preferences?.diet) prefPreview.push(`🍴 ${escHtml(preferences.diet)}`);
183
- if (preferences?.allergies?.length) prefPreview.push(`⚠️ ${escHtml(preferences.allergies.join(', '))}`);
184
- if (preferences?.spiceLevel) prefPreview.push(`🌶️ ${escHtml(preferences.spiceLevel)}`);
185
-
186
- // Tags
187
- const tagHtml = tags.length ? tags.map(t => `<span style="background:var(--bg-1);color:var(--text-3);padding:2px 6px;border-radius:4px;font-size:10px;">${escHtml(t)}</span>`).join(' ') : '';
188
-
189
- // Expandable details
190
- const phone = c.phone_number ? `<div><strong>Phone:</strong> ${escHtml(c.phone_number)}</div>` : '';
191
- const email = c.email ? `<div><strong>Email:</strong> ${escHtml(c.email)}</div>` : '';
192
- const notes = c.notes ? `<div style="margin-top:8px;"><strong>Notes:</strong> ${escHtml(c.notes)}</div>` : '';
193
- const allPrefs = preferences && Object.keys(preferences).length > 0
194
- ? `<div style="margin-top:8px;"><strong>Preferences:</strong> <pre style="font-size:11px;margin-top:4px;background:var(--bg-1);padding:8px;border-radius:4px;overflow:auto;">${escHtml(JSON.stringify(preferences, null, 2))}</pre></div>`
195
- : '';
196
-
197
- return `
198
- <div class="card contact-card" id="contact-card-${safeId}" data-contact-id="${safeId}">
199
- <div id="contact-view-${safeId}">
200
- <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
201
- <div style="flex:1;">
202
- <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px;">
203
- <strong style="font-size:15px;">${name}</strong>
204
- ${platforms.join(' ')}
205
- </div>
206
- <div class="meta" style="font-size:12px;">
207
- ${c.message_count} messages · Last seen: ${lastSeenText}
208
- </div>
209
- </div>
210
- </div>
211
-
212
- ${prefPreview.length ? `<div style="margin-bottom:10px;font-size:12px;color:var(--text-2);">${prefPreview.join(' · ')}</div>` : ''}
213
- ${tagHtml ? `<div style="margin-bottom:10px;display:flex;gap:4px;flex-wrap:wrap;">${tagHtml}</div>` : ''}
214
-
215
- <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
216
- <button data-action="send-message" data-id="${safeId}" class="btn-green" style="font-size:12px;">💬 Send Message</button>
217
- <button data-action="toggle-details" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">📋 Details</button>
218
- <button data-action="edit" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">✏️ Edit</button>
219
- <button data-action="delete" data-id="${safeId}" style="background:transparent;color:var(--text-3);border:1px solid var(--border);border-radius:6px;padding:4px 10px;cursor:pointer;font-size:12px;">🗑 Delete</button>
220
- </div>
221
- </div>
222
-
223
- <!-- Expandable details -->
224
- <div id="contact-details-${safeId}" style="display:none;margin-top:14px;padding-top:14px;border-top:1px solid var(--border);font-size:13px;color:var(--text-2);">
225
- ${phone}
226
- ${email}
227
- ${notes}
228
- ${allPrefs}
229
- </div>
230
-
231
- <!-- Edit form -->
232
- <div id="contact-edit-${safeId}" style="display:none;padding:12px;border-top:1px solid var(--border);margin-top:12px;">
233
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Display Name</label><input id="contact-name-${safeId}" type="text" value="${escHtml(c.display_name || '')}" style="margin-top:4px;width:100%;" /></div>
234
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Phone</label><input id="contact-phone-${safeId}" type="text" value="${escHtml(c.phone_number || '')}" style="margin-top:4px;width:100%;" /></div>
235
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Email</label><input id="contact-email-${safeId}" type="text" value="${escHtml(c.email || '')}" style="margin-top:4px;width:100%;" /></div>
236
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">📍 Location</label><input id="contact-location-${safeId}" type="text" value="${escHtml(c.last_location || '')}" placeholder="Grand Bend, Ontario, Canada" style="margin-top:4px;width:100%;" /></div>
237
- <div style="margin-bottom:12px;padding:12px;background:var(--bg-1);border-radius:6px;">
238
- <label style="font-size:12px;color:var(--text-2);display:block;margin-bottom:8px;font-weight:600;">🔗 Platform IDs</label>
239
- <div style="font-size:10px;color:var(--text-3);margin-bottom:12px;">Primary: ${escHtml(c.contact_id)}</div>
240
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
241
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Telegram</label><input id="platform-telegram-${safeId}" type="text" placeholder="12345678" value="${escHtml(platformLinks.telegram || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
242
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">WhatsApp</label><input id="platform-whatsapp-${safeId}" type="text" placeholder="1234@s.whatsapp.net" value="${escHtml(platformLinks.whatsapp || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
243
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Twitter</label><input id="platform-twitter-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.twitter || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
244
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Instagram</label><input id="platform-instagram-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.instagram || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
245
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">TikTok</label><input id="platform-tiktok-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.tiktok || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
246
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Slack</label><input id="platform-slack-${safeId}" type="text" placeholder="U01234ABC" value="${escHtml(platformLinks.slack || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
247
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Website</label><input id="platform-website-${safeId}" type="text" placeholder="https://example.com" value="${escHtml(platformLinks.website || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
248
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Other</label><input id="platform-other-${safeId}" type="text" placeholder="Custom ID" value="${escHtml(platformLinks.other || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
249
- </div>
250
- </div>
251
-
252
- <!-- Preferences Section -->
253
- <div style="margin-bottom:12px;padding:12px;background:var(--bg-1);border-radius:6px;">
254
- <label style="font-size:12px;color:var(--text-2);display:block;margin-bottom:8px;font-weight:600;">🍴 Preferences</label>
255
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
256
- <div>
257
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Diet</label>
258
- <select id="pref-diet-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
259
- <option value="">None</option>
260
- <option value="omnivore" ${preferences?.diet === 'omnivore' ? 'selected' : ''}>Omnivore</option>
261
- <option value="vegetarian" ${preferences?.diet === 'vegetarian' ? 'selected' : ''}>Vegetarian</option>
262
- <option value="vegan" ${preferences?.diet === 'vegan' ? 'selected' : ''}>Vegan</option>
263
- <option value="pescatarian" ${preferences?.diet === 'pescatarian' ? 'selected' : ''}>Pescatarian</option>
264
- </select>
265
- </div>
266
- <div>
267
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Spice Level</label>
268
- <select id="pref-spice-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
269
- <option value="">Any</option>
270
- <option value="mild" ${preferences?.spiceLevel === 'mild' ? 'selected' : ''}>Mild</option>
271
- <option value="medium" ${preferences?.spiceLevel === 'medium' ? 'selected' : ''}>Medium</option>
272
- <option value="hot" ${preferences?.spiceLevel === 'hot' ? 'selected' : ''}>Hot</option>
273
- </select>
274
- </div>
275
- <div>
276
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Budget</label>
277
- <select id="pref-budget-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
278
- <option value="">Any</option>
279
- <option value="budget" ${preferences?.budget === 'budget' ? 'selected' : ''}>Budget ($)</option>
280
- <option value="moderate" ${preferences?.budget === 'moderate' ? 'selected' : ''}>Moderate ($$)</option>
281
- <option value="upscale" ${preferences?.budget === 'upscale' ? 'selected' : ''}>Upscale ($$$)</option>
282
- </select>
283
- </div>
284
- <div>
285
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Atmosphere</label>
286
- <select id="pref-atmosphere-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
287
- <option value="">Any</option>
288
- <option value="quiet" ${preferences?.atmosphere === 'quiet' ? 'selected' : ''}>Quiet</option>
289
- <option value="lively" ${preferences?.atmosphere === 'lively' ? 'selected' : ''}>Lively</option>
290
- <option value="romantic" ${preferences?.atmosphere === 'romantic' ? 'selected' : ''}>Romantic</option>
291
- <option value="family-friendly" ${preferences?.atmosphere === 'family-friendly' ? 'selected' : ''}>Family-friendly</option>
292
- </select>
293
- </div>
294
- <div style="grid-column:1/-1;">
295
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Allergies (comma-separated)</label>
296
- <input id="pref-allergies-${safeId}" type="text" placeholder="shellfish, peanuts, gluten, dairy" value="${escHtml((preferences?.allergies || []).join(', '))}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" />
297
- </div>
298
- <div style="grid-column:1/-1;">
299
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Favorite Cuisines (comma-separated)</label>
300
- <input id="pref-cuisines-${safeId}" type="text" placeholder="Thai, Mexican, Italian, Japanese" value="${escHtml((preferences?.favCuisines || []).join(', '))}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" />
301
- </div>
302
- </div>
303
- </div>
304
-
305
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Notes</label><textarea id="contact-notes-${safeId}" rows="3" style="margin-top:4px;width:100%;">${escHtml(c.notes || '')}</textarea></div>
306
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Tags (comma-separated)</label><input id="contact-tags-${safeId}" type="text" value="${escHtml(tags.join(', '))}" style="margin-top:4px;width:100%;" /></div>
307
- <div style="display:flex;gap:8px;">
308
- <button data-action="save-edit" data-id="${safeId}" class="btn-green" style="font-size:12px;">💾 Save</button>
309
- <button data-action="cancel-edit" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">Cancel</button>
310
- </div>
311
- </div>
312
- </div>
313
- `;
314
- }).join('');
315
- }
316
-
317
- // ── Actions ───────────────────────────────────────────────────────────────────
318
-
319
- export function toggleContactDetails(contactId) {
320
- const detailsEl = document.getElementById('contact-details-' + contactId);
321
- if (!detailsEl) return;
322
- const isVisible = detailsEl.style.display !== 'none';
323
- detailsEl.style.display = isVisible ? 'none' : 'block';
324
- }
325
-
326
- export function toggleContactEdit(contactId) {
327
- const viewEl = document.getElementById('contact-view-' + contactId);
328
- const editEl = document.getElementById('contact-edit-' + contactId);
329
- const detailsEl = document.getElementById('contact-details-' + contactId);
330
- if (!viewEl || !editEl) return;
331
- const isEditing = editEl.style.display !== 'none';
332
- viewEl.style.display = isEditing ? '' : 'none';
333
- editEl.style.display = isEditing ? 'none' : 'block';
334
- if (detailsEl) detailsEl.style.display = 'none';
335
- }
336
-
337
- export async function saveContactEdit(contactId) {
338
- const name = document.getElementById('contact-name-' + contactId)?.value?.trim();
339
- const phone = document.getElementById('contact-phone-' + contactId)?.value?.trim();
340
- const email = document.getElementById('contact-email-' + contactId)?.value?.trim();
341
- const location = document.getElementById('contact-location-' + contactId)?.value?.trim();
342
- const notes = document.getElementById('contact-notes-' + contactId)?.value?.trim();
343
- const tagsStr = document.getElementById('contact-tags-' + contactId)?.value?.trim();
344
- const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : [];
345
-
346
- // Collect platform IDs from individual fields
347
- const platformLinks = {};
348
- const telegram = document.getElementById('platform-telegram-' + contactId)?.value?.trim();
349
- const whatsapp = document.getElementById('platform-whatsapp-' + contactId)?.value?.trim();
350
- const twitter = document.getElementById('platform-twitter-' + contactId)?.value?.trim();
351
- const instagram = document.getElementById('platform-instagram-' + contactId)?.value?.trim();
352
- const tiktok = document.getElementById('platform-tiktok-' + contactId)?.value?.trim();
353
- const slack = document.getElementById('platform-slack-' + contactId)?.value?.trim();
354
- const website = document.getElementById('platform-website-' + contactId)?.value?.trim();
355
- const other = document.getElementById('platform-other-' + contactId)?.value?.trim();
356
-
357
- if (telegram) platformLinks.telegram = telegram;
358
- if (whatsapp) platformLinks.whatsapp = whatsapp;
359
- if (twitter) platformLinks.twitter = twitter;
360
- if (instagram) platformLinks.instagram = instagram;
361
- if (tiktok) platformLinks.tiktok = tiktok;
362
- if (slack) platformLinks.slack = slack;
363
- if (website) platformLinks.website = website;
364
- if (other) platformLinks.other = other;
365
-
366
- // Collect preferences from form fields
367
- const preferences = {};
368
- const diet = document.getElementById('pref-diet-' + contactId)?.value?.trim();
369
- const spice = document.getElementById('pref-spice-' + contactId)?.value?.trim();
370
- const budget = document.getElementById('pref-budget-' + contactId)?.value?.trim();
371
- const atmosphere = document.getElementById('pref-atmosphere-' + contactId)?.value?.trim();
372
- const allergiesStr = document.getElementById('pref-allergies-' + contactId)?.value?.trim();
373
- const cuisinesStr = document.getElementById('pref-cuisines-' + contactId)?.value?.trim();
374
-
375
- if (diet) preferences.diet = diet;
376
- if (spice) preferences.spiceLevel = spice;
377
- if (budget) preferences.budget = budget;
378
- if (atmosphere) preferences.atmosphere = atmosphere;
379
- if (allergiesStr) preferences.allergies = allergiesStr.split(',').map(a => a.trim()).filter(Boolean);
380
- if (cuisinesStr) preferences.favCuisines = cuisinesStr.split(',').map(c => c.trim()).filter(Boolean);
381
-
382
- try {
383
- await postJSON('/api/contacts/update', {
384
- contactId,
385
- display_name: name,
386
- phone_number: phone,
387
- email,
388
- last_location: location,
389
- notes,
390
- tags,
391
- platform_links: platformLinks,
392
- preferences
393
- });
394
- showNotification('Contact saved');
395
- toggleContactEdit(contactId);
396
- loadContacts();
397
- } catch(e) {
398
- showNotification('Failed: ' + e.message, true);
399
- }
400
- }
401
-
402
- export async function deleteContact(contactId) {
403
- const contact = _contactsData[contactId];
404
- const name = contact ? contact.display_name : contactId;
405
- if (!confirm(`Delete contact "${name}"?\n\nMessage history will also be deleted.`)) return;
406
-
407
- try {
408
- await postJSON('/api/contacts/delete', { contactId });
409
- showNotification('Contact deleted');
410
- delete _contactsData[contactId];
411
- _allContacts = _allContacts.filter(c => c.contact_id !== contactId);
412
- applyFiltersAndRender();
413
- } catch(e) {
414
- showNotification('Failed: ' + e.message, true);
415
- }
416
- }
417
-
418
- // ── New Contact Modal ─────────────────────────────────────────────────────────
419
-
420
- export function newContact() {
421
- const modal = document.getElementById('modalOverlay') || createModalOverlay();
422
-
423
- modal.innerHTML = `
424
- <div style="background:var(--bg-card);border-radius:12px;padding:24px;max-width:500px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4);">
425
- <h3 style="margin:0 0 16px 0;">➕ New Contact</h3>
426
-
427
- <div style="margin-bottom:12px;">
428
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform *</label>
429
- <select id="newContactPlatform" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;">
430
- <option value="telegram">Telegram</option>
431
- <option value="whatsapp">WhatsApp</option>
432
- <option value="twitter">Twitter</option>
433
- <option value="instagram">Instagram</option>
434
- <option value="tiktok">TikTok</option>
435
- <option value="slack">Slack</option>
436
- <option value="website">Website</option>
437
- <option value="web">Web</option>
438
- </select>
439
- </div>
440
-
441
- <div style="margin-bottom:12px;">
442
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform ID *</label>
443
- <input id="newContactId" type="text" placeholder="12345678 (Telegram), @username (Twitter/IG/TikTok), URL (Website)" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
444
- <div style="font-size:10px;color:var(--text-3);margin-top:4px;">
445
- • Telegram: chat ID (e.g., 12345678)<br>
446
- • WhatsApp: phone@s.whatsapp.net (e.g., 15551234567@s.whatsapp.net)<br>
447
- • Twitter/Instagram/TikTok: @username<br>
448
- • Website: URL (e.g., https://example.com)<br>
449
- • Slack: user ID (e.g., U01234ABC)
450
- </div>
451
- </div>
452
-
453
- <div style="margin-bottom:12px;">
454
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Display Name *</label>
455
- <input id="newContactName" type="text" placeholder="John Doe" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
456
- </div>
457
-
458
- <div style="margin-bottom:12px;">
459
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Phone</label>
460
- <input id="newContactPhone" type="text" placeholder="+1 310 905 0857" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
461
- </div>
462
-
463
- <div style="margin-bottom:12px;">
464
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Email</label>
465
- <input id="newContactEmail" type="text" placeholder="john@example.com" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
466
- </div>
467
-
468
- <div style="margin-bottom:16px;">
469
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Notes</label>
470
- <textarea id="newContactNotes" rows="3" placeholder="Important client, prefers morning calls..." style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;"></textarea>
471
- </div>
472
-
473
- <div style="display:flex;gap:8px;justify-content:flex-end;">
474
- <button onclick="document.getElementById('modalOverlay').style.display='none'" class="btn-ghost">Cancel</button>
475
- <button onclick="window.createContact()" class="btn-green">Create Contact</button>
476
- </div>
477
- </div>
478
- `;
479
-
480
- modal.style.display = 'flex';
481
- }
482
-
483
- export async function createContact() {
484
- const platform = document.getElementById('newContactPlatform').value;
485
- const platformId = document.getElementById('newContactId').value.trim();
486
- const displayName = document.getElementById('newContactName').value.trim();
487
- const phone = document.getElementById('newContactPhone').value.trim();
488
- const email = document.getElementById('newContactEmail').value.trim();
489
- const notes = document.getElementById('newContactNotes').value.trim();
490
-
491
- if (!platform || !platformId || !displayName) {
492
- showNotification('Platform, Platform ID, and Display Name are required', true);
493
- return;
494
- }
495
-
496
- const contactId = `${platform}:${platformId}`;
497
-
498
- try {
499
- await postJSON('/api/contacts/create', {
500
- contact_id: contactId,
501
- platform,
502
- display_name: displayName,
503
- phone_number: phone || null,
504
- email: email || null,
505
- notes: notes || null
506
- });
507
- showNotification('✅ Contact created');
508
- document.getElementById('modalOverlay').style.display = 'none';
509
- loadContacts();
510
- } catch(e) {
511
- showNotification('Failed: ' + e.message, true);
512
- }
513
- }
514
-
515
- // ── Send Message Modal ────────────────────────────────────────────────────────
516
-
517
- export function showSendMessageModal(contactId) {
518
- const contact = _contactsData[contactId];
519
- if (!contact) {
520
- showNotification('Contact not found', true);
521
- return;
522
- }
523
-
524
- const modal = document.getElementById('modalOverlay') || createModalOverlay();
525
-
526
- const platformLinks = contact.platform_links || {};
527
- const hasWhatsApp = contact.platform === 'whatsapp' || platformLinks.whatsapp;
528
- const hasTelegram = contact.platform === 'telegram' || platformLinks.telegram;
529
-
530
- modal.innerHTML = `
531
- <div style="background:var(--bg-card);border-radius:12px;padding:24px;max-width:500px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4);">
532
- <h3 style="margin:0 0 16px 0;">💬 Send Message</h3>
533
- <div style="margin-bottom:12px;">
534
- <div style="font-size:13px;color:var(--text-2);margin-bottom:8px;">To: <strong>${escHtml(contact.display_name || contact.contact_id)}</strong></div>
535
- </div>
536
- <div style="margin-bottom:12px;">
537
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform</label>
538
- <select id="sendMessagePlatform" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;">
539
- ${hasWhatsApp ? '<option value="whatsapp">WhatsApp</option>' : ''}
540
- ${hasTelegram ? '<option value="telegram">Telegram</option>' : ''}
541
- ${hasWhatsApp && hasTelegram ? '<option value="both">Both</option>' : ''}
542
- </select>
543
- </div>
544
- <div style="margin-bottom:16px;">
545
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Message</label>
546
- <textarea id="sendMessageText" rows="4" placeholder="Your message here..." style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;"></textarea>
547
- </div>
548
- <div style="display:flex;gap:8px;justify-content:flex-end;">
549
- <button onclick="document.getElementById('modalOverlay').style.display='none'" class="btn-ghost">Cancel</button>
550
- <button onclick="window.sendContactMessage('${escHtml(contactId)}')" class="btn-green">Send</button>
551
- </div>
552
- </div>
553
- `;
554
-
555
- modal.style.display = 'flex';
556
- }
557
-
558
- export async function sendContactMessage(contactId) {
559
- const platform = document.getElementById('sendMessagePlatform').value;
560
- const message = document.getElementById('sendMessageText').value.trim();
561
-
562
- if (!message) {
563
- showNotification('Message cannot be empty', true);
564
- return;
565
- }
566
-
567
- try {
568
- await postJSON('/api/contacts/send', { contactId, platform, message });
569
- showNotification('✅ Message sent');
570
- document.getElementById('modalOverlay').style.display = 'none';
571
- } catch(e) {
572
- showNotification('Failed: ' + e.message, true);
573
- }
574
- }
575
-
576
- // ── Helpers ───────────────────────────────────────────────────────────────────
577
-
578
- function createModalOverlay() {
579
- let modal = document.getElementById('modalOverlay');
580
- if (!modal) {
581
- modal = document.createElement('div');
582
- modal.id = 'modalOverlay';
583
- modal.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:9999;align-items:center;justify-content:center;';
584
- modal.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; };
585
- document.body.appendChild(modal);
586
- }
587
- return modal;
588
- }
589
-
590
- function formatRelativeTime(timestamp) {
591
- const now = Date.now();
592
- const diff = now - timestamp;
593
- const minutes = Math.floor(diff / 60000);
594
- const hours = Math.floor(diff / 3600000);
595
- const days = Math.floor(diff / 86400000);
596
-
597
- if (minutes < 1) return 'just now';
598
- if (minutes < 60) return `${minutes}m ago`;
599
- if (hours < 24) return `${hours}h ago`;
600
- if (days < 7) return `${days}d ago`;
601
- return new Date(timestamp).toLocaleDateString();
602
- }
603
-
604
- // ── Event Handlers ────────────────────────────────────────────────────────────
605
-
606
- export function initContactsList() {
607
- document.addEventListener('click', (e) => {
608
- const action = e.target.getAttribute('data-action');
609
- const id = e.target.getAttribute('data-id');
610
- const letter = e.target.getAttribute('data-letter');
611
-
612
- if (action === 'toggle-details') toggleContactDetails(id);
613
- else if (action === 'edit') toggleContactEdit(id);
614
- else if (action === 'save-edit') saveContactEdit(id);
615
- else if (action === 'cancel-edit') toggleContactEdit(id);
616
- else if (action === 'delete') deleteContact(id);
617
- else if (action === 'send-message') showSendMessageModal(id);
618
- else if (action === 'newContact') newContact();
619
- else if (action === 'applyContactFilters') applyContactFilters();
620
- else if (action === 'filterByLetter') filterByLetter(letter);
621
- else if (action === 'loadContacts') loadContacts();
622
- });
623
-
624
- // Real-time search
625
- const searchInput = document.getElementById('contactsSearch');
626
- if (searchInput) {
627
- searchInput.addEventListener('input', () => {
628
- _currentFilters.search = searchInput.value.toLowerCase().trim();
629
- applyFiltersAndRender();
630
- });
631
- }
632
-
633
- // Platform filter change
634
- const platformFilter = document.getElementById('contactsPlatformFilter');
635
- if (platformFilter) {
636
- platformFilter.addEventListener('change', applyContactFilters);
637
- }
638
-
639
- // Sort change
640
- const sortBy = document.getElementById('contactsSortBy');
641
- if (sortBy) {
642
- sortBy.addEventListener('change', applyContactFilters);
643
- }
644
- }
645
-
646
- // Export to window for HTML onclick handlers
647
- if (typeof window !== 'undefined') {
648
- window.loadContacts = loadContacts;
649
- window.searchContacts = applyContactFilters;
650
- window.newContact = newContact;
651
- window.createContact = createContact;
652
- window.sendContactMessage = sendContactMessage;
653
- window.filterByLetter = filterByLetter;
654
- }