main
js 291 lines 7.89 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import * as API from "/js/api.js";
3
4 const API_BASE = "/plugins/_whatsapp_integration";
5 const STEPS = [
6 {
7 title: "Pair your account and set access",
8 description: "Turn on WhatsApp, connect the account, and choose who can reach it.",
9 },
10 {
11 title: "Choose where conversations go",
12 description: "Pick a project if you want one and shape how the agent should reply.",
13 },
14 ];
15
16 function ensureConfig(config) {
17 if (!config || typeof config !== "object") return;
18 if (typeof config.enabled !== "boolean") config.enabled = false;
19 if (!config.mode) config.mode = "self-chat";
20 if (!config.bridge_port) config.bridge_port = 3100;
21 if (!config.poll_interval_seconds) config.poll_interval_seconds = 3;
22 if (typeof config.allow_group !== "boolean") config.allow_group = false;
23
24 if (Array.isArray(config.allowed_numbers)) return;
25 if (typeof config.allowed_numbers === "string") {
26 config.allowed_numbers = config.allowed_numbers
27 .split(",")
28 .map((item) => item.trim())
29 .filter((item) => item);
30 return;
31 }
32 config.allowed_numbers = [];
33 }
34
35 export const store = createStore("whatsappConfig", {
36 config: null,
37 projects: [],
38 guideOpen: false,
39 currentStep: 0,
40 testing: false,
41 testResults: null,
42 qrVisible: false,
43 qrStatus: "",
44 qrMessage: "",
45 qrDataUrl: null,
46 qrPollTimer: null,
47 disconnecting: false,
48 disconnectMessage: "",
49 steps: STEPS,
50 _projectsLoaded: false,
51 context: null,
52
53 get showFooterNav() {
54 return true;
55 },
56
57 get isFirstStep() {
58 return this.currentStep === 0;
59 },
60
61 get isLastStep() {
62 return this.currentStep >= this.steps.length - 1;
63 },
64
65 get nextButtonLabel() {
66 return this.isLastStep ? "Done" : "Next";
67 },
68
69 get footerStepLabel() {
70 return `Step ${this.currentStep + 1} of ${this.steps.length}`;
71 },
72
73 async init(config, context = null) {
74 this.config = config || null;
75 this.context = context;
76 ensureConfig(this.config);
77 this.guideOpen = !this.hasMeaningfulConfig() && window.innerWidth > 720;
78 this.currentStep = 0;
79 this.testing = false;
80 this.testResults = null;
81 this._installWizardFooter();
82
83 if (this._projectsLoaded) return;
84 try {
85 const response = await API.callJsonApi("projects", { action: "list" });
86 this.projects = response.data || [];
87 } catch (_) {
88 this.projects = [];
89 }
90 this._projectsLoaded = true;
91 },
92
93 cleanup() {
94 if (this.context?.wizardFooter?.owner === "whatsappConfig") {
95 this.context.wizardFooter = null;
96 }
97 this.hideQr();
98 this.config = null;
99 this.context = null;
100 this.guideOpen = false;
101 this.currentStep = 0;
102 this.testing = false;
103 this.testResults = null;
104 this.disconnecting = false;
105 this.disconnectMessage = "";
106 },
107
108 currentStepMeta() {
109 return this.steps[this.currentStep] || this.steps[0];
110 },
111
112 setStep(step) {
113 this.currentStep = Math.max(0, Math.min(this.steps.length - 1, Number(step) || 0));
114 },
115
116 nextStep() {
117 if (!this.isLastStep) this.currentStep += 1;
118 },
119
120 previousStep() {
121 if (!this.isFirstStep) this.currentStep -= 1;
122 },
123
124 hasMeaningfulConfig() {
125 if (!this.config) return false;
126 return !!(
127 this.config.enabled
128 || String(this.config.project || "").trim()
129 || String(this.config.agent_instructions || "").trim()
130 || (Array.isArray(this.config.allowed_numbers) && this.config.allowed_numbers.length > 0)
131 );
132 },
133
134 allowedText() {
135 ensureConfig(this.config);
136 return (this.config?.allowed_numbers || []).join(", ");
137 },
138
139 allowedIsEmpty() {
140 ensureConfig(this.config);
141 return (this.config?.allowed_numbers || []).length === 0;
142 },
143
144 setAllowed(value) {
145 ensureConfig(this.config);
146 this.config.allowed_numbers = value
147 .split(",")
148 .map((item) => item.trim())
149 .filter((item) => item);
150 },
151
152 onEnabledChange() {
153 if (this.config?.enabled) return;
154 this.hideQr();
155 },
156
157 accessWarning() {
158 if (!this.config?.enabled) return "";
159 if (!this.allowedIsEmpty()) return "";
160 return "Allowed numbers is empty. If other people can message this number, they can reach your Agent Zero.";
161 },
162
163 statusLabel() {
164 if (!this.config?.enabled) return "Off";
165 if (this.qrStatus === "connected") return "Live";
166 if (this.allowedIsEmpty()) return "Open access";
167 return "Ready";
168 },
169
170 statusTone() {
171 const label = this.statusLabel();
172 if (label === "Live") return "success";
173 if (label === "Ready") return "ready";
174 if (label === "Off") return "muted";
175 return "warning";
176 },
177
178 modeSummary() {
179 if (!this.config) return "";
180 return this.config.mode === "self-chat" ? "Self-chat" : "Dedicated number";
181 },
182
183 projectSummary() {
184 return this.config?.project ? `Project: ${this.projectLabel(this.config.project)}` : "No project";
185 },
186
187 projectOptionValue(project) {
188 return String(project?.key || project?.name || "");
189 },
190
191 projectOptionLabel(project) {
192 return project?.label || project?.title || project?.name || project?.key || "";
193 },
194
195 projectLabel(projectKey) {
196 const normalizedProjectKey = String(projectKey || "").trim();
197 if (!normalizedProjectKey) return "";
198 const project = (this.projects || []).find((item) =>
199 String(item?.key || "") === normalizedProjectKey || String(item?.name || "") === normalizedProjectKey
200 );
201 if (project) return this.projectOptionLabel(project);
202 return normalizedProjectKey;
203 },
204
205 async testConnection() {
206 this.testing = true;
207 this.testResults = null;
208 try {
209 this.testResults = await API.callJsonApi(`${API_BASE}/test_connection`, {
210 config: { bridge_port: this.config?.bridge_port },
211 });
212 } catch (error) {
213 this.testResults = {
214 success: false,
215 results: [{ test: "WhatsApp", ok: false, message: String(error) }],
216 };
217 }
218 this.testing = false;
219 },
220
221 testButtonLabel() {
222 return this.testing ? "Checking..." : "Check WhatsApp connection";
223 },
224
225 async showQr() {
226 this.qrVisible = true;
227 this.qrStatus = "loading";
228 this.qrMessage = "Starting the WhatsApp bridge...";
229 this.qrDataUrl = null;
230 await this.pollQr();
231 this.qrPollTimer = setInterval(() => this.pollQr(), 3000);
232 },
233
234 hideQr() {
235 this.qrVisible = false;
236 this.qrDataUrl = null;
237 this.qrStatus = "";
238 if (this.qrPollTimer) {
239 clearInterval(this.qrPollTimer);
240 this.qrPollTimer = null;
241 }
242 },
243
244 async pollQr() {
245 try {
246 const response = await API.callJsonApi(`${API_BASE}/qr_code`, {});
247 this.qrStatus = response.status || "error";
248 this.qrMessage = response.message || "";
249 this.qrDataUrl = response.qr || null;
250
251 if (response.status === "connected" && this.qrPollTimer) {
252 clearInterval(this.qrPollTimer);
253 this.qrPollTimer = null;
254 }
255 } catch (error) {
256 this.qrStatus = "error";
257 this.qrMessage = String(error);
258 this.qrDataUrl = null;
259 }
260 },
261
262 async disconnectAccount() {
263 if (!window.confirm("Disconnect this WhatsApp account? You will need to scan a new QR code to reconnect.")) return;
264 this.disconnecting = true;
265 this.disconnectMessage = "";
266 try {
267 const response = await API.callJsonApi(`${API_BASE}/disconnect`, {});
268 this.disconnectMessage = response.success ? "Account disconnected" : (response.message || "Disconnect failed");
269 } catch (error) {
270 this.disconnectMessage = String(error);
271 }
272 this.disconnecting = false;
273 },
274
275 _installWizardFooter() {
276 if (!this.context) return;
277 this.context.wizardFooter = {
278 owner: "whatsappConfig",
279 visible: () => this.showFooterNav,
280 canGoBack: () => !this.isFirstStep,
281 backLabel: () => "Back",
282 note: () => this.footerStepLabel,
283 showNext: () => !this.isLastStep,
284 nextLabel: () => this.nextButtonLabel,
285 nextDisabled: () => false,
286 showSave: () => this.isLastStep,
287 onBack: () => this.previousStep(),
288 onNext: () => this.nextStep(),
289 };
290 },
291 });