@odience-network/paperclip-plugin-telegram-enhanced 0.2.0 → 0.3.1

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/dist/ui/index.js CHANGED
@@ -1,1446 +1,2818 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ // src/ui/index.tsx
2
2
  import { useEffect, useState } from "react";
3
- import { usePluginAction, usePluginData, } from "@paperclipai/plugin-sdk/ui";
4
- import { PLUGIN_ID } from "../constants.js";
5
- const TELEGRAM_PLUGIN_ID = PLUGIN_ID;
6
- const DEFAULT_ROUTING_CONFIG = {
7
- defaultChatId: "",
8
- topicRouting: false,
9
- maxAgentsPerThread: 5,
10
- notifyOnIssueCreated: true,
11
- notifyOnIssueDone: true,
12
- notifyOnIssueAssigned: false,
13
- onlyNotifyIfAssignedTo: "",
14
- notifyOnIssueBlocked: false,
15
- notifyOnBoardMention: false,
16
- boardUsernames: "",
17
- approvalsChatId: "",
18
- approvalsTopicId: "",
19
- notifyOnApprovalCreated: true,
20
- onlyNotifyBoardApprovals: false,
21
- errorsChatId: "",
22
- errorsTopicId: "",
23
- notifyOnAgentError: true,
24
- notifyOnAgentRunStarted: false,
25
- notifyOnAgentRunFinished: false,
26
- digestChatId: "",
27
- digestTopicId: "",
28
- digestMode: "off",
29
- dailyDigestTime: "09:00",
30
- bidailySecondTime: "17:00",
31
- tridailyTimes: "07:00,13:00,19:00",
32
- opsRoutes: [],
3
+ import {
4
+ usePluginAction,
5
+ usePluginData
6
+ } from "@paperclipai/plugin-sdk/ui";
7
+
8
+ // src/constants.ts
9
+ var PLUGIN_ID = "paperclip-plugin-telegram-enhanced";
10
+ var AGENT_ERROR_DEDUPLICATION_WINDOW_MS = 30 * 60 * 1e3;
11
+
12
+ // src/ui/index.tsx
13
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
+ var TELEGRAM_PLUGIN_ID = PLUGIN_ID;
15
+ var DEFAULT_ROUTING_CONFIG = {
16
+ defaultChatId: "",
17
+ topicRouting: false,
18
+ maxAgentsPerThread: 5,
19
+ notifyOnIssueCreated: true,
20
+ notifyOnIssueDone: true,
21
+ notifyOnIssueAssigned: false,
22
+ onlyNotifyIfAssignedTo: "",
23
+ notifyOnIssueBlocked: false,
24
+ notifyOnBoardMention: false,
25
+ boardUsernames: "",
26
+ approvalsChatId: "",
27
+ approvalsTopicId: "",
28
+ notifyOnApprovalCreated: true,
29
+ onlyNotifyBoardApprovals: false,
30
+ errorsChatId: "",
31
+ errorsTopicId: "",
32
+ notifyOnAgentError: true,
33
+ notifyOnAgentRunStarted: false,
34
+ notifyOnAgentRunFinished: false,
35
+ digestChatId: "",
36
+ digestTopicId: "",
37
+ digestMode: "off",
38
+ dailyDigestTime: "09:00",
39
+ bidailySecondTime: "17:00",
40
+ tridailyTimes: "07:00,13:00,19:00",
41
+ opsRoutes: []
33
42
  };
34
- const DEFAULT_CONNECTION_CONFIG = {
35
- telegramBotTokenRef: "",
36
- paperclipBaseUrl: "http://localhost:3100",
37
- paperclipPublicUrl: "",
43
+ var DEFAULT_CONNECTION_CONFIG = {
44
+ telegramBotTokenRef: "",
45
+ paperclipBaseUrl: "http://localhost:3100",
46
+ paperclipPublicUrl: ""
38
47
  };
39
- const DEFAULT_BOARD_CONFIG = {
40
- paperclipBoardApiTokenRef: "",
48
+ var DEFAULT_BOARD_CONFIG = {
49
+ paperclipBoardApiTokenRef: ""
41
50
  };
42
- const DEFAULT_ACCESS_CONFIG = {
43
- enableCommands: true,
44
- enableInbound: true,
45
- allowedTelegramUserIds: [],
46
- allowedTelegramChatIds: [],
51
+ var DEFAULT_ACCESS_CONFIG = {
52
+ enableCommands: true,
53
+ enableInbound: true,
54
+ allowedTelegramUserIds: [],
55
+ allowedTelegramChatIds: []
47
56
  };
48
- const DEFAULT_MEDIA_CONFIG = {
49
- transcriptionApiKeyRef: "",
50
- briefAgentId: "",
51
- briefAgentChatIds: [],
57
+ var DEFAULT_MEDIA_CONFIG = {
58
+ transcriptionApiKeyRef: "",
59
+ briefAgentId: "",
60
+ briefAgentChatIds: []
52
61
  };
53
- const DEFAULT_ESCALATION_CONFIG = {
54
- escalationChatId: "",
55
- escalationTimeoutMs: 900000,
56
- escalationDefaultAction: "defer",
57
- escalationHoldMessage: "Let me check on that - I'll get back to you shortly.",
62
+ var DEFAULT_ESCALATION_CONFIG = {
63
+ escalationChatId: "",
64
+ escalationTimeoutMs: 9e5,
65
+ escalationDefaultAction: "defer",
66
+ escalationHoldMessage: "Let me check on that - I'll get back to you shortly."
58
67
  };
59
- const DEFAULT_PROACTIVE_CONFIG = {
60
- maxSuggestionsPerHourPerCompany: 10,
61
- watchDeduplicationWindowMs: 86400000,
68
+ var DEFAULT_PROACTIVE_CONFIG = {
69
+ maxSuggestionsPerHourPerCompany: 10,
70
+ watchDeduplicationWindowMs: 864e5
62
71
  };
63
- const standardInputStyle = {
64
- border: "1px solid #d1d5db",
65
- borderRadius: 8,
66
- fontSize: 14,
67
- minWidth: 0,
68
- padding: "9px 10px",
72
+ var standardInputStyle = {
73
+ border: "1px solid #d1d5db",
74
+ borderRadius: 8,
75
+ fontSize: 14,
76
+ minWidth: 0,
77
+ padding: "9px 10px"
69
78
  };
70
- const helperTextStyle = {
71
- color: "#6b7280",
72
- fontSize: 12,
73
- lineHeight: "16px",
79
+ var helperTextStyle = {
80
+ color: "#6b7280",
81
+ fontSize: 12,
82
+ lineHeight: "16px"
74
83
  };
75
- const twoColumnGridStyle = {
76
- alignItems: "stretch",
77
- display: "grid",
78
- gap: 10,
79
- gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
84
+ var twoColumnGridStyle = {
85
+ alignItems: "stretch",
86
+ display: "grid",
87
+ gap: 10,
88
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))"
80
89
  };
81
- const pairedFieldStyle = {
82
- display: "grid",
83
- gap: 5,
84
- gridTemplateRows: "auto auto minmax(32px, auto)",
90
+ var pairedFieldStyle = {
91
+ display: "grid",
92
+ gap: 5,
93
+ gridTemplateRows: "auto auto minmax(32px, auto)"
85
94
  };
86
95
  function getErrorMessage(error) {
87
- return error instanceof Error ? error.message : String(error);
96
+ return error instanceof Error ? error.message : String(error);
88
97
  }
89
98
  function asString(value) {
90
- return typeof value === "string" ? value : "";
99
+ return typeof value === "string" ? value : "";
91
100
  }
92
101
  function asBoolean(value, fallback) {
93
- return typeof value === "boolean" ? value : fallback;
102
+ return typeof value === "boolean" ? value : fallback;
94
103
  }
95
104
  function asNumber(value, fallback) {
96
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
105
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
97
106
  }
98
107
  function asStringArray(value) {
99
- return Array.isArray(value)
100
- ? value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
101
- : [];
108
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
102
109
  }
103
- // board usernames may be persisted as an array (worker-side) or a raw string
104
- // (this text field). Always render a comma-separated string for the input.
105
110
  function asBoardUsernamesString(value) {
106
- if (Array.isArray(value)) {
107
- return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).join(", ");
108
- }
109
- return typeof value === "string" ? value : "";
111
+ if (Array.isArray(value)) {
112
+ return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).join(", ");
113
+ }
114
+ return typeof value === "string" ? value : "";
110
115
  }
111
116
  function asDigestMode(value) {
112
- return value === "daily" || value === "bidaily" || value === "tridaily" ? value : "off";
117
+ return value === "daily" || value === "bidaily" || value === "tridaily" ? value : "off";
113
118
  }
114
119
  function asOpsRoutes(value) {
115
- if (!Array.isArray(value))
116
- return [];
117
- return value
118
- .filter((item) => typeof item === "object" && item !== null && !Array.isArray(item))
119
- .map((item) => ({
120
- name: asString(item.name),
121
- enabled: asBoolean(item.enabled, true),
122
- companyId: asString(item.companyId),
123
- companyName: asString(item.companyName),
124
- chatId: asString(item.chatId),
125
- topicId: asString(item.topicId),
126
- }));
120
+ if (!Array.isArray(value)) return [];
121
+ return value.filter(
122
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item)
123
+ ).map((item) => ({
124
+ name: asString(item.name),
125
+ enabled: asBoolean(item.enabled, true),
126
+ companyId: asString(item.companyId),
127
+ companyName: asString(item.companyName),
128
+ chatId: asString(item.chatId),
129
+ topicId: asString(item.topicId)
130
+ }));
127
131
  }
128
- // Returns a human-readable error if the ops routes are invalid, else null.
129
- // Expects already-trimmed routes.
130
132
  function validateOpsRoutes(routes) {
131
- const seenCompanyIds = new Set();
132
- for (const route of routes) {
133
- const label = route.name || route.companyName || route.companyId || "(unnamed)";
134
- if (!route.chatId) {
135
- return `Ops route "${label}" needs a Chat ID.`;
136
- }
137
- if (!route.companyId && !route.companyName) {
138
- return `Ops route "${label}" needs a Company ID or Company name to match.`;
139
- }
140
- if (route.topicId && !/^\d+$/.test(route.topicId)) {
141
- return `Ops route "${label}" topic ID must be a numeric Telegram forum topic ID.`;
142
- }
143
- if (route.companyId) {
144
- if (seenCompanyIds.has(route.companyId)) {
145
- return `Duplicate ops route for Company ID "${route.companyId}".`;
146
- }
147
- seenCompanyIds.add(route.companyId);
148
- }
133
+ const seenCompanyIds = /* @__PURE__ */ new Set();
134
+ for (const route of routes) {
135
+ const label = route.name || route.companyName || route.companyId || "(unnamed)";
136
+ if (!route.chatId) {
137
+ return `Ops route "${label}" needs a Chat ID.`;
149
138
  }
150
- return null;
139
+ if (!route.companyId && !route.companyName) {
140
+ return `Ops route "${label}" needs a Company ID or Company name to match.`;
141
+ }
142
+ if (route.topicId && !/^\d+$/.test(route.topicId)) {
143
+ return `Ops route "${label}" topic ID must be a numeric Telegram forum topic ID.`;
144
+ }
145
+ if (route.companyId) {
146
+ if (seenCompanyIds.has(route.companyId)) {
147
+ return `Duplicate ops route for Company ID "${route.companyId}".`;
148
+ }
149
+ seenCompanyIds.add(route.companyId);
150
+ }
151
+ }
152
+ return null;
151
153
  }
152
154
  function asEscalationDefaultAction(value) {
153
- return value === "auto_reply" || value === "close" ? value : "defer";
155
+ return value === "auto_reply" || value === "close" ? value : "defer";
154
156
  }
155
157
  function extractRoutingConfig(config) {
156
- return {
157
- defaultChatId: asString(config.defaultChatId),
158
- topicRouting: asBoolean(config.topicRouting, DEFAULT_ROUTING_CONFIG.topicRouting),
159
- maxAgentsPerThread: asNumber(config.maxAgentsPerThread, DEFAULT_ROUTING_CONFIG.maxAgentsPerThread),
160
- notifyOnIssueCreated: asBoolean(config.notifyOnIssueCreated, DEFAULT_ROUTING_CONFIG.notifyOnIssueCreated),
161
- notifyOnIssueDone: asBoolean(config.notifyOnIssueDone, DEFAULT_ROUTING_CONFIG.notifyOnIssueDone),
162
- notifyOnIssueAssigned: asBoolean(config.notifyOnIssueAssigned, DEFAULT_ROUTING_CONFIG.notifyOnIssueAssigned),
163
- onlyNotifyIfAssignedTo: asString(config.onlyNotifyIfAssignedTo),
164
- notifyOnIssueBlocked: asBoolean(config.notifyOnIssueBlocked, DEFAULT_ROUTING_CONFIG.notifyOnIssueBlocked),
165
- notifyOnBoardMention: asBoolean(config.notifyOnBoardMention, DEFAULT_ROUTING_CONFIG.notifyOnBoardMention),
166
- boardUsernames: asBoardUsernamesString(config.boardUsernames),
167
- approvalsChatId: asString(config.approvalsChatId),
168
- approvalsTopicId: asString(config.approvalsTopicId),
169
- notifyOnApprovalCreated: asBoolean(config.notifyOnApprovalCreated, DEFAULT_ROUTING_CONFIG.notifyOnApprovalCreated),
170
- onlyNotifyBoardApprovals: asBoolean(config.onlyNotifyBoardApprovals, DEFAULT_ROUTING_CONFIG.onlyNotifyBoardApprovals),
171
- errorsChatId: asString(config.errorsChatId),
172
- errorsTopicId: asString(config.errorsTopicId),
173
- notifyOnAgentError: asBoolean(config.notifyOnAgentError, DEFAULT_ROUTING_CONFIG.notifyOnAgentError),
174
- notifyOnAgentRunStarted: asBoolean(config.notifyOnAgentRunStarted, DEFAULT_ROUTING_CONFIG.notifyOnAgentRunStarted),
175
- notifyOnAgentRunFinished: asBoolean(config.notifyOnAgentRunFinished, DEFAULT_ROUTING_CONFIG.notifyOnAgentRunFinished),
176
- digestChatId: asString(config.digestChatId),
177
- digestTopicId: asString(config.digestTopicId),
178
- digestMode: asDigestMode(config.digestMode),
179
- dailyDigestTime: asString(config.dailyDigestTime) || DEFAULT_ROUTING_CONFIG.dailyDigestTime,
180
- bidailySecondTime: asString(config.bidailySecondTime) || DEFAULT_ROUTING_CONFIG.bidailySecondTime,
181
- tridailyTimes: asString(config.tridailyTimes) || DEFAULT_ROUTING_CONFIG.tridailyTimes,
182
- opsRoutes: asOpsRoutes(config.opsRoutes),
183
- };
158
+ return {
159
+ defaultChatId: asString(config.defaultChatId),
160
+ topicRouting: asBoolean(config.topicRouting, DEFAULT_ROUTING_CONFIG.topicRouting),
161
+ maxAgentsPerThread: asNumber(config.maxAgentsPerThread, DEFAULT_ROUTING_CONFIG.maxAgentsPerThread),
162
+ notifyOnIssueCreated: asBoolean(
163
+ config.notifyOnIssueCreated,
164
+ DEFAULT_ROUTING_CONFIG.notifyOnIssueCreated
165
+ ),
166
+ notifyOnIssueDone: asBoolean(
167
+ config.notifyOnIssueDone,
168
+ DEFAULT_ROUTING_CONFIG.notifyOnIssueDone
169
+ ),
170
+ notifyOnIssueAssigned: asBoolean(
171
+ config.notifyOnIssueAssigned,
172
+ DEFAULT_ROUTING_CONFIG.notifyOnIssueAssigned
173
+ ),
174
+ onlyNotifyIfAssignedTo: asString(config.onlyNotifyIfAssignedTo),
175
+ notifyOnIssueBlocked: asBoolean(
176
+ config.notifyOnIssueBlocked,
177
+ DEFAULT_ROUTING_CONFIG.notifyOnIssueBlocked
178
+ ),
179
+ notifyOnBoardMention: asBoolean(
180
+ config.notifyOnBoardMention,
181
+ DEFAULT_ROUTING_CONFIG.notifyOnBoardMention
182
+ ),
183
+ boardUsernames: asBoardUsernamesString(config.boardUsernames),
184
+ approvalsChatId: asString(config.approvalsChatId),
185
+ approvalsTopicId: asString(config.approvalsTopicId),
186
+ notifyOnApprovalCreated: asBoolean(
187
+ config.notifyOnApprovalCreated,
188
+ DEFAULT_ROUTING_CONFIG.notifyOnApprovalCreated
189
+ ),
190
+ onlyNotifyBoardApprovals: asBoolean(
191
+ config.onlyNotifyBoardApprovals,
192
+ DEFAULT_ROUTING_CONFIG.onlyNotifyBoardApprovals
193
+ ),
194
+ errorsChatId: asString(config.errorsChatId),
195
+ errorsTopicId: asString(config.errorsTopicId),
196
+ notifyOnAgentError: asBoolean(
197
+ config.notifyOnAgentError,
198
+ DEFAULT_ROUTING_CONFIG.notifyOnAgentError
199
+ ),
200
+ notifyOnAgentRunStarted: asBoolean(
201
+ config.notifyOnAgentRunStarted,
202
+ DEFAULT_ROUTING_CONFIG.notifyOnAgentRunStarted
203
+ ),
204
+ notifyOnAgentRunFinished: asBoolean(
205
+ config.notifyOnAgentRunFinished,
206
+ DEFAULT_ROUTING_CONFIG.notifyOnAgentRunFinished
207
+ ),
208
+ digestChatId: asString(config.digestChatId),
209
+ digestTopicId: asString(config.digestTopicId),
210
+ digestMode: asDigestMode(config.digestMode),
211
+ dailyDigestTime: asString(config.dailyDigestTime) || DEFAULT_ROUTING_CONFIG.dailyDigestTime,
212
+ bidailySecondTime: asString(config.bidailySecondTime) || DEFAULT_ROUTING_CONFIG.bidailySecondTime,
213
+ tridailyTimes: asString(config.tridailyTimes) || DEFAULT_ROUTING_CONFIG.tridailyTimes,
214
+ opsRoutes: asOpsRoutes(config.opsRoutes)
215
+ };
184
216
  }
185
217
  function extractConnectionConfig(config) {
186
- return {
187
- telegramBotTokenRef: asString(config.telegramBotTokenRef),
188
- paperclipBaseUrl: asString(config.paperclipBaseUrl) || DEFAULT_CONNECTION_CONFIG.paperclipBaseUrl,
189
- paperclipPublicUrl: asString(config.paperclipPublicUrl),
190
- };
218
+ return {
219
+ telegramBotTokenRef: asString(config.telegramBotTokenRef),
220
+ paperclipBaseUrl: asString(config.paperclipBaseUrl) || DEFAULT_CONNECTION_CONFIG.paperclipBaseUrl,
221
+ paperclipPublicUrl: asString(config.paperclipPublicUrl)
222
+ };
191
223
  }
192
224
  function extractBoardConfig(config) {
193
- return {
194
- paperclipBoardApiTokenRef: asString(config.paperclipBoardApiTokenRef),
195
- };
225
+ return {
226
+ paperclipBoardApiTokenRef: asString(config.paperclipBoardApiTokenRef)
227
+ };
196
228
  }
197
229
  function extractAccessConfig(config) {
198
- return {
199
- enableCommands: asBoolean(config.enableCommands, DEFAULT_ACCESS_CONFIG.enableCommands),
200
- enableInbound: asBoolean(config.enableInbound, DEFAULT_ACCESS_CONFIG.enableInbound),
201
- allowedTelegramUserIds: asStringArray(config.allowedTelegramUserIds),
202
- allowedTelegramChatIds: asStringArray(config.allowedTelegramChatIds),
203
- };
230
+ return {
231
+ enableCommands: asBoolean(config.enableCommands, DEFAULT_ACCESS_CONFIG.enableCommands),
232
+ enableInbound: asBoolean(config.enableInbound, DEFAULT_ACCESS_CONFIG.enableInbound),
233
+ allowedTelegramUserIds: asStringArray(config.allowedTelegramUserIds),
234
+ allowedTelegramChatIds: asStringArray(config.allowedTelegramChatIds)
235
+ };
204
236
  }
205
237
  function extractMediaConfig(config) {
206
- return {
207
- transcriptionApiKeyRef: asString(config.transcriptionApiKeyRef),
208
- briefAgentId: asString(config.briefAgentId),
209
- briefAgentChatIds: asStringArray(config.briefAgentChatIds),
210
- };
238
+ return {
239
+ transcriptionApiKeyRef: asString(config.transcriptionApiKeyRef),
240
+ briefAgentId: asString(config.briefAgentId),
241
+ briefAgentChatIds: asStringArray(config.briefAgentChatIds)
242
+ };
211
243
  }
212
244
  function extractEscalationConfig(config) {
213
- return {
214
- escalationChatId: asString(config.escalationChatId),
215
- escalationTimeoutMs: asNumber(config.escalationTimeoutMs, DEFAULT_ESCALATION_CONFIG.escalationTimeoutMs),
216
- escalationDefaultAction: asEscalationDefaultAction(config.escalationDefaultAction),
217
- escalationHoldMessage: asString(config.escalationHoldMessage) || DEFAULT_ESCALATION_CONFIG.escalationHoldMessage,
218
- };
245
+ return {
246
+ escalationChatId: asString(config.escalationChatId),
247
+ escalationTimeoutMs: asNumber(config.escalationTimeoutMs, DEFAULT_ESCALATION_CONFIG.escalationTimeoutMs),
248
+ escalationDefaultAction: asEscalationDefaultAction(config.escalationDefaultAction),
249
+ escalationHoldMessage: asString(config.escalationHoldMessage) || DEFAULT_ESCALATION_CONFIG.escalationHoldMessage
250
+ };
219
251
  }
220
252
  function extractProactiveConfig(config) {
221
- return {
222
- maxSuggestionsPerHourPerCompany: asNumber(config.maxSuggestionsPerHourPerCompany, DEFAULT_PROACTIVE_CONFIG.maxSuggestionsPerHourPerCompany),
223
- watchDeduplicationWindowMs: asNumber(config.watchDeduplicationWindowMs, DEFAULT_PROACTIVE_CONFIG.watchDeduplicationWindowMs),
224
- };
253
+ return {
254
+ maxSuggestionsPerHourPerCompany: asNumber(
255
+ config.maxSuggestionsPerHourPerCompany,
256
+ DEFAULT_PROACTIVE_CONFIG.maxSuggestionsPerHourPerCompany
257
+ ),
258
+ watchDeduplicationWindowMs: asNumber(
259
+ config.watchDeduplicationWindowMs,
260
+ DEFAULT_PROACTIVE_CONFIG.watchDeduplicationWindowMs
261
+ )
262
+ };
225
263
  }
226
264
  async function fetchHostJson(input, init = {}) {
227
- const headers = new Headers(init.headers);
228
- headers.set("accept", "application/json");
229
- if (typeof init.body === "string" && !headers.has("content-type")) {
230
- headers.set("content-type", "application/json");
231
- }
232
- const response = await fetch(input, {
233
- ...init,
234
- headers,
235
- credentials: init.credentials ?? "same-origin",
236
- });
237
- const rawBody = await response.text();
238
- const normalizedBody = rawBody.trim();
239
- const contentType = response.headers.get("content-type") ?? "";
240
- if (contentType.includes("text/html") ||
241
- normalizedBody.startsWith("<!DOCTYPE html") ||
242
- normalizedBody.startsWith("<html")) {
243
- throw new Error("Paperclip returned HTML instead of JSON.");
244
- }
245
- let payload = null;
246
- if (normalizedBody) {
247
- try {
248
- payload = JSON.parse(normalizedBody);
249
- }
250
- catch {
251
- throw new Error("Paperclip returned an unexpected response.");
252
- }
253
- }
254
- if (!response.ok) {
255
- const message = typeof payload === "object" &&
256
- payload !== null &&
257
- "error" in payload &&
258
- typeof payload.error === "string"
259
- ? payload.error
260
- : `Request failed with status ${response.status}.`;
261
- throw new Error(message);
265
+ const headers = new Headers(init.headers);
266
+ headers.set("accept", "application/json");
267
+ if (typeof init.body === "string" && !headers.has("content-type")) {
268
+ headers.set("content-type", "application/json");
269
+ }
270
+ const response = await fetch(input, {
271
+ ...init,
272
+ headers,
273
+ credentials: init.credentials ?? "same-origin"
274
+ });
275
+ const rawBody = await response.text();
276
+ const normalizedBody = rawBody.trim();
277
+ const contentType = response.headers.get("content-type") ?? "";
278
+ if (contentType.includes("text/html") || normalizedBody.startsWith("<!DOCTYPE html") || normalizedBody.startsWith("<html")) {
279
+ throw new Error("Paperclip returned HTML instead of JSON.");
280
+ }
281
+ let payload = null;
282
+ if (normalizedBody) {
283
+ try {
284
+ payload = JSON.parse(normalizedBody);
285
+ } catch {
286
+ throw new Error("Paperclip returned an unexpected response.");
262
287
  }
263
- return payload;
288
+ }
289
+ if (!response.ok) {
290
+ const message = typeof payload === "object" && payload !== null && "error" in payload && typeof payload.error === "string" ? payload.error : `Request failed with status ${response.status}.`;
291
+ throw new Error(message);
292
+ }
293
+ return payload;
264
294
  }
265
295
  function resolveBrowserOrigin() {
266
- if (typeof window === "undefined" || typeof window.location?.origin !== "string") {
267
- return null;
268
- }
269
- const origin = window.location.origin.trim();
270
- if (!origin || origin === "null") {
271
- return null;
272
- }
273
- try {
274
- const normalizedOrigin = new URL(origin);
275
- if (normalizedOrigin.protocol !== "http:" && normalizedOrigin.protocol !== "https:") {
276
- return null;
277
- }
278
- return normalizedOrigin.origin;
279
- }
280
- catch {
281
- return null;
296
+ if (typeof window === "undefined" || typeof window.location?.origin !== "string") {
297
+ return null;
298
+ }
299
+ const origin = window.location.origin.trim();
300
+ if (!origin || origin === "null") {
301
+ return null;
302
+ }
303
+ try {
304
+ const normalizedOrigin = new URL(origin);
305
+ if (normalizedOrigin.protocol !== "http:" && normalizedOrigin.protocol !== "https:") {
306
+ return null;
282
307
  }
308
+ return normalizedOrigin.origin;
309
+ } catch {
310
+ return null;
311
+ }
283
312
  }
284
313
  function buildPaperclipUrl(input) {
285
- const origin = resolveBrowserOrigin();
286
- if (!origin || !input.trim() || input.trim().startsWith("//")) {
287
- return null;
288
- }
289
- try {
290
- const candidate = new URL(input.trim(), origin);
291
- return candidate.origin === origin ? candidate.toString() : null;
292
- }
293
- catch {
294
- return null;
295
- }
314
+ const origin = resolveBrowserOrigin();
315
+ if (!origin || !input.trim() || input.trim().startsWith("//")) {
316
+ return null;
317
+ }
318
+ try {
319
+ const candidate = new URL(input.trim(), origin);
320
+ return candidate.origin === origin ? candidate.toString() : null;
321
+ } catch {
322
+ return null;
323
+ }
296
324
  }
297
325
  function resolveCliAuthUrl(url, path) {
298
- if (typeof url === "string" && url.trim()) {
299
- return buildPaperclipUrl(url.trim());
300
- }
301
- if (typeof path !== "string" || !path.trim()) {
302
- return null;
303
- }
304
- return buildPaperclipUrl(path.trim());
326
+ if (typeof url === "string" && url.trim()) {
327
+ return buildPaperclipUrl(url.trim());
328
+ }
329
+ if (typeof path !== "string" || !path.trim()) {
330
+ return null;
331
+ }
332
+ return buildPaperclipUrl(path.trim());
305
333
  }
306
334
  function resolveCliAuthPollUrl(urlOrPath) {
307
- if (typeof urlOrPath !== "string" || !urlOrPath.trim()) {
308
- return null;
309
- }
310
- const trimmed = urlOrPath.trim();
311
- if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(trimmed)) {
312
- return buildPaperclipUrl(trimmed);
313
- }
314
- const normalizedPath = trimmed.startsWith("/api/")
315
- ? trimmed
316
- : `/api${trimmed.startsWith("/") ? "" : "/"}${trimmed}`;
317
- return buildPaperclipUrl(normalizedPath);
335
+ if (typeof urlOrPath !== "string" || !urlOrPath.trim()) {
336
+ return null;
337
+ }
338
+ const trimmed = urlOrPath.trim();
339
+ if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(trimmed)) {
340
+ return buildPaperclipUrl(trimmed);
341
+ }
342
+ const normalizedPath = trimmed.startsWith("/api/") ? trimmed : `/api${trimmed.startsWith("/") ? "" : "/"}${trimmed}`;
343
+ return buildPaperclipUrl(normalizedPath);
318
344
  }
319
345
  function normalizePollIntervalMs(value) {
320
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
321
- return 1500;
322
- }
323
- return Math.min(5000, Math.max(750, Math.floor(value)));
346
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
347
+ return 1500;
348
+ }
349
+ return Math.min(5e3, Math.max(750, Math.floor(value)));
324
350
  }
325
351
  function waitForDuration(durationMs) {
326
- return new Promise((resolve) => {
327
- globalThis.setTimeout(resolve, durationMs);
328
- });
352
+ return new Promise((resolve) => {
353
+ globalThis.setTimeout(resolve, durationMs);
354
+ });
329
355
  }
330
356
  async function requestBoardAccessChallenge(companyId) {
331
- return fetchHostJson("/api/cli-auth/challenges", {
332
- method: "POST",
333
- body: JSON.stringify({
334
- command: "paperclip plugin telegram settings",
335
- clientName: "Telegram plugin",
336
- requestedAccess: "board",
337
- requestedCompanyId: companyId,
338
- }),
339
- });
357
+ return fetchHostJson("/api/cli-auth/challenges", {
358
+ method: "POST",
359
+ body: JSON.stringify({
360
+ command: "paperclip plugin telegram settings",
361
+ clientName: "Telegram plugin",
362
+ requestedAccess: "board",
363
+ requestedCompanyId: companyId
364
+ })
365
+ });
340
366
  }
341
367
  async function waitForBoardAccessApproval(challenge) {
342
- const challengeToken = typeof challenge.token === "string" ? challenge.token.trim() : "";
343
- const pollUrl = resolveCliAuthPollUrl(challenge.pollUrl ?? challenge.pollPath);
344
- if (!challengeToken || !pollUrl) {
345
- throw new Error("Paperclip did not return a trusted board access challenge.");
368
+ const challengeToken = typeof challenge.token === "string" ? challenge.token.trim() : "";
369
+ const pollUrl = resolveCliAuthPollUrl(challenge.pollUrl ?? challenge.pollPath);
370
+ if (!challengeToken || !pollUrl) {
371
+ throw new Error("Paperclip did not return a trusted board access challenge.");
372
+ }
373
+ const expiresAtTimeMs = typeof challenge.expiresAt === "string" ? Date.parse(challenge.expiresAt) : Number.NaN;
374
+ const pollIntervalMs = normalizePollIntervalMs(challenge.suggestedPollIntervalMs);
375
+ while (true) {
376
+ const pollUrlWithToken = new URL(pollUrl);
377
+ pollUrlWithToken.searchParams.set("token", challengeToken);
378
+ const pollResult = await fetchHostJson(
379
+ pollUrlWithToken.toString()
380
+ );
381
+ const status = typeof pollResult.status === "string" ? pollResult.status.trim().toLowerCase() : "pending";
382
+ if (status === "approved") {
383
+ const boardApiToken = typeof pollResult.boardApiToken === "string" && pollResult.boardApiToken.trim() ? pollResult.boardApiToken.trim() : typeof challenge.boardApiToken === "string" && challenge.boardApiToken.trim() ? challenge.boardApiToken.trim() : "";
384
+ if (!boardApiToken) {
385
+ throw new Error("Paperclip approved board access but did not return a usable API token.");
386
+ }
387
+ return boardApiToken;
346
388
  }
347
- const expiresAtTimeMs = typeof challenge.expiresAt === "string" ? Date.parse(challenge.expiresAt) : Number.NaN;
348
- const pollIntervalMs = normalizePollIntervalMs(challenge.suggestedPollIntervalMs);
349
- while (true) {
350
- const pollUrlWithToken = new URL(pollUrl);
351
- pollUrlWithToken.searchParams.set("token", challengeToken);
352
- const pollResult = await fetchHostJson(pollUrlWithToken.toString());
353
- const status = typeof pollResult.status === "string" ? pollResult.status.trim().toLowerCase() : "pending";
354
- if (status === "approved") {
355
- const boardApiToken = typeof pollResult.boardApiToken === "string" && pollResult.boardApiToken.trim()
356
- ? pollResult.boardApiToken.trim()
357
- : typeof challenge.boardApiToken === "string" && challenge.boardApiToken.trim()
358
- ? challenge.boardApiToken.trim()
359
- : "";
360
- if (!boardApiToken) {
361
- throw new Error("Paperclip approved board access but did not return a usable API token.");
362
- }
363
- return boardApiToken;
364
- }
365
- if (status === "cancelled") {
366
- throw new Error("Board access approval was cancelled.");
367
- }
368
- if (status === "expired") {
369
- throw new Error("Board access approval expired. Start the connection flow again.");
370
- }
371
- if (Number.isFinite(expiresAtTimeMs) && Date.now() >= expiresAtTimeMs) {
372
- throw new Error("Board access approval expired. Start the connection flow again.");
373
- }
374
- await waitForDuration(pollIntervalMs);
389
+ if (status === "cancelled") {
390
+ throw new Error("Board access approval was cancelled.");
391
+ }
392
+ if (status === "expired") {
393
+ throw new Error("Board access approval expired. Start the connection flow again.");
375
394
  }
395
+ if (Number.isFinite(expiresAtTimeMs) && Date.now() >= expiresAtTimeMs) {
396
+ throw new Error("Board access approval expired. Start the connection flow again.");
397
+ }
398
+ await waitForDuration(pollIntervalMs);
399
+ }
376
400
  }
377
401
  function getIdentityLabel(identity) {
378
- const candidates = [
379
- identity.user?.displayName,
380
- identity.user?.name,
381
- identity.user?.login,
382
- identity.user?.email,
383
- identity.displayName,
384
- identity.name,
385
- identity.login,
386
- identity.email,
387
- ];
388
- for (const candidate of candidates) {
389
- if (typeof candidate === "string" && candidate.trim()) {
390
- return candidate.trim();
391
- }
402
+ const candidates = [
403
+ identity.user?.displayName,
404
+ identity.user?.name,
405
+ identity.user?.login,
406
+ identity.user?.email,
407
+ identity.displayName,
408
+ identity.name,
409
+ identity.login,
410
+ identity.email
411
+ ];
412
+ for (const candidate of candidates) {
413
+ if (typeof candidate === "string" && candidate.trim()) {
414
+ return candidate.trim();
392
415
  }
393
- return null;
416
+ }
417
+ return null;
394
418
  }
395
419
  async function fetchBoardAccessIdentity(boardApiToken) {
396
- const identity = await fetchHostJson("/api/cli-auth/me", {
397
- headers: {
398
- authorization: `Bearer ${boardApiToken.trim()}`,
399
- },
400
- });
401
- return getIdentityLabel(identity);
420
+ const identity = await fetchHostJson("/api/cli-auth/me", {
421
+ headers: {
422
+ authorization: `Bearer ${boardApiToken.trim()}`
423
+ }
424
+ });
425
+ return getIdentityLabel(identity);
402
426
  }
403
427
  async function fetchPluginConfig() {
404
- const record = await fetchHostJson(`/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`);
405
- return record?.configJson && typeof record.configJson === "object" ? record.configJson : {};
428
+ const record = await fetchHostJson(
429
+ `/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`
430
+ );
431
+ return record?.configJson && typeof record.configJson === "object" ? record.configJson : {};
406
432
  }
407
433
  async function savePluginConfig(configJson) {
408
- await fetchHostJson(`/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`, {
409
- method: "POST",
410
- body: JSON.stringify({ configJson }),
411
- });
434
+ await fetchHostJson(`/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`, {
435
+ method: "POST",
436
+ body: JSON.stringify({ configJson })
437
+ });
412
438
  }
413
439
  async function resolveOrCreateCompanySecret(companyId, name, value) {
414
- const existingSecrets = await fetchHostJson(`/api/companies/${encodeURIComponent(companyId)}/secrets`);
415
- const existing = existingSecrets.find((secret) => secret.name.trim().toLowerCase() === name.trim().toLowerCase());
416
- if (existing) {
417
- return fetchHostJson(`/api/secrets/${encodeURIComponent(existing.id)}/rotate`, {
418
- method: "POST",
419
- body: JSON.stringify({ value }),
420
- });
421
- }
422
- return fetchHostJson(`/api/companies/${encodeURIComponent(companyId)}/secrets`, {
440
+ const existingSecrets = await fetchHostJson(
441
+ `/api/companies/${encodeURIComponent(companyId)}/secrets`
442
+ );
443
+ const existing = existingSecrets.find(
444
+ (secret) => secret.name.trim().toLowerCase() === name.trim().toLowerCase()
445
+ );
446
+ if (existing) {
447
+ return fetchHostJson(
448
+ `/api/secrets/${encodeURIComponent(existing.id)}/rotate`,
449
+ {
423
450
  method: "POST",
424
- body: JSON.stringify({ name, value }),
425
- });
451
+ body: JSON.stringify({ value })
452
+ }
453
+ );
454
+ }
455
+ return fetchHostJson(
456
+ `/api/companies/${encodeURIComponent(companyId)}/secrets`,
457
+ {
458
+ method: "POST",
459
+ body: JSON.stringify({ name, value })
460
+ }
461
+ );
426
462
  }
427
- export function TelegramSettingsPage({ context }) {
428
- const boardAccess = usePluginData("board-access.read");
429
- const updateBoardAccess = usePluginAction("board-access.update");
430
- const [connecting, setConnecting] = useState(false);
431
- const [notice, setNotice] = useState(null);
432
- const [routingConfig, setRoutingConfig] = useState(DEFAULT_ROUTING_CONFIG);
433
- const [routingSnapshot, setRoutingSnapshot] = useState(DEFAULT_ROUTING_CONFIG);
434
- const [routingLoading, setRoutingLoading] = useState(true);
435
- const [routingSaving, setRoutingSaving] = useState(false);
436
- const [routingMessage, setRoutingMessage] = useState(null);
437
- const [connectionConfig, setConnectionConfig] = useState(DEFAULT_CONNECTION_CONFIG);
438
- const [connectionSnapshot, setConnectionSnapshot] = useState(DEFAULT_CONNECTION_CONFIG);
439
- const [connectionLoading, setConnectionLoading] = useState(true);
440
- const [connectionSaving, setConnectionSaving] = useState(false);
441
- const [connectionMessage, setConnectionMessage] = useState(null);
442
- const [boardConfig, setBoardConfig] = useState(DEFAULT_BOARD_CONFIG);
443
- const [boardSnapshot, setBoardSnapshot] = useState(DEFAULT_BOARD_CONFIG);
444
- const [boardConfigLoading, setBoardConfigLoading] = useState(true);
445
- const [boardConfigSaving, setBoardConfigSaving] = useState(false);
446
- const [boardConfigMessage, setBoardConfigMessage] = useState(null);
447
- const [accessConfig, setAccessConfig] = useState(DEFAULT_ACCESS_CONFIG);
448
- const [accessSnapshot, setAccessSnapshot] = useState(DEFAULT_ACCESS_CONFIG);
449
- const [accessLoading, setAccessLoading] = useState(true);
450
- const [accessSaving, setAccessSaving] = useState(false);
451
- const [accessMessage, setAccessMessage] = useState(null);
452
- const [mediaConfig, setMediaConfig] = useState(DEFAULT_MEDIA_CONFIG);
453
- const [mediaSnapshot, setMediaSnapshot] = useState(DEFAULT_MEDIA_CONFIG);
454
- const [mediaLoading, setMediaLoading] = useState(true);
455
- const [mediaSaving, setMediaSaving] = useState(false);
456
- const [mediaMessage, setMediaMessage] = useState(null);
457
- const [escalationConfig, setEscalationConfig] = useState(DEFAULT_ESCALATION_CONFIG);
458
- const [escalationSnapshot, setEscalationSnapshot] = useState(DEFAULT_ESCALATION_CONFIG);
459
- const [escalationLoading, setEscalationLoading] = useState(true);
460
- const [escalationSaving, setEscalationSaving] = useState(false);
461
- const [escalationMessage, setEscalationMessage] = useState(null);
462
- const [proactiveConfig, setProactiveConfig] = useState(DEFAULT_PROACTIVE_CONFIG);
463
- const [proactiveSnapshot, setProactiveSnapshot] = useState(DEFAULT_PROACTIVE_CONFIG);
464
- const [proactiveLoading, setProactiveLoading] = useState(true);
465
- const [proactiveSaving, setProactiveSaving] = useState(false);
466
- const [proactiveMessage, setProactiveMessage] = useState(null);
467
- const companyId = context.companyId ?? "";
468
- const companyLabel = context.companyPrefix?.trim() || "this company";
469
- const configured = Boolean(boardAccess.data?.configured);
470
- const identity = boardAccess.data?.identity?.trim() || null;
471
- const routingDirty = JSON.stringify(routingConfig) !== JSON.stringify(routingSnapshot);
472
- const connectionDirty = JSON.stringify(connectionConfig) !== JSON.stringify(connectionSnapshot);
473
- const boardConfigDirty = JSON.stringify(boardConfig) !== JSON.stringify(boardSnapshot);
474
- const accessDirty = JSON.stringify(accessConfig) !== JSON.stringify(accessSnapshot);
475
- const mediaDirty = JSON.stringify(mediaConfig) !== JSON.stringify(mediaSnapshot);
476
- const escalationDirty = JSON.stringify(escalationConfig) !== JSON.stringify(escalationSnapshot);
477
- const proactiveDirty = JSON.stringify(proactiveConfig) !== JSON.stringify(proactiveSnapshot);
478
- useEffect(() => {
479
- let cancelled = false;
480
- async function loadRoutingConfig() {
481
- setRoutingLoading(true);
482
- setRoutingMessage(null);
483
- try {
484
- const config = await fetchPluginConfig();
485
- if (cancelled)
486
- return;
487
- const nextRoutingConfig = extractRoutingConfig(config);
488
- setRoutingConfig(nextRoutingConfig);
489
- setRoutingSnapshot(nextRoutingConfig);
490
- }
491
- catch (error) {
492
- if (!cancelled) {
493
- setRoutingMessage({
494
- tone: "error",
495
- title: "Routing settings could not be loaded",
496
- text: getErrorMessage(error),
497
- });
498
- }
499
- }
500
- finally {
501
- if (!cancelled) {
502
- setRoutingLoading(false);
503
- }
504
- }
463
+ function TelegramSettingsPage({ context }) {
464
+ const boardAccess = usePluginData("board-access.read");
465
+ const updateBoardAccess = usePluginAction("board-access.update");
466
+ const botConnection = usePluginData("telegram-connection.read");
467
+ const updateBotConnection = usePluginAction("telegram-connection.update");
468
+ const clearBotConnection = usePluginAction("telegram-connection.clear");
469
+ const [botTokenInput, setBotTokenInput] = useState("");
470
+ const [botConnecting, setBotConnecting] = useState(false);
471
+ const [botConnectionMessage, setBotConnectionMessage] = useState(null);
472
+ const [connecting, setConnecting] = useState(false);
473
+ const [notice, setNotice] = useState(null);
474
+ const [routingConfig, setRoutingConfig] = useState(DEFAULT_ROUTING_CONFIG);
475
+ const [routingSnapshot, setRoutingSnapshot] = useState(DEFAULT_ROUTING_CONFIG);
476
+ const [routingLoading, setRoutingLoading] = useState(true);
477
+ const [routingSaving, setRoutingSaving] = useState(false);
478
+ const [routingMessage, setRoutingMessage] = useState(null);
479
+ const [connectionConfig, setConnectionConfig] = useState(DEFAULT_CONNECTION_CONFIG);
480
+ const [connectionSnapshot, setConnectionSnapshot] = useState(DEFAULT_CONNECTION_CONFIG);
481
+ const [connectionLoading, setConnectionLoading] = useState(true);
482
+ const [connectionSaving, setConnectionSaving] = useState(false);
483
+ const [connectionMessage, setConnectionMessage] = useState(null);
484
+ const [boardConfig, setBoardConfig] = useState(DEFAULT_BOARD_CONFIG);
485
+ const [boardSnapshot, setBoardSnapshot] = useState(DEFAULT_BOARD_CONFIG);
486
+ const [boardConfigLoading, setBoardConfigLoading] = useState(true);
487
+ const [boardConfigSaving, setBoardConfigSaving] = useState(false);
488
+ const [boardConfigMessage, setBoardConfigMessage] = useState(null);
489
+ const [accessConfig, setAccessConfig] = useState(DEFAULT_ACCESS_CONFIG);
490
+ const [accessSnapshot, setAccessSnapshot] = useState(DEFAULT_ACCESS_CONFIG);
491
+ const [accessLoading, setAccessLoading] = useState(true);
492
+ const [accessSaving, setAccessSaving] = useState(false);
493
+ const [accessMessage, setAccessMessage] = useState(null);
494
+ const [mediaConfig, setMediaConfig] = useState(DEFAULT_MEDIA_CONFIG);
495
+ const [mediaSnapshot, setMediaSnapshot] = useState(DEFAULT_MEDIA_CONFIG);
496
+ const [mediaLoading, setMediaLoading] = useState(true);
497
+ const [mediaSaving, setMediaSaving] = useState(false);
498
+ const [mediaMessage, setMediaMessage] = useState(null);
499
+ const [escalationConfig, setEscalationConfig] = useState(DEFAULT_ESCALATION_CONFIG);
500
+ const [escalationSnapshot, setEscalationSnapshot] = useState(DEFAULT_ESCALATION_CONFIG);
501
+ const [escalationLoading, setEscalationLoading] = useState(true);
502
+ const [escalationSaving, setEscalationSaving] = useState(false);
503
+ const [escalationMessage, setEscalationMessage] = useState(null);
504
+ const [proactiveConfig, setProactiveConfig] = useState(DEFAULT_PROACTIVE_CONFIG);
505
+ const [proactiveSnapshot, setProactiveSnapshot] = useState(DEFAULT_PROACTIVE_CONFIG);
506
+ const [proactiveLoading, setProactiveLoading] = useState(true);
507
+ const [proactiveSaving, setProactiveSaving] = useState(false);
508
+ const [proactiveMessage, setProactiveMessage] = useState(null);
509
+ const companyId = context.companyId ?? "";
510
+ const companyLabel = context.companyPrefix?.trim() || "this company";
511
+ const configured = Boolean(boardAccess.data?.configured);
512
+ const identity = boardAccess.data?.identity?.trim() || null;
513
+ const routingDirty = JSON.stringify(routingConfig) !== JSON.stringify(routingSnapshot);
514
+ const connectionDirty = JSON.stringify(connectionConfig) !== JSON.stringify(connectionSnapshot);
515
+ const boardConfigDirty = JSON.stringify(boardConfig) !== JSON.stringify(boardSnapshot);
516
+ const accessDirty = JSON.stringify(accessConfig) !== JSON.stringify(accessSnapshot);
517
+ const mediaDirty = JSON.stringify(mediaConfig) !== JSON.stringify(mediaSnapshot);
518
+ const escalationDirty = JSON.stringify(escalationConfig) !== JSON.stringify(escalationSnapshot);
519
+ const proactiveDirty = JSON.stringify(proactiveConfig) !== JSON.stringify(proactiveSnapshot);
520
+ useEffect(() => {
521
+ let cancelled = false;
522
+ async function loadRoutingConfig() {
523
+ setRoutingLoading(true);
524
+ setRoutingMessage(null);
525
+ try {
526
+ const config = await fetchPluginConfig();
527
+ if (cancelled) return;
528
+ const nextRoutingConfig = extractRoutingConfig(config);
529
+ setRoutingConfig(nextRoutingConfig);
530
+ setRoutingSnapshot(nextRoutingConfig);
531
+ } catch (error) {
532
+ if (!cancelled) {
533
+ setRoutingMessage({
534
+ tone: "error",
535
+ title: "Routing settings could not be loaded",
536
+ text: getErrorMessage(error)
537
+ });
505
538
  }
506
- void loadRoutingConfig();
507
- return () => {
508
- cancelled = true;
509
- };
510
- }, []);
511
- useEffect(() => {
512
- let cancelled = false;
513
- async function loadProactiveConfig() {
514
- setProactiveLoading(true);
515
- setProactiveMessage(null);
516
- try {
517
- const config = await fetchPluginConfig();
518
- if (cancelled)
519
- return;
520
- const nextProactiveConfig = extractProactiveConfig(config);
521
- setProactiveConfig(nextProactiveConfig);
522
- setProactiveSnapshot(nextProactiveConfig);
523
- }
524
- catch (error) {
525
- if (!cancelled) {
526
- setProactiveMessage({
527
- tone: "error",
528
- title: "Proactive suggestion settings could not be loaded",
529
- text: getErrorMessage(error),
530
- });
531
- }
532
- }
533
- finally {
534
- if (!cancelled) {
535
- setProactiveLoading(false);
536
- }
537
- }
539
+ } finally {
540
+ if (!cancelled) {
541
+ setRoutingLoading(false);
538
542
  }
539
- void loadProactiveConfig();
540
- return () => {
541
- cancelled = true;
542
- };
543
- }, []);
544
- useEffect(() => {
545
- let cancelled = false;
546
- async function loadEscalationConfig() {
547
- setEscalationLoading(true);
548
- setEscalationMessage(null);
549
- try {
550
- const config = await fetchPluginConfig();
551
- if (cancelled)
552
- return;
553
- const nextEscalationConfig = extractEscalationConfig(config);
554
- setEscalationConfig(nextEscalationConfig);
555
- setEscalationSnapshot(nextEscalationConfig);
556
- }
557
- catch (error) {
558
- if (!cancelled) {
559
- setEscalationMessage({
560
- tone: "error",
561
- title: "Human escalation settings could not be loaded",
562
- text: getErrorMessage(error),
563
- });
564
- }
565
- }
566
- finally {
567
- if (!cancelled) {
568
- setEscalationLoading(false);
569
- }
570
- }
571
- }
572
- void loadEscalationConfig();
573
- return () => {
574
- cancelled = true;
575
- };
576
- }, []);
577
- useEffect(() => {
578
- let cancelled = false;
579
- async function loadMediaConfig() {
580
- setMediaLoading(true);
581
- setMediaMessage(null);
582
- try {
583
- const config = await fetchPluginConfig();
584
- if (cancelled)
585
- return;
586
- const nextMediaConfig = extractMediaConfig(config);
587
- setMediaConfig(nextMediaConfig);
588
- setMediaSnapshot(nextMediaConfig);
589
- }
590
- catch (error) {
591
- if (!cancelled) {
592
- setMediaMessage({
593
- tone: "error",
594
- title: "Media intake settings could not be loaded",
595
- text: getErrorMessage(error),
596
- });
597
- }
598
- }
599
- finally {
600
- if (!cancelled) {
601
- setMediaLoading(false);
602
- }
603
- }
543
+ }
544
+ }
545
+ void loadRoutingConfig();
546
+ return () => {
547
+ cancelled = true;
548
+ };
549
+ }, []);
550
+ useEffect(() => {
551
+ let cancelled = false;
552
+ async function loadProactiveConfig() {
553
+ setProactiveLoading(true);
554
+ setProactiveMessage(null);
555
+ try {
556
+ const config = await fetchPluginConfig();
557
+ if (cancelled) return;
558
+ const nextProactiveConfig = extractProactiveConfig(config);
559
+ setProactiveConfig(nextProactiveConfig);
560
+ setProactiveSnapshot(nextProactiveConfig);
561
+ } catch (error) {
562
+ if (!cancelled) {
563
+ setProactiveMessage({
564
+ tone: "error",
565
+ title: "Proactive suggestion settings could not be loaded",
566
+ text: getErrorMessage(error)
567
+ });
604
568
  }
605
- void loadMediaConfig();
606
- return () => {
607
- cancelled = true;
608
- };
609
- }, []);
610
- useEffect(() => {
611
- let cancelled = false;
612
- async function loadAccessConfig() {
613
- setAccessLoading(true);
614
- setAccessMessage(null);
615
- try {
616
- const config = await fetchPluginConfig();
617
- if (cancelled)
618
- return;
619
- const nextAccessConfig = extractAccessConfig(config);
620
- setAccessConfig(nextAccessConfig);
621
- setAccessSnapshot(nextAccessConfig);
622
- }
623
- catch (error) {
624
- if (!cancelled) {
625
- setAccessMessage({
626
- tone: "error",
627
- title: "Access settings could not be loaded",
628
- text: getErrorMessage(error),
629
- });
630
- }
631
- }
632
- finally {
633
- if (!cancelled) {
634
- setAccessLoading(false);
635
- }
636
- }
569
+ } finally {
570
+ if (!cancelled) {
571
+ setProactiveLoading(false);
637
572
  }
638
- void loadAccessConfig();
639
- return () => {
640
- cancelled = true;
641
- };
642
- }, []);
643
- useEffect(() => {
644
- let cancelled = false;
645
- async function loadConnectionConfig() {
646
- setConnectionLoading(true);
647
- setConnectionMessage(null);
648
- try {
649
- const config = await fetchPluginConfig();
650
- if (cancelled)
651
- return;
652
- const nextConnectionConfig = extractConnectionConfig(config);
653
- setConnectionConfig(nextConnectionConfig);
654
- setConnectionSnapshot(nextConnectionConfig);
655
- }
656
- catch (error) {
657
- if (!cancelled) {
658
- setConnectionMessage({
659
- tone: "error",
660
- title: "Connection settings could not be loaded",
661
- text: getErrorMessage(error),
662
- });
663
- }
664
- }
665
- finally {
666
- if (!cancelled) {
667
- setConnectionLoading(false);
668
- }
669
- }
573
+ }
574
+ }
575
+ void loadProactiveConfig();
576
+ return () => {
577
+ cancelled = true;
578
+ };
579
+ }, []);
580
+ useEffect(() => {
581
+ let cancelled = false;
582
+ async function loadEscalationConfig() {
583
+ setEscalationLoading(true);
584
+ setEscalationMessage(null);
585
+ try {
586
+ const config = await fetchPluginConfig();
587
+ if (cancelled) return;
588
+ const nextEscalationConfig = extractEscalationConfig(config);
589
+ setEscalationConfig(nextEscalationConfig);
590
+ setEscalationSnapshot(nextEscalationConfig);
591
+ } catch (error) {
592
+ if (!cancelled) {
593
+ setEscalationMessage({
594
+ tone: "error",
595
+ title: "Human escalation settings could not be loaded",
596
+ text: getErrorMessage(error)
597
+ });
670
598
  }
671
- void loadConnectionConfig();
672
- return () => {
673
- cancelled = true;
674
- };
675
- }, []);
676
- useEffect(() => {
677
- let cancelled = false;
678
- async function loadBoardConfig() {
679
- setBoardConfigLoading(true);
680
- setBoardConfigMessage(null);
681
- try {
682
- const config = await fetchPluginConfig();
683
- if (cancelled)
684
- return;
685
- const nextBoardConfig = extractBoardConfig(config);
686
- setBoardConfig(nextBoardConfig);
687
- setBoardSnapshot(nextBoardConfig);
688
- }
689
- catch (error) {
690
- if (!cancelled) {
691
- setBoardConfigMessage({
692
- tone: "error",
693
- title: "Board fallback setting could not be loaded",
694
- text: getErrorMessage(error),
695
- });
696
- }
697
- }
698
- finally {
699
- if (!cancelled) {
700
- setBoardConfigLoading(false);
701
- }
702
- }
599
+ } finally {
600
+ if (!cancelled) {
601
+ setEscalationLoading(false);
703
602
  }
704
- void loadBoardConfig();
705
- return () => {
706
- cancelled = true;
707
- };
708
- }, []);
709
- function updateRoutingField(key, value) {
710
- setRoutingConfig((current) => ({ ...current, [key]: value }));
711
- setRoutingMessage(null);
603
+ }
712
604
  }
713
- function addOpsRoute() {
714
- setRoutingConfig((current) => ({
715
- ...current,
716
- opsRoutes: [
717
- ...current.opsRoutes,
718
- { name: "", enabled: true, companyId: "", companyName: "", chatId: "", topicId: "" },
719
- ],
720
- }));
721
- setRoutingMessage(null);
605
+ void loadEscalationConfig();
606
+ return () => {
607
+ cancelled = true;
608
+ };
609
+ }, []);
610
+ useEffect(() => {
611
+ let cancelled = false;
612
+ async function loadMediaConfig() {
613
+ setMediaLoading(true);
614
+ setMediaMessage(null);
615
+ try {
616
+ const config = await fetchPluginConfig();
617
+ if (cancelled) return;
618
+ const nextMediaConfig = extractMediaConfig(config);
619
+ setMediaConfig(nextMediaConfig);
620
+ setMediaSnapshot(nextMediaConfig);
621
+ } catch (error) {
622
+ if (!cancelled) {
623
+ setMediaMessage({
624
+ tone: "error",
625
+ title: "Media intake settings could not be loaded",
626
+ text: getErrorMessage(error)
627
+ });
628
+ }
629
+ } finally {
630
+ if (!cancelled) {
631
+ setMediaLoading(false);
632
+ }
633
+ }
722
634
  }
723
- function updateOpsRoute(index, key, value) {
724
- setRoutingConfig((current) => ({
725
- ...current,
726
- opsRoutes: current.opsRoutes.map((route, i) => i === index ? { ...route, [key]: value } : route),
727
- }));
728
- setRoutingMessage(null);
635
+ void loadMediaConfig();
636
+ return () => {
637
+ cancelled = true;
638
+ };
639
+ }, []);
640
+ useEffect(() => {
641
+ let cancelled = false;
642
+ async function loadAccessConfig() {
643
+ setAccessLoading(true);
644
+ setAccessMessage(null);
645
+ try {
646
+ const config = await fetchPluginConfig();
647
+ if (cancelled) return;
648
+ const nextAccessConfig = extractAccessConfig(config);
649
+ setAccessConfig(nextAccessConfig);
650
+ setAccessSnapshot(nextAccessConfig);
651
+ } catch (error) {
652
+ if (!cancelled) {
653
+ setAccessMessage({
654
+ tone: "error",
655
+ title: "Access settings could not be loaded",
656
+ text: getErrorMessage(error)
657
+ });
658
+ }
659
+ } finally {
660
+ if (!cancelled) {
661
+ setAccessLoading(false);
662
+ }
663
+ }
729
664
  }
730
- function removeOpsRoute(index) {
731
- setRoutingConfig((current) => ({
732
- ...current,
733
- opsRoutes: current.opsRoutes.filter((_, i) => i !== index),
734
- }));
735
- setRoutingMessage(null);
665
+ void loadAccessConfig();
666
+ return () => {
667
+ cancelled = true;
668
+ };
669
+ }, []);
670
+ useEffect(() => {
671
+ let cancelled = false;
672
+ async function loadConnectionConfig() {
673
+ setConnectionLoading(true);
674
+ setConnectionMessage(null);
675
+ try {
676
+ const config = await fetchPluginConfig();
677
+ if (cancelled) return;
678
+ const nextConnectionConfig = extractConnectionConfig(config);
679
+ setConnectionConfig(nextConnectionConfig);
680
+ setConnectionSnapshot(nextConnectionConfig);
681
+ } catch (error) {
682
+ if (!cancelled) {
683
+ setConnectionMessage({
684
+ tone: "error",
685
+ title: "Connection settings could not be loaded",
686
+ text: getErrorMessage(error)
687
+ });
688
+ }
689
+ } finally {
690
+ if (!cancelled) {
691
+ setConnectionLoading(false);
692
+ }
693
+ }
736
694
  }
737
- function updateBoardField(key, value) {
738
- setBoardConfig((current) => ({ ...current, [key]: value }));
739
- setBoardConfigMessage(null);
695
+ void loadConnectionConfig();
696
+ return () => {
697
+ cancelled = true;
698
+ };
699
+ }, []);
700
+ useEffect(() => {
701
+ let cancelled = false;
702
+ async function loadBoardConfig() {
703
+ setBoardConfigLoading(true);
704
+ setBoardConfigMessage(null);
705
+ try {
706
+ const config = await fetchPluginConfig();
707
+ if (cancelled) return;
708
+ const nextBoardConfig = extractBoardConfig(config);
709
+ setBoardConfig(nextBoardConfig);
710
+ setBoardSnapshot(nextBoardConfig);
711
+ } catch (error) {
712
+ if (!cancelled) {
713
+ setBoardConfigMessage({
714
+ tone: "error",
715
+ title: "Board fallback setting could not be loaded",
716
+ text: getErrorMessage(error)
717
+ });
718
+ }
719
+ } finally {
720
+ if (!cancelled) {
721
+ setBoardConfigLoading(false);
722
+ }
723
+ }
740
724
  }
741
- function updateAccessField(key, value) {
742
- setAccessConfig((current) => ({ ...current, [key]: value }));
743
- setAccessMessage(null);
725
+ void loadBoardConfig();
726
+ return () => {
727
+ cancelled = true;
728
+ };
729
+ }, []);
730
+ function updateRoutingField(key, value) {
731
+ setRoutingConfig((current) => ({ ...current, [key]: value }));
732
+ setRoutingMessage(null);
733
+ }
734
+ function addOpsRoute() {
735
+ setRoutingConfig((current) => ({
736
+ ...current,
737
+ opsRoutes: [
738
+ ...current.opsRoutes,
739
+ { name: "", enabled: true, companyId: "", companyName: "", chatId: "", topicId: "" }
740
+ ]
741
+ }));
742
+ setRoutingMessage(null);
743
+ }
744
+ function updateOpsRoute(index, key, value) {
745
+ setRoutingConfig((current) => ({
746
+ ...current,
747
+ opsRoutes: current.opsRoutes.map(
748
+ (route, i) => i === index ? { ...route, [key]: value } : route
749
+ )
750
+ }));
751
+ setRoutingMessage(null);
752
+ }
753
+ function removeOpsRoute(index) {
754
+ setRoutingConfig((current) => ({
755
+ ...current,
756
+ opsRoutes: current.opsRoutes.filter((_, i) => i !== index)
757
+ }));
758
+ setRoutingMessage(null);
759
+ }
760
+ function updateBoardField(key, value) {
761
+ setBoardConfig((current) => ({ ...current, [key]: value }));
762
+ setBoardConfigMessage(null);
763
+ }
764
+ function updateAccessField(key, value) {
765
+ setAccessConfig((current) => ({ ...current, [key]: value }));
766
+ setAccessMessage(null);
767
+ }
768
+ function updateConnectionField(key, value) {
769
+ setConnectionConfig((current) => ({ ...current, [key]: value }));
770
+ setConnectionMessage(null);
771
+ }
772
+ function updateMediaField(key, value) {
773
+ setMediaConfig((current) => ({ ...current, [key]: value }));
774
+ setMediaMessage(null);
775
+ }
776
+ function updateEscalationField(key, value) {
777
+ setEscalationConfig((current) => ({ ...current, [key]: value }));
778
+ setEscalationMessage(null);
779
+ }
780
+ function updateProactiveField(key, value) {
781
+ setProactiveConfig((current) => ({ ...current, [key]: value }));
782
+ setProactiveMessage(null);
783
+ }
784
+ async function handleSaveBoardConfig() {
785
+ setBoardConfigSaving(true);
786
+ setBoardConfigMessage(null);
787
+ try {
788
+ const currentConfig = await fetchPluginConfig();
789
+ const nextConfig = { ...currentConfig, ...boardConfig };
790
+ await savePluginConfig(nextConfig);
791
+ setBoardSnapshot(boardConfig);
792
+ setBoardConfigMessage({
793
+ tone: "success",
794
+ title: "Board fallback saved",
795
+ text: "The connection workflow remains preferred. This secret reference is used only as a manual fallback."
796
+ });
797
+ } catch (error) {
798
+ setBoardConfigMessage({
799
+ tone: "error",
800
+ title: "Board fallback could not be saved",
801
+ text: getErrorMessage(error)
802
+ });
803
+ } finally {
804
+ setBoardConfigSaving(false);
744
805
  }
745
- function updateConnectionField(key, value) {
746
- setConnectionConfig((current) => ({ ...current, [key]: value }));
747
- setConnectionMessage(null);
806
+ }
807
+ async function handleSaveAccessConfig() {
808
+ setAccessSaving(true);
809
+ setAccessMessage(null);
810
+ try {
811
+ const currentConfig = await fetchPluginConfig();
812
+ const nextConfig = { ...currentConfig, ...accessConfig };
813
+ await savePluginConfig(nextConfig);
814
+ setAccessSnapshot(accessConfig);
815
+ setAccessMessage({
816
+ tone: "success",
817
+ title: "Bot access settings saved",
818
+ text: "If the worker has already cached Telegram updates, restart the plugin if the new allowlist behavior is not picked up immediately."
819
+ });
820
+ } catch (error) {
821
+ setAccessMessage({
822
+ tone: "error",
823
+ title: "Bot access settings could not be saved",
824
+ text: getErrorMessage(error)
825
+ });
826
+ } finally {
827
+ setAccessSaving(false);
748
828
  }
749
- function updateMediaField(key, value) {
750
- setMediaConfig((current) => ({ ...current, [key]: value }));
751
- setMediaMessage(null);
829
+ }
830
+ async function handleSaveRoutingConfig() {
831
+ setRoutingSaving(true);
832
+ setRoutingMessage(null);
833
+ try {
834
+ const trimmedRoutes = routingConfig.opsRoutes.map((route) => ({
835
+ name: route.name.trim(),
836
+ enabled: route.enabled,
837
+ companyId: route.companyId.trim(),
838
+ companyName: route.companyName.trim(),
839
+ chatId: route.chatId.trim(),
840
+ topicId: route.topicId.trim()
841
+ }));
842
+ const opsRoutes = trimmedRoutes.filter(
843
+ (route) => route.companyId || route.companyName || route.chatId || route.name
844
+ );
845
+ const opsRouteError = validateOpsRoutes(opsRoutes);
846
+ if (opsRouteError) {
847
+ setRoutingMessage({ tone: "error", title: "Ops route is invalid", text: opsRouteError });
848
+ return;
849
+ }
850
+ const sanitizedRouting = { ...routingConfig, opsRoutes };
851
+ const currentConfig = await fetchPluginConfig();
852
+ const nextConfig = { ...currentConfig, ...sanitizedRouting };
853
+ await savePluginConfig(nextConfig);
854
+ setRoutingConfig(sanitizedRouting);
855
+ setRoutingSnapshot(sanitizedRouting);
856
+ setRoutingMessage({
857
+ tone: "success",
858
+ title: "Notification routing saved",
859
+ text: "Refresh the page if another browser tab edited these settings at the same time."
860
+ });
861
+ } catch (error) {
862
+ setRoutingMessage({
863
+ tone: "error",
864
+ title: "Notification routing could not be saved",
865
+ text: getErrorMessage(error)
866
+ });
867
+ } finally {
868
+ setRoutingSaving(false);
752
869
  }
753
- function updateEscalationField(key, value) {
754
- setEscalationConfig((current) => ({ ...current, [key]: value }));
755
- setEscalationMessage(null);
870
+ }
871
+ async function handleConnectBot() {
872
+ const token = botTokenInput.trim();
873
+ if (!token) {
874
+ setBotConnectionMessage({ tone: "error", title: "Enter a bot token first" });
875
+ return;
756
876
  }
757
- function updateProactiveField(key, value) {
758
- setProactiveConfig((current) => ({ ...current, [key]: value }));
759
- setProactiveMessage(null);
877
+ setBotConnecting(true);
878
+ setBotConnectionMessage(null);
879
+ try {
880
+ const result = await updateBotConnection({ token });
881
+ try {
882
+ await savePluginConfig(await fetchPluginConfig());
883
+ } catch {
884
+ }
885
+ setBotTokenInput("");
886
+ await botConnection.refresh?.();
887
+ const who = result?.botUsername ? `@${result.botUsername}` : "your bot";
888
+ setBotConnectionMessage({
889
+ tone: "success",
890
+ title: `Connected ${who} instance-wide`,
891
+ text: "The bot token is stored once for the whole instance \u2014 every company can now reach the board through this bot. No company secret required."
892
+ });
893
+ } catch (error) {
894
+ setBotConnectionMessage({
895
+ tone: "error",
896
+ title: "Could not connect the bot",
897
+ text: getErrorMessage(error)
898
+ });
899
+ } finally {
900
+ setBotConnecting(false);
760
901
  }
761
- async function handleSaveBoardConfig() {
762
- setBoardConfigSaving(true);
763
- setBoardConfigMessage(null);
764
- try {
765
- const currentConfig = await fetchPluginConfig();
766
- const nextConfig = { ...currentConfig, ...boardConfig };
767
- await savePluginConfig(nextConfig);
768
- setBoardSnapshot(boardConfig);
769
- setBoardConfigMessage({
770
- tone: "success",
771
- title: "Board fallback saved",
772
- text: "The connection workflow remains preferred. This secret reference is used only as a manual fallback.",
773
- });
774
- }
775
- catch (error) {
776
- setBoardConfigMessage({
777
- tone: "error",
778
- title: "Board fallback could not be saved",
779
- text: getErrorMessage(error),
780
- });
781
- }
782
- finally {
783
- setBoardConfigSaving(false);
784
- }
902
+ }
903
+ async function handleDisconnectBot() {
904
+ setBotConnecting(true);
905
+ setBotConnectionMessage(null);
906
+ try {
907
+ await clearBotConnection({});
908
+ await botConnection.refresh?.();
909
+ setBotConnectionMessage({
910
+ tone: "success",
911
+ title: "Bot disconnected",
912
+ text: "The stored instance token was cleared. The plugin will idle until a bot is reconnected."
913
+ });
914
+ } catch (error) {
915
+ setBotConnectionMessage({
916
+ tone: "error",
917
+ title: "Could not disconnect the bot",
918
+ text: getErrorMessage(error)
919
+ });
920
+ } finally {
921
+ setBotConnecting(false);
785
922
  }
786
- async function handleSaveAccessConfig() {
787
- setAccessSaving(true);
788
- setAccessMessage(null);
789
- try {
790
- const currentConfig = await fetchPluginConfig();
791
- const nextConfig = { ...currentConfig, ...accessConfig };
792
- await savePluginConfig(nextConfig);
793
- setAccessSnapshot(accessConfig);
794
- setAccessMessage({
795
- tone: "success",
796
- title: "Bot access settings saved",
797
- text: "If the worker has already cached Telegram updates, restart the plugin if the new allowlist behavior is not picked up immediately.",
798
- });
799
- }
800
- catch (error) {
801
- setAccessMessage({
802
- tone: "error",
803
- title: "Bot access settings could not be saved",
804
- text: getErrorMessage(error),
805
- });
806
- }
807
- finally {
808
- setAccessSaving(false);
809
- }
923
+ }
924
+ async function handleSaveConnectionConfig() {
925
+ setConnectionSaving(true);
926
+ setConnectionMessage(null);
927
+ try {
928
+ const currentConfig = await fetchPluginConfig();
929
+ const nextConfig = { ...currentConfig, ...connectionConfig };
930
+ await savePluginConfig(nextConfig);
931
+ setConnectionSnapshot(connectionConfig);
932
+ setConnectionMessage({
933
+ tone: "success",
934
+ title: "Connection settings saved",
935
+ text: "These settings control the bot token and the Paperclip URLs used by Telegram messages and approval actions."
936
+ });
937
+ } catch (error) {
938
+ setConnectionMessage({
939
+ tone: "error",
940
+ title: "Connection settings could not be saved",
941
+ text: getErrorMessage(error)
942
+ });
943
+ } finally {
944
+ setConnectionSaving(false);
810
945
  }
811
- async function handleSaveRoutingConfig() {
812
- setRoutingSaving(true);
813
- setRoutingMessage(null);
814
- try {
815
- // Drop blank rows the operator added but never filled in, then validate
816
- // the rest: every ops route needs a chat ID and a company match key
817
- // (companyId or companyName), and companyId must be unique across routes.
818
- const trimmedRoutes = routingConfig.opsRoutes.map((route) => ({
819
- name: route.name.trim(),
820
- enabled: route.enabled,
821
- companyId: route.companyId.trim(),
822
- companyName: route.companyName.trim(),
823
- chatId: route.chatId.trim(),
824
- topicId: route.topicId.trim(),
825
- }));
826
- const opsRoutes = trimmedRoutes.filter((route) => route.companyId || route.companyName || route.chatId || route.name);
827
- const opsRouteError = validateOpsRoutes(opsRoutes);
828
- if (opsRouteError) {
829
- setRoutingMessage({ tone: "error", title: "Ops route is invalid", text: opsRouteError });
830
- return;
831
- }
832
- const sanitizedRouting = { ...routingConfig, opsRoutes };
833
- const currentConfig = await fetchPluginConfig();
834
- const nextConfig = { ...currentConfig, ...sanitizedRouting };
835
- await savePluginConfig(nextConfig);
836
- setRoutingConfig(sanitizedRouting);
837
- setRoutingSnapshot(sanitizedRouting);
838
- setRoutingMessage({
839
- tone: "success",
840
- title: "Notification routing saved",
841
- text: "Refresh the page if another browser tab edited these settings at the same time.",
842
- });
843
- }
844
- catch (error) {
845
- setRoutingMessage({
846
- tone: "error",
847
- title: "Notification routing could not be saved",
848
- text: getErrorMessage(error),
849
- });
850
- }
851
- finally {
852
- setRoutingSaving(false);
853
- }
946
+ }
947
+ async function handleSaveMediaConfig() {
948
+ setMediaSaving(true);
949
+ setMediaMessage(null);
950
+ try {
951
+ const currentConfig = await fetchPluginConfig();
952
+ const nextConfig = { ...currentConfig, ...mediaConfig };
953
+ await savePluginConfig(nextConfig);
954
+ setMediaSnapshot(mediaConfig);
955
+ setMediaMessage({
956
+ tone: "success",
957
+ title: "Media intake settings saved",
958
+ text: "Media in configured intake chats is routed to the Brief Agent. Media in other chats can still go to active topic agent sessions."
959
+ });
960
+ } catch (error) {
961
+ setMediaMessage({
962
+ tone: "error",
963
+ title: "Media intake settings could not be saved",
964
+ text: getErrorMessage(error)
965
+ });
966
+ } finally {
967
+ setMediaSaving(false);
854
968
  }
855
- async function handleSaveConnectionConfig() {
856
- setConnectionSaving(true);
857
- setConnectionMessage(null);
858
- try {
859
- const currentConfig = await fetchPluginConfig();
860
- const nextConfig = { ...currentConfig, ...connectionConfig };
861
- await savePluginConfig(nextConfig);
862
- setConnectionSnapshot(connectionConfig);
863
- setConnectionMessage({
864
- tone: "success",
865
- title: "Connection settings saved",
866
- text: "These settings control the bot token and the Paperclip URLs used by Telegram messages and approval actions.",
867
- });
868
- }
869
- catch (error) {
870
- setConnectionMessage({
871
- tone: "error",
872
- title: "Connection settings could not be saved",
873
- text: getErrorMessage(error),
874
- });
875
- }
876
- finally {
877
- setConnectionSaving(false);
878
- }
969
+ }
970
+ async function handleSaveEscalationConfig() {
971
+ setEscalationSaving(true);
972
+ setEscalationMessage(null);
973
+ try {
974
+ const currentConfig = await fetchPluginConfig();
975
+ const nextConfig = { ...currentConfig, ...escalationConfig };
976
+ await savePluginConfig(nextConfig);
977
+ setEscalationSnapshot(escalationConfig);
978
+ setEscalationMessage({
979
+ tone: "success",
980
+ title: "Human escalation settings saved",
981
+ text: "Escalations are sent to the configured Telegram chat when an agent invokes the human handoff tool."
982
+ });
983
+ } catch (error) {
984
+ setEscalationMessage({
985
+ tone: "error",
986
+ title: "Human escalation settings could not be saved",
987
+ text: getErrorMessage(error)
988
+ });
989
+ } finally {
990
+ setEscalationSaving(false);
879
991
  }
880
- async function handleSaveMediaConfig() {
881
- setMediaSaving(true);
882
- setMediaMessage(null);
883
- try {
884
- const currentConfig = await fetchPluginConfig();
885
- const nextConfig = { ...currentConfig, ...mediaConfig };
886
- await savePluginConfig(nextConfig);
887
- setMediaSnapshot(mediaConfig);
888
- setMediaMessage({
889
- tone: "success",
890
- title: "Media intake settings saved",
891
- text: "Media in configured intake chats is routed to the Brief Agent. Media in other chats can still go to active topic agent sessions.",
892
- });
893
- }
894
- catch (error) {
895
- setMediaMessage({
896
- tone: "error",
897
- title: "Media intake settings could not be saved",
898
- text: getErrorMessage(error),
899
- });
900
- }
901
- finally {
902
- setMediaSaving(false);
903
- }
992
+ }
993
+ async function handleSaveProactiveConfig() {
994
+ setProactiveSaving(true);
995
+ setProactiveMessage(null);
996
+ try {
997
+ const currentConfig = await fetchPluginConfig();
998
+ const nextConfig = { ...currentConfig, ...proactiveConfig };
999
+ await savePluginConfig(nextConfig);
1000
+ setProactiveSnapshot(proactiveConfig);
1001
+ setProactiveMessage({
1002
+ tone: "success",
1003
+ title: "Proactive suggestion settings saved",
1004
+ text: "These limits apply when the scheduled watch job evaluates registered watches and sends Telegram suggestions."
1005
+ });
1006
+ } catch (error) {
1007
+ setProactiveMessage({
1008
+ tone: "error",
1009
+ title: "Proactive suggestion settings could not be saved",
1010
+ text: getErrorMessage(error)
1011
+ });
1012
+ } finally {
1013
+ setProactiveSaving(false);
904
1014
  }
905
- async function handleSaveEscalationConfig() {
906
- setEscalationSaving(true);
907
- setEscalationMessage(null);
908
- try {
909
- const currentConfig = await fetchPluginConfig();
910
- const nextConfig = { ...currentConfig, ...escalationConfig };
911
- await savePluginConfig(nextConfig);
912
- setEscalationSnapshot(escalationConfig);
913
- setEscalationMessage({
914
- tone: "success",
915
- title: "Human escalation settings saved",
916
- text: "Escalations are sent to the configured Telegram chat when an agent invokes the human handoff tool.",
917
- });
918
- }
919
- catch (error) {
920
- setEscalationMessage({
921
- tone: "error",
922
- title: "Human escalation settings could not be saved",
923
- text: getErrorMessage(error),
924
- });
925
- }
926
- finally {
927
- setEscalationSaving(false);
928
- }
1015
+ }
1016
+ async function handleConnectBoardAccess() {
1017
+ if (!companyId) {
1018
+ setNotice({
1019
+ tone: "error",
1020
+ title: "Open company settings first",
1021
+ text: "Board access tokens are saved as company secrets, so this flow needs a company context."
1022
+ });
1023
+ return;
929
1024
  }
930
- async function handleSaveProactiveConfig() {
931
- setProactiveSaving(true);
932
- setProactiveMessage(null);
933
- try {
934
- const currentConfig = await fetchPluginConfig();
935
- const nextConfig = { ...currentConfig, ...proactiveConfig };
936
- await savePluginConfig(nextConfig);
937
- setProactiveSnapshot(proactiveConfig);
938
- setProactiveMessage({
939
- tone: "success",
940
- title: "Proactive suggestion settings saved",
941
- text: "These limits apply when the scheduled watch job evaluates registered watches and sends Telegram suggestions.",
942
- });
943
- }
944
- catch (error) {
945
- setProactiveMessage({
946
- tone: "error",
947
- title: "Proactive suggestion settings could not be saved",
948
- text: getErrorMessage(error),
949
- });
950
- }
951
- finally {
952
- setProactiveSaving(false);
953
- }
1025
+ setConnecting(true);
1026
+ setNotice(null);
1027
+ let approvalWindow = null;
1028
+ try {
1029
+ if (typeof window !== "undefined") {
1030
+ approvalWindow = window.open("about:blank", "_blank");
1031
+ }
1032
+ const challenge = await requestBoardAccessChallenge(companyId);
1033
+ const approvalUrl = resolveCliAuthUrl(challenge.approvalUrl, challenge.approvalPath);
1034
+ if (!approvalUrl) {
1035
+ throw new Error("Paperclip did not return a trusted board approval URL.");
1036
+ }
1037
+ if (!approvalWindow && typeof window !== "undefined") {
1038
+ approvalWindow = window.open(approvalUrl, "_blank");
1039
+ } else {
1040
+ approvalWindow?.location.replace(approvalUrl);
1041
+ }
1042
+ if (!approvalWindow) {
1043
+ throw new Error("Allow pop-ups for Paperclip, then try connecting board access again.");
1044
+ }
1045
+ const boardApiToken = await waitForBoardAccessApproval(challenge);
1046
+ const nextIdentity = await fetchBoardAccessIdentity(boardApiToken);
1047
+ const secretName = `telegram_board_api_${companyId.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}`;
1048
+ const secret = await resolveOrCreateCompanySecret(companyId, secretName, boardApiToken);
1049
+ await updateBoardAccess({
1050
+ companyId,
1051
+ paperclipBoardApiTokenRef: secret.id,
1052
+ identity: nextIdentity
1053
+ });
1054
+ await boardAccess.refresh();
1055
+ setNotice({
1056
+ tone: "success",
1057
+ title: nextIdentity ? `Connected as ${nextIdentity}` : "Board access connected",
1058
+ text: "Telegram approval actions can now authenticate with Paperclip."
1059
+ });
1060
+ } catch (error) {
1061
+ setNotice({
1062
+ tone: "error",
1063
+ title: "Board access could not be connected",
1064
+ text: getErrorMessage(error)
1065
+ });
1066
+ } finally {
1067
+ setConnecting(false);
1068
+ try {
1069
+ approvalWindow?.close();
1070
+ } catch {
1071
+ }
954
1072
  }
955
- async function handleConnectBoardAccess() {
956
- if (!companyId) {
957
- setNotice({
958
- tone: "error",
959
- title: "Open company settings first",
960
- text: "Board access tokens are saved as company secrets, so this flow needs a company context.",
961
- });
962
- return;
963
- }
964
- setConnecting(true);
965
- setNotice(null);
966
- let approvalWindow = null;
967
- try {
968
- if (typeof window !== "undefined") {
969
- approvalWindow = window.open("about:blank", "_blank");
970
- }
971
- const challenge = await requestBoardAccessChallenge(companyId);
972
- const approvalUrl = resolveCliAuthUrl(challenge.approvalUrl, challenge.approvalPath);
973
- if (!approvalUrl) {
974
- throw new Error("Paperclip did not return a trusted board approval URL.");
975
- }
976
- if (!approvalWindow && typeof window !== "undefined") {
977
- approvalWindow = window.open(approvalUrl, "_blank");
978
- }
979
- else {
980
- approvalWindow?.location.replace(approvalUrl);
1073
+ }
1074
+ return /* @__PURE__ */ jsxs("main", { style: { display: "grid", gap: 24, padding: 24, color: "#111827" }, children: [
1075
+ /* @__PURE__ */ jsxs("section", { style: { display: "grid", gap: 8 }, children: [
1076
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: 24, lineHeight: "32px", margin: 0 }, children: "Telegram Bot" }),
1077
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0, maxWidth: 760 }, children: "Configure Telegram connection, access control, notification routing, media intake, escalation, and proactive suggestion behavior." })
1078
+ ] }),
1079
+ notice ? /* @__PURE__ */ jsxs(
1080
+ "div",
1081
+ {
1082
+ style: {
1083
+ border: `1px solid ${notice.tone === "success" ? "#99f6e4" : "#fecaca"}`,
1084
+ borderRadius: 8,
1085
+ background: notice.tone === "success" ? "#f0fdfa" : "#fef2f2",
1086
+ color: notice.tone === "success" ? "#115e59" : "#991b1b",
1087
+ padding: 14
1088
+ },
1089
+ children: [
1090
+ /* @__PURE__ */ jsx("strong", { children: notice.title }),
1091
+ notice.text ? /* @__PURE__ */ jsx("p", { style: { margin: "6px 0 0" }, children: notice.text }) : null
1092
+ ]
1093
+ }
1094
+ ) : null,
1095
+ /* @__PURE__ */ jsxs(
1096
+ "section",
1097
+ {
1098
+ style: {
1099
+ border: "1px solid #e5e7eb",
1100
+ borderRadius: 8,
1101
+ display: "grid",
1102
+ gap: 18,
1103
+ padding: 18
1104
+ },
1105
+ children: [
1106
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
1107
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Bot Connection" }),
1108
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Connect your Telegram bot once for the whole instance. Every company can then reach the board through this bot \u2014 no per-company secret needed. The token is validated with Telegram and stored securely server-side; it is never shown again here." })
1109
+ ] }),
1110
+ botConnection.loading ? /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Checking bot connection\u2026" }) : botConnection.data?.configured ? /* @__PURE__ */ jsxs(
1111
+ "div",
1112
+ {
1113
+ style: {
1114
+ background: "#f0fdf4",
1115
+ border: "1px solid #bbf7d0",
1116
+ borderRadius: 8,
1117
+ color: "#166534",
1118
+ display: "grid",
1119
+ gap: 4,
1120
+ padding: 14
1121
+ },
1122
+ children: [
1123
+ /* @__PURE__ */ jsx("strong", { children: botConnection.data.source === "instance-state" ? `Connected${botConnection.data.botUsername ? ` as @${botConnection.data.botUsername}` : ""} (instance-wide)` : "Connected via legacy secret reference" }),
1124
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 13 }, children: botConnection.data.source === "instance-state" ? "This bot serves every company on the instance." : "Using the advanced telegramBotTokenRef secret below. Reconnect above to switch to the instance-wide token store." })
1125
+ ]
981
1126
  }
982
- if (!approvalWindow) {
983
- throw new Error("Allow pop-ups for Paperclip, then try connecting board access again.");
1127
+ ) : /* @__PURE__ */ jsxs(
1128
+ "div",
1129
+ {
1130
+ style: {
1131
+ background: "#fffbeb",
1132
+ border: "1px solid #fde68a",
1133
+ borderRadius: 8,
1134
+ color: "#92400e",
1135
+ padding: 14
1136
+ },
1137
+ children: [
1138
+ /* @__PURE__ */ jsx("strong", { children: "No bot connected." }),
1139
+ " Paste a bot token from @BotFather below to connect."
1140
+ ]
984
1141
  }
985
- const boardApiToken = await waitForBoardAccessApproval(challenge);
986
- const nextIdentity = await fetchBoardAccessIdentity(boardApiToken);
987
- const secretName = `telegram_board_api_${companyId.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}`;
988
- const secret = await resolveOrCreateCompanySecret(companyId, secretName, boardApiToken);
989
- await updateBoardAccess({
990
- companyId,
991
- paperclipBoardApiTokenRef: secret.id,
992
- identity: nextIdentity,
993
- });
994
- await boardAccess.refresh();
995
- setNotice({
996
- tone: "success",
997
- title: nextIdentity ? `Connected as ${nextIdentity}` : "Board access connected",
998
- text: "Telegram approval actions can now authenticate with Paperclip.",
999
- });
1000
- }
1001
- catch (error) {
1002
- setNotice({
1003
- tone: "error",
1004
- title: "Board access could not be connected",
1005
- text: getErrorMessage(error),
1006
- });
1007
- }
1008
- finally {
1009
- setConnecting(false);
1010
- try {
1011
- approvalWindow?.close();
1142
+ ),
1143
+ /* @__PURE__ */ jsx("div", { style: { display: "grid", gap: 12 }, children: /* @__PURE__ */ jsx(
1144
+ TextField,
1145
+ {
1146
+ disabled: botConnecting,
1147
+ label: "Telegram bot token",
1148
+ onChange: (value) => setBotTokenInput(value),
1149
+ placeholder: "123456789:AA\u2026 (from @BotFather)",
1150
+ type: "password",
1151
+ value: botTokenInput,
1152
+ children: "Pasted once and stored server-side for the whole instance. Leave blank to keep the current connection."
1012
1153
  }
1013
- catch {
1014
- // Ignore browser close restrictions.
1154
+ ) }),
1155
+ botConnectionMessage ? /* @__PURE__ */ jsx(NoticeBlock, { notice: botConnectionMessage }) : null,
1156
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [
1157
+ botConnection.data?.configured && botConnection.data.source === "instance-state" ? /* @__PURE__ */ jsx(
1158
+ "button",
1159
+ {
1160
+ disabled: botConnecting,
1161
+ onClick: () => void handleDisconnectBot(),
1162
+ style: {
1163
+ background: "white",
1164
+ border: "1px solid #d1d5db",
1165
+ borderRadius: 8,
1166
+ color: "#374151",
1167
+ cursor: botConnecting ? "not-allowed" : "pointer",
1168
+ fontWeight: 700,
1169
+ padding: "10px 14px"
1170
+ },
1171
+ children: "Disconnect"
1172
+ }
1173
+ ) : null,
1174
+ /* @__PURE__ */ jsx(
1175
+ "button",
1176
+ {
1177
+ disabled: botConnecting || !botTokenInput.trim(),
1178
+ onClick: () => void handleConnectBot(),
1179
+ style: {
1180
+ background: botConnecting || !botTokenInput.trim() ? "#9ca3af" : "#111827",
1181
+ border: "none",
1182
+ borderRadius: 8,
1183
+ color: "white",
1184
+ cursor: botConnecting || !botTokenInput.trim() ? "not-allowed" : "pointer",
1185
+ fontWeight: 700,
1186
+ padding: "10px 14px"
1187
+ },
1188
+ children: botConnecting ? "Connecting\u2026" : "Connect bot"
1189
+ }
1190
+ )
1191
+ ] })
1192
+ ]
1193
+ }
1194
+ ),
1195
+ /* @__PURE__ */ jsxs(
1196
+ "section",
1197
+ {
1198
+ style: {
1199
+ border: "1px solid #e5e7eb",
1200
+ borderRadius: 8,
1201
+ display: "grid",
1202
+ gap: 18,
1203
+ padding: 18
1204
+ },
1205
+ children: [
1206
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
1207
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Connection & URLs" }),
1208
+ /* @__PURE__ */ jsxs("p", { style: { color: "#6b7280", margin: 0 }, children: [
1209
+ "Paperclip URLs used by the Telegram worker. The bot token is configured above in ",
1210
+ /* @__PURE__ */ jsx("strong", { children: "Bot Connection" }),
1211
+ "; the secret-ref field below is an advanced fallback for legacy installs."
1212
+ ] })
1213
+ ] }),
1214
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 12 }, children: [
1215
+ /* @__PURE__ */ jsxs(
1216
+ TextField,
1217
+ {
1218
+ disabled: connectionLoading || connectionSaving,
1219
+ label: "Telegram bot token secret ref (advanced / legacy)",
1220
+ onChange: (value) => updateConnectionField("telegramBotTokenRef", value),
1221
+ placeholder: "Secret UUID from Paperclip settings",
1222
+ value: connectionConfig.telegramBotTokenRef,
1223
+ children: [
1224
+ "Optional fallback. Secret UUID for your bot token. Prefer ",
1225
+ /* @__PURE__ */ jsx("strong", { children: "Bot Connection" }),
1226
+ " above \u2014 secret refs are company-scoped and are disabled on recent paperclipai master (post-#5429)."
1227
+ ]
1228
+ }
1229
+ ),
1230
+ /* @__PURE__ */ jsx(
1231
+ TextField,
1232
+ {
1233
+ disabled: connectionLoading || connectionSaving,
1234
+ label: "Paperclip API URL",
1235
+ onChange: (value) => updateConnectionField("paperclipBaseUrl", value),
1236
+ placeholder: "http://localhost:3100",
1237
+ value: connectionConfig.paperclipBaseUrl,
1238
+ children: "Internal Paperclip API URL used by the plugin for actions such as approvals and comments. Keep localhost for same-server deployments."
1239
+ }
1240
+ ),
1241
+ /* @__PURE__ */ jsx(
1242
+ TextField,
1243
+ {
1244
+ disabled: connectionLoading || connectionSaving,
1245
+ label: "Paperclip public URL",
1246
+ onChange: (value) => updateConnectionField("paperclipPublicUrl", value),
1247
+ placeholder: "https://paperclip.example.com",
1248
+ value: connectionConfig.paperclipPublicUrl,
1249
+ children: "Public URL used in Telegram links. Leave empty to fall back to the API URL."
1250
+ }
1251
+ )
1252
+ ] }),
1253
+ connectionMessage ? /* @__PURE__ */ jsx(NoticeBlock, { notice: connectionMessage }) : null,
1254
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [
1255
+ /* @__PURE__ */ jsx(
1256
+ "button",
1257
+ {
1258
+ disabled: connectionLoading || connectionSaving,
1259
+ onClick: () => {
1260
+ setConnectionConfig(connectionSnapshot);
1261
+ setConnectionMessage(null);
1262
+ },
1263
+ style: {
1264
+ background: "white",
1265
+ border: "1px solid #d1d5db",
1266
+ borderRadius: 8,
1267
+ color: "#374151",
1268
+ cursor: connectionLoading || connectionSaving ? "not-allowed" : "pointer",
1269
+ fontWeight: 700,
1270
+ padding: "10px 14px"
1271
+ },
1272
+ type: "button",
1273
+ children: "Reset"
1274
+ }
1275
+ ),
1276
+ /* @__PURE__ */ jsx(
1277
+ "button",
1278
+ {
1279
+ disabled: connectionLoading || connectionSaving || !connectionDirty,
1280
+ onClick: () => {
1281
+ void handleSaveConnectionConfig();
1282
+ },
1283
+ style: {
1284
+ background: connectionLoading || connectionSaving || !connectionDirty ? "#9ca3af" : "#111827",
1285
+ border: 0,
1286
+ borderRadius: 8,
1287
+ color: "white",
1288
+ cursor: connectionLoading || connectionSaving || !connectionDirty ? "not-allowed" : "pointer",
1289
+ fontWeight: 700,
1290
+ minWidth: 160,
1291
+ padding: "10px 14px"
1292
+ },
1293
+ type: "button",
1294
+ children: connectionSaving ? "Saving..." : "Save connection"
1295
+ }
1296
+ )
1297
+ ] })
1298
+ ]
1299
+ }
1300
+ ),
1301
+ /* @__PURE__ */ jsxs(
1302
+ "section",
1303
+ {
1304
+ style: {
1305
+ border: "1px solid #e5e7eb",
1306
+ borderRadius: 8,
1307
+ display: "grid",
1308
+ gap: 18,
1309
+ padding: 18
1310
+ },
1311
+ children: [
1312
+ /* @__PURE__ */ jsxs("div", { style: { alignItems: "start", display: "flex", gap: 16, justifyContent: "space-between" }, children: [
1313
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
1314
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Board Access Connection" }),
1315
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Telegram approval buttons need board access when Paperclip requires authenticated approval mutations." })
1316
+ ] }),
1317
+ /* @__PURE__ */ jsx(
1318
+ "span",
1319
+ {
1320
+ style: {
1321
+ background: configured ? "#ccfbf1" : "#f3f4f6",
1322
+ borderRadius: 999,
1323
+ color: configured ? "#0f766e" : "#4b5563",
1324
+ fontSize: 12,
1325
+ fontWeight: 700,
1326
+ padding: "5px 10px",
1327
+ whiteSpace: "nowrap"
1328
+ },
1329
+ children: connecting ? "Connecting" : configured ? "Connected" : "Not connected"
1330
+ }
1331
+ )
1332
+ ] }),
1333
+ /* @__PURE__ */ jsxs(
1334
+ "div",
1335
+ {
1336
+ style: {
1337
+ alignItems: "center",
1338
+ background: "#f9fafb",
1339
+ border: "1px solid #e5e7eb",
1340
+ borderRadius: 8,
1341
+ display: "flex",
1342
+ gap: 16,
1343
+ justifyContent: "space-between",
1344
+ padding: 14
1345
+ },
1346
+ children: [
1347
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
1348
+ /* @__PURE__ */ jsx("strong", { children: !companyId ? "Open this page inside a company" : configured ? identity ? `Connected as ${identity}` : `Connected for ${companyLabel}` : `Connect board access for ${companyLabel}` }),
1349
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280" }, children: configured ? "The board token is stored as a Paperclip secret; the plugin keeps only the secret reference." : "This opens a Paperclip approval page, then saves the resulting board token as a company secret." })
1350
+ ] }),
1351
+ /* @__PURE__ */ jsx(
1352
+ "button",
1353
+ {
1354
+ disabled: !companyId || connecting || boardAccess.loading,
1355
+ onClick: () => {
1356
+ void handleConnectBoardAccess();
1357
+ },
1358
+ style: {
1359
+ background: !companyId || connecting || boardAccess.loading ? "#9ca3af" : "#111827",
1360
+ border: 0,
1361
+ borderRadius: 8,
1362
+ color: "white",
1363
+ cursor: !companyId || connecting || boardAccess.loading ? "not-allowed" : "pointer",
1364
+ fontWeight: 700,
1365
+ minWidth: 190,
1366
+ padding: "10px 14px"
1367
+ },
1368
+ type: "button",
1369
+ children: connecting ? "Waiting for approval..." : configured ? "Reconnect board access" : "Connect board access"
1370
+ }
1371
+ )
1372
+ ]
1015
1373
  }
1016
- }
1017
- }
1018
- return (_jsxs("main", { style: { display: "grid", gap: 24, padding: 24, color: "#111827" }, children: [_jsxs("section", { style: { display: "grid", gap: 8 }, children: [_jsx("h1", { style: { fontSize: 24, lineHeight: "32px", margin: 0 }, children: "Telegram Bot" }), _jsx("p", { style: { color: "#6b7280", margin: 0, maxWidth: 760 }, children: "Configure Telegram connection, access control, notification routing, media intake, escalation, and proactive suggestion behavior." })] }), notice ? (_jsxs("div", { style: {
1019
- border: `1px solid ${notice.tone === "success" ? "#99f6e4" : "#fecaca"}`,
1020
- borderRadius: 8,
1021
- background: notice.tone === "success" ? "#f0fdfa" : "#fef2f2",
1022
- color: notice.tone === "success" ? "#115e59" : "#991b1b",
1023
- padding: 14,
1024
- }, children: [_jsx("strong", { children: notice.title }), notice.text ? _jsx("p", { style: { margin: "6px 0 0" }, children: notice.text }) : null] })) : null, _jsxs("section", { style: {
1025
- border: "1px solid #e5e7eb",
1374
+ ),
1375
+ /* @__PURE__ */ jsxs("div", { style: { borderTop: "1px solid #e5e7eb", display: "grid", gap: 12, paddingTop: 14 }, children: [
1376
+ /* @__PURE__ */ jsx(
1377
+ TextField,
1378
+ {
1379
+ disabled: boardConfigLoading || boardConfigSaving,
1380
+ label: "Board API token secret ref fallback",
1381
+ onChange: (value) => updateBoardField("paperclipBoardApiTokenRef", value),
1382
+ placeholder: "Optional Paperclip secret UUID",
1383
+ value: boardConfig.paperclipBoardApiTokenRef,
1384
+ children: "Optional manual fallback for approval buttons and /approve. The Board Access Connection above is preferred because it creates and tracks the company-scoped secret for you."
1385
+ }
1386
+ ),
1387
+ boardConfigMessage ? /* @__PURE__ */ jsx(NoticeBlock, { notice: boardConfigMessage }) : null,
1388
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [
1389
+ /* @__PURE__ */ jsx(
1390
+ "button",
1391
+ {
1392
+ disabled: boardConfigLoading || boardConfigSaving,
1393
+ onClick: () => {
1394
+ setBoardConfig(boardSnapshot);
1395
+ setBoardConfigMessage(null);
1396
+ },
1397
+ style: {
1398
+ background: "white",
1399
+ border: "1px solid #d1d5db",
1026
1400
  borderRadius: 8,
1027
- display: "grid",
1028
- gap: 18,
1029
- padding: 18,
1030
- }, children: [_jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Connection & URLs" }), _jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Core connection values used by the Telegram worker. Save the bot token as a Paperclip secret and paste its secret UUID here." })] }), _jsxs("div", { style: { display: "grid", gap: 12 }, children: [_jsx(TextField, { disabled: connectionLoading || connectionSaving, label: "Telegram bot token secret ref", onChange: (value) => updateConnectionField("telegramBotTokenRef", value), placeholder: "Secret UUID from Paperclip settings", value: connectionConfig.telegramBotTokenRef, children: "Secret UUID for your Telegram bot token from @BotFather. The plugin resolves this secret before polling Telegram." }), _jsx(TextField, { disabled: connectionLoading || connectionSaving, label: "Paperclip API URL", onChange: (value) => updateConnectionField("paperclipBaseUrl", value), placeholder: "http://localhost:3100", value: connectionConfig.paperclipBaseUrl, children: "Internal Paperclip API URL used by the plugin for actions such as approvals and comments. Keep localhost for same-server deployments." }), _jsx(TextField, { disabled: connectionLoading || connectionSaving, label: "Paperclip public URL", onChange: (value) => updateConnectionField("paperclipPublicUrl", value), placeholder: "https://paperclip.example.com", value: connectionConfig.paperclipPublicUrl, children: "Public URL used in Telegram links. Leave empty to fall back to the API URL." })] }), connectionMessage ? _jsx(NoticeBlock, { notice: connectionMessage }) : null, _jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [_jsx("button", { disabled: connectionLoading || connectionSaving, onClick: () => {
1031
- setConnectionConfig(connectionSnapshot);
1032
- setConnectionMessage(null);
1033
- }, style: {
1034
- background: "white",
1035
- border: "1px solid #d1d5db",
1036
- borderRadius: 8,
1037
- color: "#374151",
1038
- cursor: connectionLoading || connectionSaving ? "not-allowed" : "pointer",
1039
- fontWeight: 700,
1040
- padding: "10px 14px",
1041
- }, type: "button", children: "Reset" }), _jsx("button", { disabled: connectionLoading || connectionSaving || !connectionDirty, onClick: () => {
1042
- void handleSaveConnectionConfig();
1043
- }, style: {
1044
- background: connectionLoading || connectionSaving || !connectionDirty ? "#9ca3af" : "#111827",
1045
- border: 0,
1046
- borderRadius: 8,
1047
- color: "white",
1048
- cursor: connectionLoading || connectionSaving || !connectionDirty ? "not-allowed" : "pointer",
1049
- fontWeight: 700,
1050
- minWidth: 160,
1051
- padding: "10px 14px",
1052
- }, type: "button", children: connectionSaving ? "Saving..." : "Save connection" })] })] }), _jsxs("section", { style: {
1053
- border: "1px solid #e5e7eb",
1401
+ color: "#374151",
1402
+ cursor: boardConfigLoading || boardConfigSaving ? "not-allowed" : "pointer",
1403
+ fontWeight: 700,
1404
+ padding: "10px 14px"
1405
+ },
1406
+ type: "button",
1407
+ children: "Reset"
1408
+ }
1409
+ ),
1410
+ /* @__PURE__ */ jsx(
1411
+ "button",
1412
+ {
1413
+ disabled: boardConfigLoading || boardConfigSaving || !boardConfigDirty,
1414
+ onClick: () => {
1415
+ void handleSaveBoardConfig();
1416
+ },
1417
+ style: {
1418
+ background: boardConfigLoading || boardConfigSaving || !boardConfigDirty ? "#9ca3af" : "#111827",
1419
+ border: 0,
1054
1420
  borderRadius: 8,
1055
- display: "grid",
1056
- gap: 18,
1057
- padding: 18,
1058
- }, children: [_jsxs("div", { style: { alignItems: "start", display: "flex", gap: 16, justifyContent: "space-between" }, children: [_jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Board Access Connection" }), _jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Telegram approval buttons need board access when Paperclip requires authenticated approval mutations." })] }), _jsx("span", { style: {
1059
- background: configured ? "#ccfbf1" : "#f3f4f6",
1060
- borderRadius: 999,
1061
- color: configured ? "#0f766e" : "#4b5563",
1062
- fontSize: 12,
1063
- fontWeight: 700,
1064
- padding: "5px 10px",
1065
- whiteSpace: "nowrap",
1066
- }, children: connecting ? "Connecting" : configured ? "Connected" : "Not connected" })] }), _jsxs("div", { style: {
1067
- alignItems: "center",
1068
- background: "#f9fafb",
1069
- border: "1px solid #e5e7eb",
1421
+ color: "white",
1422
+ cursor: boardConfigLoading || boardConfigSaving || !boardConfigDirty ? "not-allowed" : "pointer",
1423
+ fontWeight: 700,
1424
+ minWidth: 160,
1425
+ padding: "10px 14px"
1426
+ },
1427
+ type: "button",
1428
+ children: boardConfigSaving ? "Saving..." : "Save fallback"
1429
+ }
1430
+ )
1431
+ ] })
1432
+ ] }),
1433
+ boardAccess.error ? /* @__PURE__ */ jsxs("p", { style: { color: "#991b1b", margin: 0 }, children: [
1434
+ "Could not read board access state: ",
1435
+ boardAccess.error.message
1436
+ ] }) : null
1437
+ ]
1438
+ }
1439
+ ),
1440
+ /* @__PURE__ */ jsxs(
1441
+ "section",
1442
+ {
1443
+ style: {
1444
+ border: "1px solid #e5e7eb",
1445
+ borderRadius: 8,
1446
+ display: "grid",
1447
+ gap: 18,
1448
+ padding: 18
1449
+ },
1450
+ children: [
1451
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
1452
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Bot Interaction & Access Control" }),
1453
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Controls who can use the bot interactively. Empty allowlists are permissive; set both user and chat IDs for strict private-group access." })
1454
+ ] }),
1455
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 12 }, children: [
1456
+ /* @__PURE__ */ jsx(
1457
+ CheckboxField,
1458
+ {
1459
+ checked: accessConfig.enableCommands,
1460
+ disabled: accessLoading || accessSaving,
1461
+ label: "Enable bot commands",
1462
+ onChange: (value) => updateAccessField("enableCommands", value),
1463
+ children: "Allow Telegram users to run commands such as /status, /issues, and /agents. Use allowlists when commands are enabled."
1464
+ }
1465
+ ),
1466
+ /* @__PURE__ */ jsx(
1467
+ CheckboxField,
1468
+ {
1469
+ checked: accessConfig.enableInbound,
1470
+ disabled: accessLoading || accessSaving,
1471
+ label: "Enable inbound replies",
1472
+ onChange: (value) => updateAccessField("enableInbound", value),
1473
+ children: "Route Telegram replies to Paperclip issue comments when a message replies to a bot notification. Use allowlists when inbound replies are enabled."
1474
+ }
1475
+ ),
1476
+ /* @__PURE__ */ jsx(
1477
+ ArrayField,
1478
+ {
1479
+ disabled: accessLoading || accessSaving,
1480
+ emptyValueLabel: "No user IDs configured",
1481
+ label: "Allowed Telegram user IDs",
1482
+ newItemLabel: "Add user ID",
1483
+ onChange: (value) => updateAccessField("allowedTelegramUserIds", value),
1484
+ placeholder: "6395513943",
1485
+ value: accessConfig.allowedTelegramUserIds,
1486
+ children: "Optional. One Telegram user ID per line. Leave empty to allow any user. Applies to commands, inbound replies, media intake, and button callbacks."
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ jsx(
1490
+ ArrayField,
1491
+ {
1492
+ disabled: accessLoading || accessSaving,
1493
+ emptyValueLabel: "No chat IDs configured",
1494
+ label: "Allowed Telegram chat IDs",
1495
+ newItemLabel: "Add chat ID",
1496
+ onChange: (value) => updateAccessField("allowedTelegramChatIds", value),
1497
+ placeholder: "-1003800613668",
1498
+ value: accessConfig.allowedTelegramChatIds,
1499
+ children: "Optional. One chat ID per line. Use private DM IDs and/or private group IDs. If both user and chat allowlists are set, both must match."
1500
+ }
1501
+ )
1502
+ ] }),
1503
+ accessMessage ? /* @__PURE__ */ jsx(NoticeBlock, { notice: accessMessage }) : null,
1504
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [
1505
+ /* @__PURE__ */ jsx(
1506
+ "button",
1507
+ {
1508
+ disabled: accessLoading || accessSaving,
1509
+ onClick: () => {
1510
+ setAccessConfig(accessSnapshot);
1511
+ setAccessMessage(null);
1512
+ },
1513
+ style: {
1514
+ background: "white",
1515
+ border: "1px solid #d1d5db",
1516
+ borderRadius: 8,
1517
+ color: "#374151",
1518
+ cursor: accessLoading || accessSaving ? "not-allowed" : "pointer",
1519
+ fontWeight: 700,
1520
+ padding: "10px 14px"
1521
+ },
1522
+ type: "button",
1523
+ children: "Reset"
1524
+ }
1525
+ ),
1526
+ /* @__PURE__ */ jsx(
1527
+ "button",
1528
+ {
1529
+ disabled: accessLoading || accessSaving || !accessDirty,
1530
+ onClick: () => {
1531
+ void handleSaveAccessConfig();
1532
+ },
1533
+ style: {
1534
+ background: accessLoading || accessSaving || !accessDirty ? "#9ca3af" : "#111827",
1535
+ border: 0,
1536
+ borderRadius: 8,
1537
+ color: "white",
1538
+ cursor: accessLoading || accessSaving || !accessDirty ? "not-allowed" : "pointer",
1539
+ fontWeight: 700,
1540
+ minWidth: 160,
1541
+ padding: "10px 14px"
1542
+ },
1543
+ type: "button",
1544
+ children: accessSaving ? "Saving..." : "Save access"
1545
+ }
1546
+ )
1547
+ ] })
1548
+ ]
1549
+ }
1550
+ ),
1551
+ /* @__PURE__ */ jsxs(
1552
+ "section",
1553
+ {
1554
+ style: {
1555
+ border: "1px solid #e5e7eb",
1556
+ borderRadius: 8,
1557
+ display: "grid",
1558
+ gap: 18,
1559
+ padding: 18
1560
+ },
1561
+ children: [
1562
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
1563
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Notification Routing & Forum Topics" }),
1564
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Grouped operational destinations. Empty Chat IDs fall back to the default route; Topic IDs are optional and only apply inside the matching Telegram forum group." })
1565
+ ] }),
1566
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 12 }, children: [
1567
+ /* @__PURE__ */ jsxs(
1568
+ "section",
1569
+ {
1570
+ style: {
1571
+ border: "1px solid #e5e7eb",
1572
+ borderRadius: 8,
1573
+ display: "grid",
1574
+ gap: 10,
1575
+ padding: 12
1576
+ },
1577
+ children: [
1578
+ /* @__PURE__ */ jsx("strong", { children: "Default route" }),
1579
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5 }, children: [
1580
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Fallback Chat ID" }),
1581
+ /* @__PURE__ */ jsx(
1582
+ "input",
1583
+ {
1584
+ disabled: routingLoading || routingSaving,
1585
+ onChange: (event) => updateRoutingField("defaultChatId", event.currentTarget.value),
1586
+ placeholder: "Default chat ID",
1587
+ style: {
1588
+ border: "1px solid #d1d5db",
1589
+ borderRadius: 8,
1590
+ fontSize: 14,
1591
+ minWidth: 0,
1592
+ padding: "9px 10px"
1593
+ },
1594
+ type: "text",
1595
+ value: routingConfig.defaultChatId
1596
+ }
1597
+ ),
1598
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Used when a notification type leaves its Chat ID empty and no company-specific chat is connected." })
1599
+ ] }),
1600
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1601
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1602
+ /* @__PURE__ */ jsx(
1603
+ "input",
1604
+ {
1605
+ checked: routingConfig.topicRouting,
1606
+ disabled: routingLoading || routingSaving,
1607
+ onChange: (event) => updateRoutingField("topicRouting", event.currentTarget.checked),
1608
+ type: "checkbox"
1609
+ }
1610
+ ),
1611
+ "Forum topic routing"
1612
+ ] }),
1613
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Route project-linked notifications to Telegram forum topics mapped with /connect_topic." })
1614
+ ] }),
1615
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5 }, children: [
1616
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Max agents per forum topic" }),
1617
+ /* @__PURE__ */ jsx(
1618
+ "input",
1619
+ {
1620
+ disabled: routingLoading || routingSaving,
1621
+ min: 1,
1622
+ onChange: (event) => updateRoutingField("maxAgentsPerThread", Number(event.currentTarget.value)),
1623
+ placeholder: "3",
1624
+ style: {
1625
+ border: "1px solid #d1d5db",
1626
+ borderRadius: 8,
1627
+ fontSize: 14,
1628
+ maxWidth: 180,
1629
+ minWidth: 0,
1630
+ padding: "9px 10px"
1631
+ },
1632
+ type: "number",
1633
+ value: routingConfig.maxAgentsPerThread
1634
+ }
1635
+ ),
1636
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Maximum concurrent agent sessions allowed inside one Telegram forum topic. This applies to /acp agent sessions, not notification delivery." })
1637
+ ] })
1638
+ ]
1639
+ }
1640
+ ),
1641
+ /* @__PURE__ */ jsxs(
1642
+ "section",
1643
+ {
1644
+ style: {
1645
+ border: "1px solid #e5e7eb",
1646
+ borderRadius: 8,
1647
+ display: "grid",
1648
+ gap: 10,
1649
+ padding: 12
1650
+ },
1651
+ children: [
1652
+ /* @__PURE__ */ jsx("strong", { children: "Issues" }),
1653
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 10 }, children: [
1654
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1655
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1656
+ /* @__PURE__ */ jsx(
1657
+ "input",
1658
+ {
1659
+ checked: routingConfig.notifyOnIssueCreated,
1660
+ disabled: routingLoading || routingSaving,
1661
+ onChange: (event) => updateRoutingField("notifyOnIssueCreated", event.currentTarget.checked),
1662
+ type: "checkbox"
1663
+ }
1664
+ ),
1665
+ "Created"
1666
+ ] }),
1667
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send a Telegram notification when a new issue is created." })
1668
+ ] }),
1669
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1670
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1671
+ /* @__PURE__ */ jsx(
1672
+ "input",
1673
+ {
1674
+ checked: routingConfig.notifyOnIssueDone,
1675
+ disabled: routingLoading || routingSaving,
1676
+ onChange: (event) => updateRoutingField("notifyOnIssueDone", event.currentTarget.checked),
1677
+ type: "checkbox"
1678
+ }
1679
+ ),
1680
+ "Completed"
1681
+ ] }),
1682
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send a Telegram notification when an issue is completed." })
1683
+ ] }),
1684
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1685
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1686
+ /* @__PURE__ */ jsx(
1687
+ "input",
1688
+ {
1689
+ checked: routingConfig.notifyOnIssueAssigned,
1690
+ disabled: routingLoading || routingSaving,
1691
+ onChange: (event) => updateRoutingField("notifyOnIssueAssigned", event.currentTarget.checked),
1692
+ type: "checkbox"
1693
+ }
1694
+ ),
1695
+ "Assignment changes"
1696
+ ] }),
1697
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send a Telegram notification when an issue assignee changes." })
1698
+ ] }),
1699
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1700
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1701
+ /* @__PURE__ */ jsx(
1702
+ "input",
1703
+ {
1704
+ checked: routingConfig.notifyOnIssueBlocked,
1705
+ disabled: routingLoading || routingSaving,
1706
+ onChange: (event) => updateRoutingField("notifyOnIssueBlocked", event.currentTarget.checked),
1707
+ type: "checkbox"
1708
+ }
1709
+ ),
1710
+ "Blocked"
1711
+ ] }),
1712
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Notify when an issue becomes blocked and is owned by a human/board user. Agent-only blocks are ignored to reduce noise." })
1713
+ ] })
1714
+ ] }),
1715
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5 }, children: [
1716
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Only when assigned to user ID" }),
1717
+ /* @__PURE__ */ jsx(
1718
+ "input",
1719
+ {
1720
+ disabled: routingLoading || routingSaving,
1721
+ onChange: (event) => updateRoutingField("onlyNotifyIfAssignedTo", event.currentTarget.value),
1722
+ placeholder: "Paperclip user ID",
1723
+ style: {
1724
+ border: "1px solid #d1d5db",
1725
+ borderRadius: 8,
1726
+ fontSize: 14,
1727
+ minWidth: 0,
1728
+ padding: "9px 10px"
1729
+ },
1730
+ type: "text",
1731
+ value: routingConfig.onlyNotifyIfAssignedTo
1732
+ }
1733
+ ),
1734
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Optional. Restricts assignment-change notifications to issues assigned to this Paperclip user." })
1735
+ ] }),
1736
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 10 }, children: [
1737
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1738
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1739
+ /* @__PURE__ */ jsx(
1740
+ "input",
1741
+ {
1742
+ checked: routingConfig.notifyOnBoardMention,
1743
+ disabled: routingLoading || routingSaving,
1744
+ onChange: (event) => updateRoutingField("notifyOnBoardMention", event.currentTarget.checked),
1745
+ type: "checkbox"
1746
+ }
1747
+ ),
1748
+ "Board mentions"
1749
+ ] }),
1750
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Notify when an issue comment @-mentions one of the board usernames below. Matching is case-insensitive and word-boundary aware." })
1751
+ ] }),
1752
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5 }, children: [
1753
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Board usernames" }),
1754
+ /* @__PURE__ */ jsx(
1755
+ "input",
1756
+ {
1757
+ disabled: routingLoading || routingSaving || !routingConfig.notifyOnBoardMention,
1758
+ onChange: (event) => updateRoutingField("boardUsernames", event.currentTarget.value),
1759
+ placeholder: "ceo, board (comma-separated, no @)",
1760
+ style: {
1761
+ border: "1px solid #d1d5db",
1070
1762
  borderRadius: 8,
1071
- display: "flex",
1072
- gap: 16,
1073
- justifyContent: "space-between",
1074
- padding: 14,
1075
- }, children: [_jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("strong", { children: !companyId
1076
- ? "Open this page inside a company"
1077
- : configured
1078
- ? identity
1079
- ? `Connected as ${identity}`
1080
- : `Connected for ${companyLabel}`
1081
- : `Connect board access for ${companyLabel}` }), _jsx("span", { style: { color: "#6b7280" }, children: configured
1082
- ? "The board token is stored as a Paperclip secret; the plugin keeps only the secret reference."
1083
- : "This opens a Paperclip approval page, then saves the resulting board token as a company secret." })] }), _jsx("button", { disabled: !companyId || connecting || boardAccess.loading, onClick: () => {
1084
- void handleConnectBoardAccess();
1085
- }, style: {
1086
- background: !companyId || connecting || boardAccess.loading ? "#9ca3af" : "#111827",
1087
- border: 0,
1088
- borderRadius: 8,
1089
- color: "white",
1090
- cursor: !companyId || connecting || boardAccess.loading ? "not-allowed" : "pointer",
1091
- fontWeight: 700,
1092
- minWidth: 190,
1093
- padding: "10px 14px",
1094
- }, type: "button", children: connecting ? "Waiting for approval..." : configured ? "Reconnect board access" : "Connect board access" })] }), _jsxs("div", { style: { borderTop: "1px solid #e5e7eb", display: "grid", gap: 12, paddingTop: 14 }, children: [_jsx(TextField, { disabled: boardConfigLoading || boardConfigSaving, label: "Board API token secret ref fallback", onChange: (value) => updateBoardField("paperclipBoardApiTokenRef", value), placeholder: "Optional Paperclip secret UUID", value: boardConfig.paperclipBoardApiTokenRef, children: "Optional manual fallback for approval buttons and /approve. The Board Access Connection above is preferred because it creates and tracks the company-scoped secret for you." }), boardConfigMessage ? _jsx(NoticeBlock, { notice: boardConfigMessage }) : null, _jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [_jsx("button", { disabled: boardConfigLoading || boardConfigSaving, onClick: () => {
1095
- setBoardConfig(boardSnapshot);
1096
- setBoardConfigMessage(null);
1097
- }, style: {
1098
- background: "white",
1099
- border: "1px solid #d1d5db",
1100
- borderRadius: 8,
1101
- color: "#374151",
1102
- cursor: boardConfigLoading || boardConfigSaving ? "not-allowed" : "pointer",
1103
- fontWeight: 700,
1104
- padding: "10px 14px",
1105
- }, type: "button", children: "Reset" }), _jsx("button", { disabled: boardConfigLoading || boardConfigSaving || !boardConfigDirty, onClick: () => {
1106
- void handleSaveBoardConfig();
1107
- }, style: {
1108
- background: boardConfigLoading || boardConfigSaving || !boardConfigDirty ? "#9ca3af" : "#111827",
1109
- border: 0,
1110
- borderRadius: 8,
1111
- color: "white",
1112
- cursor: boardConfigLoading || boardConfigSaving || !boardConfigDirty ? "not-allowed" : "pointer",
1113
- fontWeight: 700,
1114
- minWidth: 160,
1115
- padding: "10px 14px",
1116
- }, type: "button", children: boardConfigSaving ? "Saving..." : "Save fallback" })] })] }), boardAccess.error ? (_jsxs("p", { style: { color: "#991b1b", margin: 0 }, children: ["Could not read board access state: ", boardAccess.error.message] })) : null] }), _jsxs("section", { style: {
1117
- border: "1px solid #e5e7eb",
1118
- borderRadius: 8,
1119
- display: "grid",
1120
- gap: 18,
1121
- padding: 18,
1122
- }, children: [_jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Bot Interaction & Access Control" }), _jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Controls who can use the bot interactively. Empty allowlists are permissive; set both user and chat IDs for strict private-group access." })] }), _jsxs("div", { style: { display: "grid", gap: 12 }, children: [_jsx(CheckboxField, { checked: accessConfig.enableCommands, disabled: accessLoading || accessSaving, label: "Enable bot commands", onChange: (value) => updateAccessField("enableCommands", value), children: "Allow Telegram users to run commands such as /status, /issues, and /agents. Use allowlists when commands are enabled." }), _jsx(CheckboxField, { checked: accessConfig.enableInbound, disabled: accessLoading || accessSaving, label: "Enable inbound replies", onChange: (value) => updateAccessField("enableInbound", value), children: "Route Telegram replies to Paperclip issue comments when a message replies to a bot notification. Use allowlists when inbound replies are enabled." }), _jsx(ArrayField, { disabled: accessLoading || accessSaving, emptyValueLabel: "No user IDs configured", label: "Allowed Telegram user IDs", newItemLabel: "Add user ID", onChange: (value) => updateAccessField("allowedTelegramUserIds", value), placeholder: "6395513943", value: accessConfig.allowedTelegramUserIds, children: "Optional. One Telegram user ID per line. Leave empty to allow any user. Applies to commands, inbound replies, media intake, and button callbacks." }), _jsx(ArrayField, { disabled: accessLoading || accessSaving, emptyValueLabel: "No chat IDs configured", label: "Allowed Telegram chat IDs", newItemLabel: "Add chat ID", onChange: (value) => updateAccessField("allowedTelegramChatIds", value), placeholder: "-1003800613668", value: accessConfig.allowedTelegramChatIds, children: "Optional. One chat ID per line. Use private DM IDs and/or private group IDs. If both user and chat allowlists are set, both must match." })] }), accessMessage ? _jsx(NoticeBlock, { notice: accessMessage }) : null, _jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [_jsx("button", { disabled: accessLoading || accessSaving, onClick: () => {
1123
- setAccessConfig(accessSnapshot);
1124
- setAccessMessage(null);
1125
- }, style: {
1126
- background: "white",
1127
- border: "1px solid #d1d5db",
1128
- borderRadius: 8,
1129
- color: "#374151",
1130
- cursor: accessLoading || accessSaving ? "not-allowed" : "pointer",
1131
- fontWeight: 700,
1132
- padding: "10px 14px",
1133
- }, type: "button", children: "Reset" }), _jsx("button", { disabled: accessLoading || accessSaving || !accessDirty, onClick: () => {
1134
- void handleSaveAccessConfig();
1135
- }, style: {
1136
- background: accessLoading || accessSaving || !accessDirty ? "#9ca3af" : "#111827",
1137
- border: 0,
1138
- borderRadius: 8,
1139
- color: "white",
1140
- cursor: accessLoading || accessSaving || !accessDirty ? "not-allowed" : "pointer",
1141
- fontWeight: 700,
1142
- minWidth: 160,
1143
- padding: "10px 14px",
1144
- }, type: "button", children: accessSaving ? "Saving..." : "Save access" })] })] }), _jsxs("section", { style: {
1145
- border: "1px solid #e5e7eb",
1146
- borderRadius: 8,
1147
- display: "grid",
1148
- gap: 18,
1149
- padding: 18,
1150
- }, children: [_jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Notification Routing & Forum Topics" }), _jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Grouped operational destinations. Empty Chat IDs fall back to the default route; Topic IDs are optional and only apply inside the matching Telegram forum group." })] }), _jsxs("div", { style: { display: "grid", gap: 12 }, children: [_jsxs("section", { style: {
1151
- border: "1px solid #e5e7eb",
1152
- borderRadius: 8,
1153
- display: "grid",
1154
- gap: 10,
1155
- padding: 12,
1156
- }, children: [_jsx("strong", { children: "Default route" }), _jsxs("label", { style: { display: "grid", gap: 5 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Fallback Chat ID" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("defaultChatId", event.currentTarget.value), placeholder: "Default chat ID", style: {
1157
- border: "1px solid #d1d5db",
1158
- borderRadius: 8,
1159
- fontSize: 14,
1160
- minWidth: 0,
1161
- padding: "9px 10px",
1162
- }, type: "text", value: routingConfig.defaultChatId }), _jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Used when a notification type leaves its Chat ID empty and no company-specific chat is connected." })] }), _jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.topicRouting, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("topicRouting", event.currentTarget.checked), type: "checkbox" }), "Forum topic routing"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Route project-linked notifications to Telegram forum topics mapped with /connect_topic." })] }), _jsxs("label", { style: { display: "grid", gap: 5 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Max agents per forum topic" }), _jsx("input", { disabled: routingLoading || routingSaving, min: 1, onChange: (event) => updateRoutingField("maxAgentsPerThread", Number(event.currentTarget.value)), placeholder: "3", style: {
1163
- border: "1px solid #d1d5db",
1164
- borderRadius: 8,
1165
- fontSize: 14,
1166
- maxWidth: 180,
1167
- minWidth: 0,
1168
- padding: "9px 10px",
1169
- }, type: "number", value: routingConfig.maxAgentsPerThread }), _jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Maximum concurrent agent sessions allowed inside one Telegram forum topic. This applies to /acp agent sessions, not notification delivery." })] })] }), _jsxs("section", { style: {
1170
- border: "1px solid #e5e7eb",
1171
- borderRadius: 8,
1172
- display: "grid",
1173
- gap: 10,
1174
- padding: 12,
1175
- }, children: [_jsx("strong", { children: "Issues" }), _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnIssueCreated, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnIssueCreated", event.currentTarget.checked), type: "checkbox" }), "Created"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send a Telegram notification when a new issue is created." })] }), _jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnIssueDone, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnIssueDone", event.currentTarget.checked), type: "checkbox" }), "Completed"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send a Telegram notification when an issue is completed." })] }), _jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnIssueAssigned, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnIssueAssigned", event.currentTarget.checked), type: "checkbox" }), "Assignment changes"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send a Telegram notification when an issue assignee changes." })] }), _jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnIssueBlocked, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnIssueBlocked", event.currentTarget.checked), type: "checkbox" }), "Blocked"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Notify when an issue becomes blocked and is owned by a human/board user. Agent-only blocks are ignored to reduce noise." })] })] }), _jsxs("label", { style: { display: "grid", gap: 5 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Only when assigned to user ID" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("onlyNotifyIfAssignedTo", event.currentTarget.value), placeholder: "Paperclip user ID", style: {
1176
- border: "1px solid #d1d5db",
1177
- borderRadius: 8,
1178
- fontSize: 14,
1179
- minWidth: 0,
1180
- padding: "9px 10px",
1181
- }, type: "text", value: routingConfig.onlyNotifyIfAssignedTo }), _jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Optional. Restricts assignment-change notifications to issues assigned to this Paperclip user." })] }), _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnBoardMention, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnBoardMention", event.currentTarget.checked), type: "checkbox" }), "Board mentions"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Notify when an issue comment @-mentions one of the board usernames below. Matching is case-insensitive and word-boundary aware." })] }), _jsxs("label", { style: { display: "grid", gap: 5 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Board usernames" }), _jsx("input", { disabled: routingLoading || routingSaving || !routingConfig.notifyOnBoardMention, onChange: (event) => updateRoutingField("boardUsernames", event.currentTarget.value), placeholder: "ceo, board (comma-separated, no @)", style: {
1182
- border: "1px solid #d1d5db",
1183
- borderRadius: 8,
1184
- fontSize: 14,
1185
- minWidth: 0,
1186
- padding: "9px 10px",
1187
- }, type: "text", value: routingConfig.boardUsernames }), _jsxs("span", { style: { color: "#6b7280", fontSize: 12 }, children: ["Comma- or space-separated handles. A comment forwards only when it contains ", _jsx("code", { children: "@<handle>" }), " for one of these."] })] })] })] }), _jsx(RoutingRow, { title: "Approvals", chatId: routingConfig.approvalsChatId, topicId: routingConfig.approvalsTopicId, chatPlaceholder: "Approvals chat ID", topicPlaceholder: "Approvals topic ID", disabled: routingLoading || routingSaving, onChatIdChange: (value) => updateRoutingField("approvalsChatId", value), onTopicIdChange: (value) => updateRoutingField("approvalsTopicId", value), chatHelp: "Leave empty to use the default route for approval notifications.", footer: _jsxs(_Fragment, { children: [_jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnApprovalCreated, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnApprovalCreated", event.currentTarget.checked), type: "checkbox" }), "Enabled"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send Telegram notifications when approval requests are created." })] }), _jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.onlyNotifyBoardApprovals, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("onlyNotifyBoardApprovals", event.currentTarget.checked), type: "checkbox" }), "Board requests only"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Ignore internal approvals and notify only when an agent requests Board approval." })] })] }) }), _jsx(RoutingRow, { title: "Errors", chatId: routingConfig.errorsChatId, topicId: routingConfig.errorsTopicId, chatPlaceholder: "Errors chat ID", topicPlaceholder: "Errors topic ID", disabled: routingLoading || routingSaving, onChatIdChange: (value) => updateRoutingField("errorsChatId", value), onTopicIdChange: (value) => updateRoutingField("errorsTopicId", value), chatHelp: "Leave empty to use the default route for agent error notifications.", footer: _jsxs(_Fragment, { children: [_jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnAgentError, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnAgentError", event.currentTarget.checked), type: "checkbox" }), "Errors enabled"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send Telegram notifications when an agent run reports an error." })] }), _jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnAgentRunStarted, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnAgentRunStarted", event.currentTarget.checked), type: "checkbox" }), "Run started"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Notify on every agent run start. Off by default - high-frequency on busy instances. Routes to a matching Ops route below, otherwise the default chat." })] }), _jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: routingConfig.notifyOnAgentRunFinished, disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("notifyOnAgentRunFinished", event.currentTarget.checked), type: "checkbox" }), "Run finished"] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Notify on every agent run completion. Off by default - high-frequency on busy instances. Routes to a matching Ops route below, otherwise the default chat." })] })] }) }), _jsxs("section", { style: {
1188
- border: "1px solid #e5e7eb",
1189
- borderRadius: 8,
1190
- display: "grid",
1191
- gap: 10,
1192
- padding: 12,
1193
- }, children: [_jsxs("div", { style: { alignItems: "center", display: "flex", justifyContent: "space-between" }, children: [_jsx("strong", { children: "Ops routes" }), _jsx("button", { disabled: routingLoading || routingSaving, onClick: () => addOpsRoute(), style: {
1194
- background: "#111827",
1195
- border: "none",
1196
- borderRadius: 8,
1197
- color: "#fff",
1198
- cursor: routingLoading || routingSaving ? "not-allowed" : "pointer",
1199
- fontSize: 13,
1200
- fontWeight: 600,
1201
- padding: "6px 12px",
1202
- }, type: "button", children: "Add ops route" })] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Divert run-lifecycle (run started / run finished) notifications for a specific company to a dedicated ops chat, keeping the primary chat for important signals. The first enabled route matching by Company ID (or Company name) wins; if none match, ops events fall back to the default chat." }), routingConfig.opsRoutes.length === 0 ? (_jsx("span", { style: { color: "#9ca3af", fontSize: 12, fontStyle: "italic" }, children: "No ops routes configured." })) : (_jsx("div", { style: { display: "grid", gap: 12 }, children: routingConfig.opsRoutes.map((route, index) => (_jsxs("div", { style: {
1203
- border: "1px solid #e5e7eb",
1204
- borderRadius: 8,
1205
- display: "grid",
1206
- gap: 8,
1207
- padding: 10,
1208
- }, children: [_jsxs("div", { style: { alignItems: "center", display: "flex", gap: 12, justifyContent: "space-between" }, children: [_jsxs("label", { style: { alignItems: "center", color: "#374151", display: "flex", fontSize: 13, gap: 8 }, children: [_jsx("input", { checked: route.enabled, disabled: routingLoading || routingSaving, onChange: (event) => updateOpsRoute(index, "enabled", event.currentTarget.checked), type: "checkbox" }), "Enabled"] }), _jsx("button", { disabled: routingLoading || routingSaving, onClick: () => removeOpsRoute(index), style: {
1209
- background: "transparent",
1210
- border: "1px solid #d1d5db",
1211
- borderRadius: 8,
1212
- color: "#b91c1c",
1213
- cursor: routingLoading || routingSaving ? "not-allowed" : "pointer",
1214
- fontSize: 12,
1215
- fontWeight: 600,
1216
- padding: "4px 10px",
1217
- }, type: "button", children: "Remove" })] }), _jsxs("label", { style: { display: "grid", gap: 4 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Name (optional)" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateOpsRoute(index, "name", event.currentTarget.value), placeholder: "e.g. Acme Ops", style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" }, type: "text", value: route.name })] }), _jsxs("div", { style: { display: "grid", gap: 8, gridTemplateColumns: "repeat(2, minmax(0, 1fr))" }, children: [_jsxs("label", { style: { display: "grid", gap: 4 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Company ID" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateOpsRoute(index, "companyId", event.currentTarget.value), placeholder: "Company UUID", style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" }, type: "text", value: route.companyId })] }), _jsxs("label", { style: { display: "grid", gap: 4 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Company name" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateOpsRoute(index, "companyName", event.currentTarget.value), placeholder: "Fallback match by name", style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" }, type: "text", value: route.companyName })] }), _jsxs("label", { style: { display: "grid", gap: 4 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Chat ID" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateOpsRoute(index, "chatId", event.currentTarget.value), placeholder: "Ops chat ID", style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" }, type: "text", value: route.chatId })] }), _jsxs("label", { style: { display: "grid", gap: 4 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Topic ID (optional)" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateOpsRoute(index, "topicId", event.currentTarget.value), placeholder: "Forum topic ID", style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" }, type: "text", value: route.topicId })] })] })] }, index))) }))] }), _jsx(RoutingRow, { title: "Digests", chatId: routingConfig.digestChatId, topicId: routingConfig.digestTopicId, chatPlaceholder: "Digest chat ID", topicPlaceholder: "Digest topic ID", disabled: routingLoading || routingSaving, onChatIdChange: (value) => updateRoutingField("digestChatId", value), onTopicIdChange: (value) => updateRoutingField("digestTopicId", value), chatHelp: "Leave empty to use the company/default route for digest notifications.", footer: _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsxs("label", { style: { display: "grid", gap: 6 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Mode" }), _jsxs("select", { disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("digestMode", event.currentTarget.value), style: {
1218
- border: "1px solid #d1d5db",
1219
- borderRadius: 8,
1220
- fontSize: 14,
1221
- maxWidth: 280,
1222
- padding: "9px 10px",
1223
- }, value: routingConfig.digestMode, children: [_jsx("option", { value: "off", children: "Off" }), _jsx("option", { value: "daily", children: "Daily" }), _jsx("option", { value: "bidaily", children: "Bidaily" }), _jsx("option", { value: "tridaily", children: "Tridaily" })] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Off disables digest notifications. Times are UTC." })] }), _jsxs("div", { style: { alignItems: "stretch", display: "grid", gap: 10, gridTemplateColumns: "repeat(3, minmax(0, 1fr))" }, children: [_jsxs("label", { style: { display: "grid", gap: 5, gridTemplateRows: "auto auto minmax(32px, auto)" }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Daily time" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("dailyDigestTime", event.currentTarget.value), placeholder: "09:00", style: {
1224
- border: "1px solid #d1d5db",
1225
- borderRadius: 8,
1226
- fontSize: 14,
1227
- minWidth: 0,
1228
- padding: "9px 10px",
1229
- }, type: "text", value: routingConfig.dailyDigestTime }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, lineHeight: "16px" }, children: "Used for daily mode and as the first bidaily slot." })] }), _jsxs("label", { style: { display: "grid", gap: 5, gridTemplateRows: "auto auto minmax(32px, auto)" }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Bidaily second time" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("bidailySecondTime", event.currentTarget.value), placeholder: "17:00", style: {
1230
- border: "1px solid #d1d5db",
1231
- borderRadius: 8,
1232
- fontSize: 14,
1233
- minWidth: 0,
1234
- padding: "9px 10px",
1235
- }, type: "text", value: routingConfig.bidailySecondTime }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, lineHeight: "16px" }, children: "Second send time when bidaily mode is selected." })] }), _jsxs("label", { style: { display: "grid", gap: 5, gridTemplateRows: "auto auto minmax(32px, auto)" }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Tridaily times" }), _jsx("input", { disabled: routingLoading || routingSaving, onChange: (event) => updateRoutingField("tridailyTimes", event.currentTarget.value), placeholder: "07:00,13:00,19:00", style: {
1236
- border: "1px solid #d1d5db",
1237
- borderRadius: 8,
1238
- fontSize: 14,
1239
- minWidth: 0,
1240
- padding: "9px 10px",
1241
- }, type: "text", value: routingConfig.tridailyTimes }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, lineHeight: "16px" }, children: "Three comma-separated UTC times for tridaily mode." })] })] })] }) })] }), routingMessage ? _jsx(NoticeBlock, { notice: routingMessage }) : null, _jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [_jsx("button", { disabled: routingLoading || routingSaving, onClick: () => {
1242
- setRoutingConfig(routingSnapshot);
1243
- setRoutingMessage(null);
1244
- }, style: {
1245
- background: "white",
1246
- border: "1px solid #d1d5db",
1247
- borderRadius: 8,
1248
- color: "#374151",
1249
- cursor: routingLoading || routingSaving ? "not-allowed" : "pointer",
1250
- fontWeight: 700,
1251
- padding: "10px 14px",
1252
- }, type: "button", children: "Reset" }), _jsx("button", { disabled: routingLoading || routingSaving || !routingDirty, onClick: () => {
1253
- void handleSaveRoutingConfig();
1254
- }, style: {
1255
- background: routingLoading || routingSaving || !routingDirty ? "#9ca3af" : "#111827",
1256
- border: 0,
1257
- borderRadius: 8,
1258
- color: "white",
1259
- cursor: routingLoading || routingSaving || !routingDirty ? "not-allowed" : "pointer",
1260
- fontWeight: 700,
1261
- minWidth: 160,
1262
- padding: "10px 14px",
1263
- }, type: "button", children: routingSaving ? "Saving..." : "Save routing" })] })] }), _jsxs("section", { style: {
1264
- border: "1px solid #e5e7eb",
1265
- borderRadius: 8,
1266
- display: "grid",
1267
- gap: 18,
1268
- padding: 18,
1269
- }, children: [_jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Media Intake / Brief Agent" }), _jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Routes Telegram voice, audio, documents, and photos either to a Brief Agent intake flow or to active agent sessions inside forum topics." })] }), _jsxs("div", { style: { display: "grid", gap: 12 }, children: [_jsx(TextField, { disabled: mediaLoading || mediaSaving, label: "Transcription API key secret ref", onChange: (value) => updateMediaField("transcriptionApiKeyRef", value), placeholder: "OpenAI API key secret UUID", value: mediaConfig.transcriptionApiKeyRef, children: "Secret UUID for the OpenAI API key used to transcribe voice and audio before routing media to the Brief Agent or an active topic agent session." }), _jsx(TextField, { disabled: mediaLoading || mediaSaving, label: "Brief Agent ID", onChange: (value) => updateMediaField("briefAgentId", value), placeholder: "Paperclip agent ID", value: mediaConfig.briefAgentId, children: "Agent ID that processes media intake briefs. Leave empty to disable the dedicated Brief Agent intake flow." }), _jsx(ArrayField, { disabled: mediaLoading || mediaSaving, emptyValueLabel: "No intake chat IDs configured", label: "Brief Agent intake chat IDs", newItemLabel: "Add intake chat ID", onChange: (value) => updateMediaField("briefAgentChatIds", value), placeholder: "-1003800613668", value: mediaConfig.briefAgentChatIds, children: "Telegram chat IDs where media is routed to the Brief Agent. Media in other chats goes to active agent sessions when a matching forum topic session exists." })] }), mediaMessage ? _jsx(NoticeBlock, { notice: mediaMessage }) : null, _jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [_jsx("button", { disabled: mediaLoading || mediaSaving, onClick: () => {
1270
- setMediaConfig(mediaSnapshot);
1271
- setMediaMessage(null);
1272
- }, style: {
1273
- background: "white",
1274
- border: "1px solid #d1d5db",
1275
- borderRadius: 8,
1276
- color: "#374151",
1277
- cursor: mediaLoading || mediaSaving ? "not-allowed" : "pointer",
1278
- fontWeight: 700,
1279
- padding: "10px 14px",
1280
- }, type: "button", children: "Reset" }), _jsx("button", { disabled: mediaLoading || mediaSaving || !mediaDirty, onClick: () => {
1281
- void handleSaveMediaConfig();
1282
- }, style: {
1283
- background: mediaLoading || mediaSaving || !mediaDirty ? "#9ca3af" : "#111827",
1284
- border: 0,
1285
- borderRadius: 8,
1286
- color: "white",
1287
- cursor: mediaLoading || mediaSaving || !mediaDirty ? "not-allowed" : "pointer",
1288
- fontWeight: 700,
1289
- minWidth: 160,
1290
- padding: "10px 14px",
1291
- }, type: "button", children: mediaSaving ? "Saving..." : "Save media intake" })] })] }), _jsxs("section", { style: {
1292
- border: "1px solid #e5e7eb",
1293
- borderRadius: 8,
1294
- display: "grid",
1295
- gap: 18,
1296
- padding: 18,
1297
- }, children: [_jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Human Escalation" }), _jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Controls where human handoff requests go and what the bot tells the original Telegram user while waiting." })] }), _jsxs("div", { style: { display: "grid", gap: 12 }, children: [_jsx(TextField, { disabled: escalationLoading || escalationSaving, label: "Escalation Chat ID", onChange: (value) => updateEscalationField("escalationChatId", value), placeholder: "-1003800613668", value: escalationConfig.escalationChatId, children: "Telegram chat ID where escalations are sent for human review. Leave empty to log escalations without forwarding them to Telegram." }), _jsxs("div", { style: twoColumnGridStyle, children: [_jsxs("label", { style: pairedFieldStyle, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Escalation timeout (ms)" }), _jsx("input", { disabled: escalationLoading || escalationSaving, min: 0, onChange: (event) => updateEscalationField("escalationTimeoutMs", Number(event.currentTarget.value)), placeholder: "900000", style: standardInputStyle, type: "number", value: escalationConfig.escalationTimeoutMs }), _jsx("span", { style: helperTextStyle, children: "How long to wait for a human response. Default is 900000 ms, or 15 minutes." })] }), _jsxs("label", { style: pairedFieldStyle, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Default action on timeout" }), _jsxs("select", { disabled: escalationLoading || escalationSaving, onChange: (event) => updateEscalationField("escalationDefaultAction", event.currentTarget.value), style: standardInputStyle, value: escalationConfig.escalationDefaultAction, children: [_jsx("option", { value: "defer", children: "Defer" }), _jsx("option", { value: "auto_reply", children: "Auto reply" }), _jsx("option", { value: "close", children: "Close" })] }), _jsx("span", { style: helperTextStyle, children: "Defer does nothing, auto reply sends the suggested reply, and close ends the escalation path." })] })] }), _jsx(TextAreaField, { disabled: escalationLoading || escalationSaving, label: "Hold message", onChange: (value) => updateEscalationField("escalationHoldMessage", value), placeholder: "Let me check on that - I'll get back to you shortly.", rows: 3, value: escalationConfig.escalationHoldMessage, children: "Message sent to the original Telegram user when their conversation is escalated to a human." })] }), escalationMessage ? _jsx(NoticeBlock, { notice: escalationMessage }) : null, _jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [_jsx("button", { disabled: escalationLoading || escalationSaving, onClick: () => {
1298
- setEscalationConfig(escalationSnapshot);
1299
- setEscalationMessage(null);
1300
- }, style: {
1301
- background: "white",
1302
- border: "1px solid #d1d5db",
1303
- borderRadius: 8,
1304
- color: "#374151",
1305
- cursor: escalationLoading || escalationSaving ? "not-allowed" : "pointer",
1306
- fontWeight: 700,
1307
- padding: "10px 14px",
1308
- }, type: "button", children: "Reset" }), _jsx("button", { disabled: escalationLoading || escalationSaving || !escalationDirty, onClick: () => {
1309
- void handleSaveEscalationConfig();
1310
- }, style: {
1311
- background: escalationLoading || escalationSaving || !escalationDirty ? "#9ca3af" : "#111827",
1312
- border: 0,
1313
- borderRadius: 8,
1314
- color: "white",
1315
- cursor: escalationLoading || escalationSaving || !escalationDirty ? "not-allowed" : "pointer",
1316
- fontWeight: 700,
1317
- minWidth: 160,
1318
- padding: "10px 14px",
1319
- }, type: "button", children: escalationSaving ? "Saving..." : "Save escalation" })] })] }), _jsxs("section", { style: {
1320
- border: "1px solid #e5e7eb",
1321
- borderRadius: 8,
1322
- display: "grid",
1323
- gap: 18,
1324
- padding: 18,
1325
- }, children: [_jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Proactive Suggestions" }), _jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Controls the scheduled watch system that sends Telegram suggestions when registered watches match Paperclip activity." })] }), _jsxs("div", { style: twoColumnGridStyle, children: [_jsxs("label", { style: pairedFieldStyle, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Suggestion rate limit" }), _jsx("input", { disabled: proactiveLoading || proactiveSaving, min: 0, onChange: (event) => updateProactiveField("maxSuggestionsPerHourPerCompany", Number(event.currentTarget.value)), placeholder: "10", style: standardInputStyle, type: "number", value: proactiveConfig.maxSuggestionsPerHourPerCompany }), _jsx("span", { style: helperTextStyle, children: "Maximum proactive suggestions sent per company per hour. Set to 0 to suppress watch suggestions without deleting watches." })] }), _jsxs("label", { style: pairedFieldStyle, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Watch deduplication window (ms)" }), _jsx("input", { disabled: proactiveLoading || proactiveSaving, min: 0, onChange: (event) => updateProactiveField("watchDeduplicationWindowMs", Number(event.currentTarget.value)), placeholder: "86400000", style: standardInputStyle, type: "number", value: proactiveConfig.watchDeduplicationWindowMs }), _jsx("span", { style: helperTextStyle, children: "Suppresses repeat suggestions for the same watch/entity pair within this window. Default is 86400000 ms, or 24 hours." })] })] }), _jsxs("div", { style: {
1326
- background: "#f9fafb",
1327
- border: "1px solid #e5e7eb",
1763
+ fontSize: 14,
1764
+ minWidth: 0,
1765
+ padding: "9px 10px"
1766
+ },
1767
+ type: "text",
1768
+ value: routingConfig.boardUsernames
1769
+ }
1770
+ ),
1771
+ /* @__PURE__ */ jsxs("span", { style: { color: "#6b7280", fontSize: 12 }, children: [
1772
+ "Comma- or space-separated handles. A comment forwards only when it contains ",
1773
+ /* @__PURE__ */ jsx("code", { children: "@<handle>" }),
1774
+ " for one of these."
1775
+ ] })
1776
+ ] })
1777
+ ] })
1778
+ ]
1779
+ }
1780
+ ),
1781
+ /* @__PURE__ */ jsx(
1782
+ RoutingRow,
1783
+ {
1784
+ title: "Approvals",
1785
+ chatId: routingConfig.approvalsChatId,
1786
+ topicId: routingConfig.approvalsTopicId,
1787
+ chatPlaceholder: "Approvals chat ID",
1788
+ topicPlaceholder: "Approvals topic ID",
1789
+ disabled: routingLoading || routingSaving,
1790
+ onChatIdChange: (value) => updateRoutingField("approvalsChatId", value),
1791
+ onTopicIdChange: (value) => updateRoutingField("approvalsTopicId", value),
1792
+ chatHelp: "Leave empty to use the default route for approval notifications.",
1793
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
1794
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1795
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1796
+ /* @__PURE__ */ jsx(
1797
+ "input",
1798
+ {
1799
+ checked: routingConfig.notifyOnApprovalCreated,
1800
+ disabled: routingLoading || routingSaving,
1801
+ onChange: (event) => updateRoutingField("notifyOnApprovalCreated", event.currentTarget.checked),
1802
+ type: "checkbox"
1803
+ }
1804
+ ),
1805
+ "Enabled"
1806
+ ] }),
1807
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send Telegram notifications when approval requests are created." })
1808
+ ] }),
1809
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1810
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1811
+ /* @__PURE__ */ jsx(
1812
+ "input",
1813
+ {
1814
+ checked: routingConfig.onlyNotifyBoardApprovals,
1815
+ disabled: routingLoading || routingSaving,
1816
+ onChange: (event) => updateRoutingField("onlyNotifyBoardApprovals", event.currentTarget.checked),
1817
+ type: "checkbox"
1818
+ }
1819
+ ),
1820
+ "Board requests only"
1821
+ ] }),
1822
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Ignore internal approvals and notify only when an agent requests Board approval." })
1823
+ ] })
1824
+ ] })
1825
+ }
1826
+ ),
1827
+ /* @__PURE__ */ jsx(
1828
+ RoutingRow,
1829
+ {
1830
+ title: "Errors",
1831
+ chatId: routingConfig.errorsChatId,
1832
+ topicId: routingConfig.errorsTopicId,
1833
+ chatPlaceholder: "Errors chat ID",
1834
+ topicPlaceholder: "Errors topic ID",
1835
+ disabled: routingLoading || routingSaving,
1836
+ onChatIdChange: (value) => updateRoutingField("errorsChatId", value),
1837
+ onTopicIdChange: (value) => updateRoutingField("errorsTopicId", value),
1838
+ chatHelp: "Leave empty to use the default route for agent error notifications.",
1839
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
1840
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1841
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1842
+ /* @__PURE__ */ jsx(
1843
+ "input",
1844
+ {
1845
+ checked: routingConfig.notifyOnAgentError,
1846
+ disabled: routingLoading || routingSaving,
1847
+ onChange: (event) => updateRoutingField("notifyOnAgentError", event.currentTarget.checked),
1848
+ type: "checkbox"
1849
+ }
1850
+ ),
1851
+ "Errors enabled"
1852
+ ] }),
1853
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Send Telegram notifications when an agent run reports an error." })
1854
+ ] }),
1855
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1856
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1857
+ /* @__PURE__ */ jsx(
1858
+ "input",
1859
+ {
1860
+ checked: routingConfig.notifyOnAgentRunStarted,
1861
+ disabled: routingLoading || routingSaving,
1862
+ onChange: (event) => updateRoutingField("notifyOnAgentRunStarted", event.currentTarget.checked),
1863
+ type: "checkbox"
1864
+ }
1865
+ ),
1866
+ "Run started"
1867
+ ] }),
1868
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Notify on every agent run start. Off by default - high-frequency on busy instances. Routes to a matching Ops route below, otherwise the default chat." })
1869
+ ] }),
1870
+ /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
1871
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
1872
+ /* @__PURE__ */ jsx(
1873
+ "input",
1874
+ {
1875
+ checked: routingConfig.notifyOnAgentRunFinished,
1876
+ disabled: routingLoading || routingSaving,
1877
+ onChange: (event) => updateRoutingField("notifyOnAgentRunFinished", event.currentTarget.checked),
1878
+ type: "checkbox"
1879
+ }
1880
+ ),
1881
+ "Run finished"
1882
+ ] }),
1883
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: "Notify on every agent run completion. Off by default - high-frequency on busy instances. Routes to a matching Ops route below, otherwise the default chat." })
1884
+ ] })
1885
+ ] })
1886
+ }
1887
+ ),
1888
+ /* @__PURE__ */ jsxs(
1889
+ "section",
1890
+ {
1891
+ style: {
1892
+ border: "1px solid #e5e7eb",
1893
+ borderRadius: 8,
1894
+ display: "grid",
1895
+ gap: 10,
1896
+ padding: 12
1897
+ },
1898
+ children: [
1899
+ /* @__PURE__ */ jsxs("div", { style: { alignItems: "center", display: "flex", justifyContent: "space-between" }, children: [
1900
+ /* @__PURE__ */ jsx("strong", { children: "Ops routes" }),
1901
+ /* @__PURE__ */ jsx(
1902
+ "button",
1903
+ {
1904
+ disabled: routingLoading || routingSaving,
1905
+ onClick: () => addOpsRoute(),
1906
+ style: {
1907
+ background: "#111827",
1908
+ border: "none",
1909
+ borderRadius: 8,
1910
+ color: "#fff",
1911
+ cursor: routingLoading || routingSaving ? "not-allowed" : "pointer",
1912
+ fontSize: 13,
1913
+ fontWeight: 600,
1914
+ padding: "6px 12px"
1915
+ },
1916
+ type: "button",
1917
+ children: "Add ops route"
1918
+ }
1919
+ )
1920
+ ] }),
1921
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Divert run-lifecycle (run started / run finished) notifications for a specific company to a dedicated ops chat, keeping the primary chat for important signals. The first enabled route matching by Company ID (or Company name) wins; if none match, ops events fall back to the default chat." }),
1922
+ routingConfig.opsRoutes.length === 0 ? /* @__PURE__ */ jsx("span", { style: { color: "#9ca3af", fontSize: 12, fontStyle: "italic" }, children: "No ops routes configured." }) : /* @__PURE__ */ jsx("div", { style: { display: "grid", gap: 12 }, children: routingConfig.opsRoutes.map((route, index) => /* @__PURE__ */ jsxs(
1923
+ "div",
1924
+ {
1925
+ style: {
1926
+ border: "1px solid #e5e7eb",
1927
+ borderRadius: 8,
1928
+ display: "grid",
1929
+ gap: 8,
1930
+ padding: 10
1931
+ },
1932
+ children: [
1933
+ /* @__PURE__ */ jsxs("div", { style: { alignItems: "center", display: "flex", gap: 12, justifyContent: "space-between" }, children: [
1934
+ /* @__PURE__ */ jsxs("label", { style: { alignItems: "center", color: "#374151", display: "flex", fontSize: 13, gap: 8 }, children: [
1935
+ /* @__PURE__ */ jsx(
1936
+ "input",
1937
+ {
1938
+ checked: route.enabled,
1939
+ disabled: routingLoading || routingSaving,
1940
+ onChange: (event) => updateOpsRoute(index, "enabled", event.currentTarget.checked),
1941
+ type: "checkbox"
1942
+ }
1943
+ ),
1944
+ "Enabled"
1945
+ ] }),
1946
+ /* @__PURE__ */ jsx(
1947
+ "button",
1948
+ {
1949
+ disabled: routingLoading || routingSaving,
1950
+ onClick: () => removeOpsRoute(index),
1951
+ style: {
1952
+ background: "transparent",
1953
+ border: "1px solid #d1d5db",
1954
+ borderRadius: 8,
1955
+ color: "#b91c1c",
1956
+ cursor: routingLoading || routingSaving ? "not-allowed" : "pointer",
1957
+ fontSize: 12,
1958
+ fontWeight: 600,
1959
+ padding: "4px 10px"
1960
+ },
1961
+ type: "button",
1962
+ children: "Remove"
1963
+ }
1964
+ )
1965
+ ] }),
1966
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
1967
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Name (optional)" }),
1968
+ /* @__PURE__ */ jsx(
1969
+ "input",
1970
+ {
1971
+ disabled: routingLoading || routingSaving,
1972
+ onChange: (event) => updateOpsRoute(index, "name", event.currentTarget.value),
1973
+ placeholder: "e.g. Acme Ops",
1974
+ style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" },
1975
+ type: "text",
1976
+ value: route.name
1977
+ }
1978
+ )
1979
+ ] }),
1980
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 8, gridTemplateColumns: "repeat(2, minmax(0, 1fr))" }, children: [
1981
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
1982
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Company ID" }),
1983
+ /* @__PURE__ */ jsx(
1984
+ "input",
1985
+ {
1986
+ disabled: routingLoading || routingSaving,
1987
+ onChange: (event) => updateOpsRoute(index, "companyId", event.currentTarget.value),
1988
+ placeholder: "Company UUID",
1989
+ style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" },
1990
+ type: "text",
1991
+ value: route.companyId
1992
+ }
1993
+ )
1994
+ ] }),
1995
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
1996
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Company name" }),
1997
+ /* @__PURE__ */ jsx(
1998
+ "input",
1999
+ {
2000
+ disabled: routingLoading || routingSaving,
2001
+ onChange: (event) => updateOpsRoute(index, "companyName", event.currentTarget.value),
2002
+ placeholder: "Fallback match by name",
2003
+ style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" },
2004
+ type: "text",
2005
+ value: route.companyName
2006
+ }
2007
+ )
2008
+ ] }),
2009
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
2010
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Chat ID" }),
2011
+ /* @__PURE__ */ jsx(
2012
+ "input",
2013
+ {
2014
+ disabled: routingLoading || routingSaving,
2015
+ onChange: (event) => updateOpsRoute(index, "chatId", event.currentTarget.value),
2016
+ placeholder: "Ops chat ID",
2017
+ style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" },
2018
+ type: "text",
2019
+ value: route.chatId
2020
+ }
2021
+ )
2022
+ ] }),
2023
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
2024
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Topic ID (optional)" }),
2025
+ /* @__PURE__ */ jsx(
2026
+ "input",
2027
+ {
2028
+ disabled: routingLoading || routingSaving,
2029
+ onChange: (event) => updateOpsRoute(index, "topicId", event.currentTarget.value),
2030
+ placeholder: "Forum topic ID",
2031
+ style: { border: "1px solid #d1d5db", borderRadius: 8, fontSize: 14, minWidth: 0, padding: "9px 10px" },
2032
+ type: "text",
2033
+ value: route.topicId
2034
+ }
2035
+ )
2036
+ ] })
2037
+ ] })
2038
+ ]
2039
+ },
2040
+ index
2041
+ )) })
2042
+ ]
2043
+ }
2044
+ ),
2045
+ /* @__PURE__ */ jsx(
2046
+ RoutingRow,
2047
+ {
2048
+ title: "Digests",
2049
+ chatId: routingConfig.digestChatId,
2050
+ topicId: routingConfig.digestTopicId,
2051
+ chatPlaceholder: "Digest chat ID",
2052
+ topicPlaceholder: "Digest topic ID",
2053
+ disabled: routingLoading || routingSaving,
2054
+ onChatIdChange: (value) => updateRoutingField("digestChatId", value),
2055
+ onTopicIdChange: (value) => updateRoutingField("digestTopicId", value),
2056
+ chatHelp: "Leave empty to use the company/default route for digest notifications.",
2057
+ footer: /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 10 }, children: [
2058
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 6 }, children: [
2059
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Mode" }),
2060
+ /* @__PURE__ */ jsxs(
2061
+ "select",
2062
+ {
2063
+ disabled: routingLoading || routingSaving,
2064
+ onChange: (event) => updateRoutingField("digestMode", event.currentTarget.value),
2065
+ style: {
2066
+ border: "1px solid #d1d5db",
2067
+ borderRadius: 8,
2068
+ fontSize: 14,
2069
+ maxWidth: 280,
2070
+ padding: "9px 10px"
2071
+ },
2072
+ value: routingConfig.digestMode,
2073
+ children: [
2074
+ /* @__PURE__ */ jsx("option", { value: "off", children: "Off" }),
2075
+ /* @__PURE__ */ jsx("option", { value: "daily", children: "Daily" }),
2076
+ /* @__PURE__ */ jsx("option", { value: "bidaily", children: "Bidaily" }),
2077
+ /* @__PURE__ */ jsx("option", { value: "tridaily", children: "Tridaily" })
2078
+ ]
2079
+ }
2080
+ ),
2081
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: "Off disables digest notifications. Times are UTC." })
2082
+ ] }),
2083
+ /* @__PURE__ */ jsxs("div", { style: { alignItems: "stretch", display: "grid", gap: 10, gridTemplateColumns: "repeat(3, minmax(0, 1fr))" }, children: [
2084
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5, gridTemplateRows: "auto auto minmax(32px, auto)" }, children: [
2085
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Daily time" }),
2086
+ /* @__PURE__ */ jsx(
2087
+ "input",
2088
+ {
2089
+ disabled: routingLoading || routingSaving,
2090
+ onChange: (event) => updateRoutingField("dailyDigestTime", event.currentTarget.value),
2091
+ placeholder: "09:00",
2092
+ style: {
2093
+ border: "1px solid #d1d5db",
2094
+ borderRadius: 8,
2095
+ fontSize: 14,
2096
+ minWidth: 0,
2097
+ padding: "9px 10px"
2098
+ },
2099
+ type: "text",
2100
+ value: routingConfig.dailyDigestTime
2101
+ }
2102
+ ),
2103
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, lineHeight: "16px" }, children: "Used for daily mode and as the first bidaily slot." })
2104
+ ] }),
2105
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5, gridTemplateRows: "auto auto minmax(32px, auto)" }, children: [
2106
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Bidaily second time" }),
2107
+ /* @__PURE__ */ jsx(
2108
+ "input",
2109
+ {
2110
+ disabled: routingLoading || routingSaving,
2111
+ onChange: (event) => updateRoutingField("bidailySecondTime", event.currentTarget.value),
2112
+ placeholder: "17:00",
2113
+ style: {
2114
+ border: "1px solid #d1d5db",
1328
2115
  borderRadius: 8,
1329
- color: "#4b5563",
1330
- display: "grid",
1331
- fontSize: 13,
1332
- gap: 4,
1333
- padding: 12,
1334
- }, children: [_jsx("strong", { style: { color: "#374151" }, children: "Watch controls" }), _jsx("span", { children: "Individual watches are created by agents through the `register_watch` tool and stored per company. This section controls global rate limiting and duplicate suppression; it does not create or delete watch definitions." })] }), proactiveMessage ? _jsx(NoticeBlock, { notice: proactiveMessage }) : null, _jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [_jsx("button", { disabled: proactiveLoading || proactiveSaving, onClick: () => {
1335
- setProactiveConfig(proactiveSnapshot);
1336
- setProactiveMessage(null);
1337
- }, style: {
1338
- background: "white",
1339
- border: "1px solid #d1d5db",
1340
- borderRadius: 8,
1341
- color: "#374151",
1342
- cursor: proactiveLoading || proactiveSaving ? "not-allowed" : "pointer",
1343
- fontWeight: 700,
1344
- padding: "10px 14px",
1345
- }, type: "button", children: "Reset" }), _jsx("button", { disabled: proactiveLoading || proactiveSaving || !proactiveDirty, onClick: () => {
1346
- void handleSaveProactiveConfig();
1347
- }, style: {
1348
- background: proactiveLoading || proactiveSaving || !proactiveDirty ? "#9ca3af" : "#111827",
1349
- border: 0,
1350
- borderRadius: 8,
1351
- color: "white",
1352
- cursor: proactiveLoading || proactiveSaving || !proactiveDirty ? "not-allowed" : "pointer",
1353
- fontWeight: 700,
1354
- minWidth: 160,
1355
- padding: "10px 14px",
1356
- }, type: "button", children: proactiveSaving ? "Saving..." : "Save suggestions" })] })] })] }));
2116
+ fontSize: 14,
2117
+ minWidth: 0,
2118
+ padding: "9px 10px"
2119
+ },
2120
+ type: "text",
2121
+ value: routingConfig.bidailySecondTime
2122
+ }
2123
+ ),
2124
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, lineHeight: "16px" }, children: "Second send time when bidaily mode is selected." })
2125
+ ] }),
2126
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5, gridTemplateRows: "auto auto minmax(32px, auto)" }, children: [
2127
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Tridaily times" }),
2128
+ /* @__PURE__ */ jsx(
2129
+ "input",
2130
+ {
2131
+ disabled: routingLoading || routingSaving,
2132
+ onChange: (event) => updateRoutingField("tridailyTimes", event.currentTarget.value),
2133
+ placeholder: "07:00,13:00,19:00",
2134
+ style: {
2135
+ border: "1px solid #d1d5db",
2136
+ borderRadius: 8,
2137
+ fontSize: 14,
2138
+ minWidth: 0,
2139
+ padding: "9px 10px"
2140
+ },
2141
+ type: "text",
2142
+ value: routingConfig.tridailyTimes
2143
+ }
2144
+ ),
2145
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, lineHeight: "16px" }, children: "Three comma-separated UTC times for tridaily mode." })
2146
+ ] })
2147
+ ] })
2148
+ ] })
2149
+ }
2150
+ )
2151
+ ] }),
2152
+ routingMessage ? /* @__PURE__ */ jsx(NoticeBlock, { notice: routingMessage }) : null,
2153
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [
2154
+ /* @__PURE__ */ jsx(
2155
+ "button",
2156
+ {
2157
+ disabled: routingLoading || routingSaving,
2158
+ onClick: () => {
2159
+ setRoutingConfig(routingSnapshot);
2160
+ setRoutingMessage(null);
2161
+ },
2162
+ style: {
2163
+ background: "white",
2164
+ border: "1px solid #d1d5db",
2165
+ borderRadius: 8,
2166
+ color: "#374151",
2167
+ cursor: routingLoading || routingSaving ? "not-allowed" : "pointer",
2168
+ fontWeight: 700,
2169
+ padding: "10px 14px"
2170
+ },
2171
+ type: "button",
2172
+ children: "Reset"
2173
+ }
2174
+ ),
2175
+ /* @__PURE__ */ jsx(
2176
+ "button",
2177
+ {
2178
+ disabled: routingLoading || routingSaving || !routingDirty,
2179
+ onClick: () => {
2180
+ void handleSaveRoutingConfig();
2181
+ },
2182
+ style: {
2183
+ background: routingLoading || routingSaving || !routingDirty ? "#9ca3af" : "#111827",
2184
+ border: 0,
2185
+ borderRadius: 8,
2186
+ color: "white",
2187
+ cursor: routingLoading || routingSaving || !routingDirty ? "not-allowed" : "pointer",
2188
+ fontWeight: 700,
2189
+ minWidth: 160,
2190
+ padding: "10px 14px"
2191
+ },
2192
+ type: "button",
2193
+ children: routingSaving ? "Saving..." : "Save routing"
2194
+ }
2195
+ )
2196
+ ] })
2197
+ ]
2198
+ }
2199
+ ),
2200
+ /* @__PURE__ */ jsxs(
2201
+ "section",
2202
+ {
2203
+ style: {
2204
+ border: "1px solid #e5e7eb",
2205
+ borderRadius: 8,
2206
+ display: "grid",
2207
+ gap: 18,
2208
+ padding: 18
2209
+ },
2210
+ children: [
2211
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
2212
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Media Intake / Brief Agent" }),
2213
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Routes Telegram voice, audio, documents, and photos either to a Brief Agent intake flow or to active agent sessions inside forum topics." })
2214
+ ] }),
2215
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 12 }, children: [
2216
+ /* @__PURE__ */ jsx(
2217
+ TextField,
2218
+ {
2219
+ disabled: mediaLoading || mediaSaving,
2220
+ label: "Transcription API key secret ref",
2221
+ onChange: (value) => updateMediaField("transcriptionApiKeyRef", value),
2222
+ placeholder: "OpenAI API key secret UUID",
2223
+ value: mediaConfig.transcriptionApiKeyRef,
2224
+ children: "Secret UUID for the OpenAI API key used to transcribe voice and audio before routing media to the Brief Agent or an active topic agent session."
2225
+ }
2226
+ ),
2227
+ /* @__PURE__ */ jsx(
2228
+ TextField,
2229
+ {
2230
+ disabled: mediaLoading || mediaSaving,
2231
+ label: "Brief Agent ID",
2232
+ onChange: (value) => updateMediaField("briefAgentId", value),
2233
+ placeholder: "Paperclip agent ID",
2234
+ value: mediaConfig.briefAgentId,
2235
+ children: "Agent ID that processes media intake briefs. Leave empty to disable the dedicated Brief Agent intake flow."
2236
+ }
2237
+ ),
2238
+ /* @__PURE__ */ jsx(
2239
+ ArrayField,
2240
+ {
2241
+ disabled: mediaLoading || mediaSaving,
2242
+ emptyValueLabel: "No intake chat IDs configured",
2243
+ label: "Brief Agent intake chat IDs",
2244
+ newItemLabel: "Add intake chat ID",
2245
+ onChange: (value) => updateMediaField("briefAgentChatIds", value),
2246
+ placeholder: "-1003800613668",
2247
+ value: mediaConfig.briefAgentChatIds,
2248
+ children: "Telegram chat IDs where media is routed to the Brief Agent. Media in other chats goes to active agent sessions when a matching forum topic session exists."
2249
+ }
2250
+ )
2251
+ ] }),
2252
+ mediaMessage ? /* @__PURE__ */ jsx(NoticeBlock, { notice: mediaMessage }) : null,
2253
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [
2254
+ /* @__PURE__ */ jsx(
2255
+ "button",
2256
+ {
2257
+ disabled: mediaLoading || mediaSaving,
2258
+ onClick: () => {
2259
+ setMediaConfig(mediaSnapshot);
2260
+ setMediaMessage(null);
2261
+ },
2262
+ style: {
2263
+ background: "white",
2264
+ border: "1px solid #d1d5db",
2265
+ borderRadius: 8,
2266
+ color: "#374151",
2267
+ cursor: mediaLoading || mediaSaving ? "not-allowed" : "pointer",
2268
+ fontWeight: 700,
2269
+ padding: "10px 14px"
2270
+ },
2271
+ type: "button",
2272
+ children: "Reset"
2273
+ }
2274
+ ),
2275
+ /* @__PURE__ */ jsx(
2276
+ "button",
2277
+ {
2278
+ disabled: mediaLoading || mediaSaving || !mediaDirty,
2279
+ onClick: () => {
2280
+ void handleSaveMediaConfig();
2281
+ },
2282
+ style: {
2283
+ background: mediaLoading || mediaSaving || !mediaDirty ? "#9ca3af" : "#111827",
2284
+ border: 0,
2285
+ borderRadius: 8,
2286
+ color: "white",
2287
+ cursor: mediaLoading || mediaSaving || !mediaDirty ? "not-allowed" : "pointer",
2288
+ fontWeight: 700,
2289
+ minWidth: 160,
2290
+ padding: "10px 14px"
2291
+ },
2292
+ type: "button",
2293
+ children: mediaSaving ? "Saving..." : "Save media intake"
2294
+ }
2295
+ )
2296
+ ] })
2297
+ ]
2298
+ }
2299
+ ),
2300
+ /* @__PURE__ */ jsxs(
2301
+ "section",
2302
+ {
2303
+ style: {
2304
+ border: "1px solid #e5e7eb",
2305
+ borderRadius: 8,
2306
+ display: "grid",
2307
+ gap: 18,
2308
+ padding: 18
2309
+ },
2310
+ children: [
2311
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
2312
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Human Escalation" }),
2313
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Controls where human handoff requests go and what the bot tells the original Telegram user while waiting." })
2314
+ ] }),
2315
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 12 }, children: [
2316
+ /* @__PURE__ */ jsx(
2317
+ TextField,
2318
+ {
2319
+ disabled: escalationLoading || escalationSaving,
2320
+ label: "Escalation Chat ID",
2321
+ onChange: (value) => updateEscalationField("escalationChatId", value),
2322
+ placeholder: "-1003800613668",
2323
+ value: escalationConfig.escalationChatId,
2324
+ children: "Telegram chat ID where escalations are sent for human review. Leave empty to log escalations without forwarding them to Telegram."
2325
+ }
2326
+ ),
2327
+ /* @__PURE__ */ jsxs("div", { style: twoColumnGridStyle, children: [
2328
+ /* @__PURE__ */ jsxs("label", { style: pairedFieldStyle, children: [
2329
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Escalation timeout (ms)" }),
2330
+ /* @__PURE__ */ jsx(
2331
+ "input",
2332
+ {
2333
+ disabled: escalationLoading || escalationSaving,
2334
+ min: 0,
2335
+ onChange: (event) => updateEscalationField("escalationTimeoutMs", Number(event.currentTarget.value)),
2336
+ placeholder: "900000",
2337
+ style: standardInputStyle,
2338
+ type: "number",
2339
+ value: escalationConfig.escalationTimeoutMs
2340
+ }
2341
+ ),
2342
+ /* @__PURE__ */ jsx("span", { style: helperTextStyle, children: "How long to wait for a human response. Default is 900000 ms, or 15 minutes." })
2343
+ ] }),
2344
+ /* @__PURE__ */ jsxs("label", { style: pairedFieldStyle, children: [
2345
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Default action on timeout" }),
2346
+ /* @__PURE__ */ jsxs(
2347
+ "select",
2348
+ {
2349
+ disabled: escalationLoading || escalationSaving,
2350
+ onChange: (event) => updateEscalationField("escalationDefaultAction", event.currentTarget.value),
2351
+ style: standardInputStyle,
2352
+ value: escalationConfig.escalationDefaultAction,
2353
+ children: [
2354
+ /* @__PURE__ */ jsx("option", { value: "defer", children: "Defer" }),
2355
+ /* @__PURE__ */ jsx("option", { value: "auto_reply", children: "Auto reply" }),
2356
+ /* @__PURE__ */ jsx("option", { value: "close", children: "Close" })
2357
+ ]
2358
+ }
2359
+ ),
2360
+ /* @__PURE__ */ jsx("span", { style: helperTextStyle, children: "Defer does nothing, auto reply sends the suggested reply, and close ends the escalation path." })
2361
+ ] })
2362
+ ] }),
2363
+ /* @__PURE__ */ jsx(
2364
+ TextAreaField,
2365
+ {
2366
+ disabled: escalationLoading || escalationSaving,
2367
+ label: "Hold message",
2368
+ onChange: (value) => updateEscalationField("escalationHoldMessage", value),
2369
+ placeholder: "Let me check on that - I'll get back to you shortly.",
2370
+ rows: 3,
2371
+ value: escalationConfig.escalationHoldMessage,
2372
+ children: "Message sent to the original Telegram user when their conversation is escalated to a human."
2373
+ }
2374
+ )
2375
+ ] }),
2376
+ escalationMessage ? /* @__PURE__ */ jsx(NoticeBlock, { notice: escalationMessage }) : null,
2377
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [
2378
+ /* @__PURE__ */ jsx(
2379
+ "button",
2380
+ {
2381
+ disabled: escalationLoading || escalationSaving,
2382
+ onClick: () => {
2383
+ setEscalationConfig(escalationSnapshot);
2384
+ setEscalationMessage(null);
2385
+ },
2386
+ style: {
2387
+ background: "white",
2388
+ border: "1px solid #d1d5db",
2389
+ borderRadius: 8,
2390
+ color: "#374151",
2391
+ cursor: escalationLoading || escalationSaving ? "not-allowed" : "pointer",
2392
+ fontWeight: 700,
2393
+ padding: "10px 14px"
2394
+ },
2395
+ type: "button",
2396
+ children: "Reset"
2397
+ }
2398
+ ),
2399
+ /* @__PURE__ */ jsx(
2400
+ "button",
2401
+ {
2402
+ disabled: escalationLoading || escalationSaving || !escalationDirty,
2403
+ onClick: () => {
2404
+ void handleSaveEscalationConfig();
2405
+ },
2406
+ style: {
2407
+ background: escalationLoading || escalationSaving || !escalationDirty ? "#9ca3af" : "#111827",
2408
+ border: 0,
2409
+ borderRadius: 8,
2410
+ color: "white",
2411
+ cursor: escalationLoading || escalationSaving || !escalationDirty ? "not-allowed" : "pointer",
2412
+ fontWeight: 700,
2413
+ minWidth: 160,
2414
+ padding: "10px 14px"
2415
+ },
2416
+ type: "button",
2417
+ children: escalationSaving ? "Saving..." : "Save escalation"
2418
+ }
2419
+ )
2420
+ ] })
2421
+ ]
2422
+ }
2423
+ ),
2424
+ /* @__PURE__ */ jsxs(
2425
+ "section",
2426
+ {
2427
+ style: {
2428
+ border: "1px solid #e5e7eb",
2429
+ borderRadius: 8,
2430
+ display: "grid",
2431
+ gap: 18,
2432
+ padding: 18
2433
+ },
2434
+ children: [
2435
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
2436
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: 18, fontWeight: 700, lineHeight: "28px", margin: 0 }, children: "Proactive Suggestions" }),
2437
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", margin: 0 }, children: "Controls the scheduled watch system that sends Telegram suggestions when registered watches match Paperclip activity." })
2438
+ ] }),
2439
+ /* @__PURE__ */ jsxs("div", { style: twoColumnGridStyle, children: [
2440
+ /* @__PURE__ */ jsxs("label", { style: pairedFieldStyle, children: [
2441
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Suggestion rate limit" }),
2442
+ /* @__PURE__ */ jsx(
2443
+ "input",
2444
+ {
2445
+ disabled: proactiveLoading || proactiveSaving,
2446
+ min: 0,
2447
+ onChange: (event) => updateProactiveField("maxSuggestionsPerHourPerCompany", Number(event.currentTarget.value)),
2448
+ placeholder: "10",
2449
+ style: standardInputStyle,
2450
+ type: "number",
2451
+ value: proactiveConfig.maxSuggestionsPerHourPerCompany
2452
+ }
2453
+ ),
2454
+ /* @__PURE__ */ jsx("span", { style: helperTextStyle, children: "Maximum proactive suggestions sent per company per hour. Set to 0 to suppress watch suggestions without deleting watches." })
2455
+ ] }),
2456
+ /* @__PURE__ */ jsxs("label", { style: pairedFieldStyle, children: [
2457
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Watch deduplication window (ms)" }),
2458
+ /* @__PURE__ */ jsx(
2459
+ "input",
2460
+ {
2461
+ disabled: proactiveLoading || proactiveSaving,
2462
+ min: 0,
2463
+ onChange: (event) => updateProactiveField("watchDeduplicationWindowMs", Number(event.currentTarget.value)),
2464
+ placeholder: "86400000",
2465
+ style: standardInputStyle,
2466
+ type: "number",
2467
+ value: proactiveConfig.watchDeduplicationWindowMs
2468
+ }
2469
+ ),
2470
+ /* @__PURE__ */ jsx("span", { style: helperTextStyle, children: "Suppresses repeat suggestions for the same watch/entity pair within this window. Default is 86400000 ms, or 24 hours." })
2471
+ ] })
2472
+ ] }),
2473
+ /* @__PURE__ */ jsxs(
2474
+ "div",
2475
+ {
2476
+ style: {
2477
+ background: "#f9fafb",
2478
+ border: "1px solid #e5e7eb",
2479
+ borderRadius: 8,
2480
+ color: "#4b5563",
2481
+ display: "grid",
2482
+ fontSize: 13,
2483
+ gap: 4,
2484
+ padding: 12
2485
+ },
2486
+ children: [
2487
+ /* @__PURE__ */ jsx("strong", { style: { color: "#374151" }, children: "Watch controls" }),
2488
+ /* @__PURE__ */ jsx("span", { children: "Individual watches are created by agents through the `register_watch` tool and stored per company. This section controls global rate limiting and duplicate suppression; it does not create or delete watch definitions." })
2489
+ ]
2490
+ }
2491
+ ),
2492
+ proactiveMessage ? /* @__PURE__ */ jsx(NoticeBlock, { notice: proactiveMessage }) : null,
2493
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, justifyContent: "flex-end" }, children: [
2494
+ /* @__PURE__ */ jsx(
2495
+ "button",
2496
+ {
2497
+ disabled: proactiveLoading || proactiveSaving,
2498
+ onClick: () => {
2499
+ setProactiveConfig(proactiveSnapshot);
2500
+ setProactiveMessage(null);
2501
+ },
2502
+ style: {
2503
+ background: "white",
2504
+ border: "1px solid #d1d5db",
2505
+ borderRadius: 8,
2506
+ color: "#374151",
2507
+ cursor: proactiveLoading || proactiveSaving ? "not-allowed" : "pointer",
2508
+ fontWeight: 700,
2509
+ padding: "10px 14px"
2510
+ },
2511
+ type: "button",
2512
+ children: "Reset"
2513
+ }
2514
+ ),
2515
+ /* @__PURE__ */ jsx(
2516
+ "button",
2517
+ {
2518
+ disabled: proactiveLoading || proactiveSaving || !proactiveDirty,
2519
+ onClick: () => {
2520
+ void handleSaveProactiveConfig();
2521
+ },
2522
+ style: {
2523
+ background: proactiveLoading || proactiveSaving || !proactiveDirty ? "#9ca3af" : "#111827",
2524
+ border: 0,
2525
+ borderRadius: 8,
2526
+ color: "white",
2527
+ cursor: proactiveLoading || proactiveSaving || !proactiveDirty ? "not-allowed" : "pointer",
2528
+ fontWeight: 700,
2529
+ minWidth: 160,
2530
+ padding: "10px 14px"
2531
+ },
2532
+ type: "button",
2533
+ children: proactiveSaving ? "Saving..." : "Save suggestions"
2534
+ }
2535
+ )
2536
+ ] })
2537
+ ]
2538
+ }
2539
+ )
2540
+ ] });
1357
2541
  }
1358
2542
  function NoticeBlock({ notice }) {
1359
- return (_jsxs("div", { style: {
1360
- border: `1px solid ${notice.tone === "success" ? "#99f6e4" : "#fecaca"}`,
1361
- borderRadius: 8,
1362
- background: notice.tone === "success" ? "#f0fdfa" : "#fef2f2",
1363
- color: notice.tone === "success" ? "#115e59" : "#991b1b",
1364
- padding: 12,
1365
- }, children: [_jsx("strong", { children: notice.title }), notice.text ? _jsx("p", { style: { margin: "6px 0 0" }, children: notice.text }) : null] }));
2543
+ return /* @__PURE__ */ jsxs(
2544
+ "div",
2545
+ {
2546
+ style: {
2547
+ border: `1px solid ${notice.tone === "success" ? "#99f6e4" : "#fecaca"}`,
2548
+ borderRadius: 8,
2549
+ background: notice.tone === "success" ? "#f0fdfa" : "#fef2f2",
2550
+ color: notice.tone === "success" ? "#115e59" : "#991b1b",
2551
+ padding: 12
2552
+ },
2553
+ children: [
2554
+ /* @__PURE__ */ jsx("strong", { children: notice.title }),
2555
+ notice.text ? /* @__PURE__ */ jsx("p", { style: { margin: "6px 0 0" }, children: notice.text }) : null
2556
+ ]
2557
+ }
2558
+ );
1366
2559
  }
1367
- function TextField({ label, value, placeholder, disabled, children, onChange, }) {
1368
- return (_jsxs("label", { style: { display: "grid", gap: 5 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: label }), _jsx("input", { disabled: disabled, onChange: (event) => onChange(event.currentTarget.value), placeholder: placeholder, style: {
1369
- border: "1px solid #d1d5db",
1370
- borderRadius: 8,
1371
- fontSize: 14,
1372
- minWidth: 0,
1373
- padding: "9px 10px",
1374
- }, type: "text", value: value }), _jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: children })] }));
2560
+ function TextField({
2561
+ label,
2562
+ value,
2563
+ placeholder,
2564
+ disabled,
2565
+ children,
2566
+ onChange,
2567
+ type = "text"
2568
+ }) {
2569
+ return /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5 }, children: [
2570
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: label }),
2571
+ /* @__PURE__ */ jsx(
2572
+ "input",
2573
+ {
2574
+ autoComplete: type === "password" ? "off" : void 0,
2575
+ disabled,
2576
+ onChange: (event) => onChange(event.currentTarget.value),
2577
+ placeholder,
2578
+ style: {
2579
+ border: "1px solid #d1d5db",
2580
+ borderRadius: 8,
2581
+ fontSize: 14,
2582
+ minWidth: 0,
2583
+ padding: "9px 10px"
2584
+ },
2585
+ type,
2586
+ value
2587
+ }
2588
+ ),
2589
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children })
2590
+ ] });
1375
2591
  }
1376
- function TextAreaField({ label, value, placeholder, rows = 3, disabled, children, onChange, }) {
1377
- return (_jsxs("label", { style: { display: "grid", gap: 5 }, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: label }), _jsx("textarea", { disabled: disabled, onChange: (event) => onChange(event.currentTarget.value), placeholder: placeholder, rows: rows, style: {
1378
- border: "1px solid #d1d5db",
1379
- borderRadius: 8,
1380
- fontSize: 14,
1381
- minWidth: 0,
1382
- padding: "9px 10px",
1383
- resize: "vertical",
1384
- }, value: value }), _jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: children })] }));
2592
+ function TextAreaField({
2593
+ label,
2594
+ value,
2595
+ placeholder,
2596
+ rows = 3,
2597
+ disabled,
2598
+ children,
2599
+ onChange
2600
+ }) {
2601
+ return /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 5 }, children: [
2602
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: label }),
2603
+ /* @__PURE__ */ jsx(
2604
+ "textarea",
2605
+ {
2606
+ disabled,
2607
+ onChange: (event) => onChange(event.currentTarget.value),
2608
+ placeholder,
2609
+ rows,
2610
+ style: {
2611
+ border: "1px solid #d1d5db",
2612
+ borderRadius: 8,
2613
+ fontSize: 14,
2614
+ minWidth: 0,
2615
+ padding: "9px 10px",
2616
+ resize: "vertical"
2617
+ },
2618
+ value
2619
+ }
2620
+ ),
2621
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children })
2622
+ ] });
1385
2623
  }
1386
- function ArrayField({ label, value, placeholder, disabled, emptyValueLabel, newItemLabel, children, onChange, }) {
1387
- function updateItem(index, nextValue) {
1388
- const next = [...value];
1389
- next[index] = nextValue;
1390
- onChange(next);
1391
- }
1392
- function removeItem(index) {
1393
- onChange(value.filter((_, itemIndex) => itemIndex !== index));
1394
- }
1395
- function addItem() {
1396
- onChange([...value, ""]);
1397
- }
1398
- return (_jsxs("div", { style: { display: "grid", gap: 7 }, children: [_jsx("div", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: label }), _jsxs("div", { style: { display: "grid", gap: 8 }, children: [value.length === 0 ? (_jsx("div", { style: {
1399
- border: "1px dashed #d1d5db",
1400
- borderRadius: 8,
1401
- color: "#6b7280",
1402
- fontSize: 13,
1403
- padding: "9px 10px",
1404
- }, children: emptyValueLabel })) : null, value.map((item, index) => (_jsxs("div", { style: { alignItems: "center", display: "grid", gap: 8, gridTemplateColumns: "minmax(0, 1fr) auto" }, children: [_jsx("input", { disabled: disabled, onBlur: () => {
1405
- const cleaned = value.map((entry) => entry.trim()).filter(Boolean);
1406
- if (JSON.stringify(cleaned) !== JSON.stringify(value)) {
1407
- onChange(cleaned);
1408
- }
1409
- }, onChange: (event) => updateItem(index, event.currentTarget.value), placeholder: placeholder, style: {
1410
- border: "1px solid #d1d5db",
1411
- borderRadius: 8,
1412
- fontSize: 14,
1413
- minWidth: 0,
1414
- padding: "9px 10px",
1415
- }, type: "text", value: item }), _jsx("button", { disabled: disabled, onClick: () => removeItem(index), style: {
1416
- background: "white",
1417
- border: "1px solid #d1d5db",
1418
- borderRadius: 8,
1419
- color: "#374151",
1420
- cursor: disabled ? "not-allowed" : "pointer",
1421
- fontWeight: 700,
1422
- padding: "9px 12px",
1423
- }, type: "button", children: "Remove" })] }, index)))] }), _jsx("button", { disabled: disabled, onClick: addItem, style: {
1424
- background: "white",
1425
- border: "1px solid #d1d5db",
1426
- borderRadius: 8,
1427
- color: "#374151",
1428
- cursor: disabled ? "not-allowed" : "pointer",
1429
- fontWeight: 700,
1430
- justifySelf: "start",
1431
- padding: "9px 12px",
1432
- }, type: "button", children: newItemLabel }), _jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children: children })] }));
2624
+ function ArrayField({
2625
+ label,
2626
+ value,
2627
+ placeholder,
2628
+ disabled,
2629
+ emptyValueLabel,
2630
+ newItemLabel,
2631
+ children,
2632
+ onChange
2633
+ }) {
2634
+ function updateItem(index, nextValue) {
2635
+ const next = [...value];
2636
+ next[index] = nextValue;
2637
+ onChange(next);
2638
+ }
2639
+ function removeItem(index) {
2640
+ onChange(value.filter((_, itemIndex) => itemIndex !== index));
2641
+ }
2642
+ function addItem() {
2643
+ onChange([...value, ""]);
2644
+ }
2645
+ return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 7 }, children: [
2646
+ /* @__PURE__ */ jsx("div", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: label }),
2647
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 8 }, children: [
2648
+ value.length === 0 ? /* @__PURE__ */ jsx(
2649
+ "div",
2650
+ {
2651
+ style: {
2652
+ border: "1px dashed #d1d5db",
2653
+ borderRadius: 8,
2654
+ color: "#6b7280",
2655
+ fontSize: 13,
2656
+ padding: "9px 10px"
2657
+ },
2658
+ children: emptyValueLabel
2659
+ }
2660
+ ) : null,
2661
+ value.map((item, index) => /* @__PURE__ */ jsxs("div", { style: { alignItems: "center", display: "grid", gap: 8, gridTemplateColumns: "minmax(0, 1fr) auto" }, children: [
2662
+ /* @__PURE__ */ jsx(
2663
+ "input",
2664
+ {
2665
+ disabled,
2666
+ onBlur: () => {
2667
+ const cleaned = value.map((entry) => entry.trim()).filter(Boolean);
2668
+ if (JSON.stringify(cleaned) !== JSON.stringify(value)) {
2669
+ onChange(cleaned);
2670
+ }
2671
+ },
2672
+ onChange: (event) => updateItem(index, event.currentTarget.value),
2673
+ placeholder,
2674
+ style: {
2675
+ border: "1px solid #d1d5db",
2676
+ borderRadius: 8,
2677
+ fontSize: 14,
2678
+ minWidth: 0,
2679
+ padding: "9px 10px"
2680
+ },
2681
+ type: "text",
2682
+ value: item
2683
+ }
2684
+ ),
2685
+ /* @__PURE__ */ jsx(
2686
+ "button",
2687
+ {
2688
+ disabled,
2689
+ onClick: () => removeItem(index),
2690
+ style: {
2691
+ background: "white",
2692
+ border: "1px solid #d1d5db",
2693
+ borderRadius: 8,
2694
+ color: "#374151",
2695
+ cursor: disabled ? "not-allowed" : "pointer",
2696
+ fontWeight: 700,
2697
+ padding: "9px 12px"
2698
+ },
2699
+ type: "button",
2700
+ children: "Remove"
2701
+ }
2702
+ )
2703
+ ] }, index))
2704
+ ] }),
2705
+ /* @__PURE__ */ jsx(
2706
+ "button",
2707
+ {
2708
+ disabled,
2709
+ onClick: addItem,
2710
+ style: {
2711
+ background: "white",
2712
+ border: "1px solid #d1d5db",
2713
+ borderRadius: 8,
2714
+ color: "#374151",
2715
+ cursor: disabled ? "not-allowed" : "pointer",
2716
+ fontWeight: 700,
2717
+ justifySelf: "start",
2718
+ padding: "9px 12px"
2719
+ },
2720
+ type: "button",
2721
+ children: newItemLabel
2722
+ }
2723
+ ),
2724
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12 }, children })
2725
+ ] });
1433
2726
  }
1434
- function CheckboxField({ label, checked, disabled, children, onChange, }) {
1435
- return (_jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [_jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [_jsx("input", { checked: checked, disabled: disabled, onChange: (event) => onChange(event.currentTarget.checked), type: "checkbox" }), label] }), _jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children: children })] }));
2727
+ function CheckboxField({
2728
+ label,
2729
+ checked,
2730
+ disabled,
2731
+ children,
2732
+ onChange
2733
+ }) {
2734
+ return /* @__PURE__ */ jsxs("label", { style: { color: "#374151", display: "grid", gap: 3, fontSize: 13 }, children: [
2735
+ /* @__PURE__ */ jsxs("span", { style: { alignItems: "center", display: "flex", gap: 8 }, children: [
2736
+ /* @__PURE__ */ jsx(
2737
+ "input",
2738
+ {
2739
+ checked,
2740
+ disabled,
2741
+ onChange: (event) => onChange(event.currentTarget.checked),
2742
+ type: "checkbox"
2743
+ }
2744
+ ),
2745
+ label
2746
+ ] }),
2747
+ /* @__PURE__ */ jsx("span", { style: { color: "#6b7280", fontSize: 12, marginLeft: 22 }, children })
2748
+ ] });
1436
2749
  }
1437
- function RoutingRow({ title, chatId, topicId, chatPlaceholder, topicPlaceholder, chatHelp, disabled, children, footer, onChatIdChange, onTopicIdChange, }) {
1438
- return (_jsxs("div", { style: {
1439
- border: "1px solid #e5e7eb",
1440
- borderRadius: 8,
1441
- display: "grid",
1442
- gap: 10,
1443
- padding: 12,
1444
- }, children: [_jsxs("div", { style: { alignItems: "center", display: "flex", gap: 12, justifyContent: "space-between" }, children: [_jsx("strong", { children: title }), children ? _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 12 }, children: children }) : null] }), _jsx("div", { style: { display: "grid", gap: 10 }, children: _jsxs("div", { style: twoColumnGridStyle, children: [_jsxs("label", { style: pairedFieldStyle, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Chat ID" }), _jsx("input", { disabled: disabled, onChange: (event) => onChatIdChange(event.currentTarget.value), placeholder: chatPlaceholder, style: standardInputStyle, type: "text", value: chatId }), _jsx("span", { style: helperTextStyle, children: chatHelp })] }), _jsxs("label", { style: pairedFieldStyle, children: [_jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Topic ID" }), _jsx("input", { disabled: disabled, onChange: (event) => onTopicIdChange(event.currentTarget.value), placeholder: topicPlaceholder, style: standardInputStyle, type: "text", value: topicId }), _jsx("span", { style: helperTextStyle, children: "Optional. Used only when the Chat ID points to a Telegram forum group." })] })] }) }), footer ? _jsx("div", { style: { display: "grid", gap: 10 }, children: footer }) : null] }));
2750
+ function RoutingRow({
2751
+ title,
2752
+ chatId,
2753
+ topicId,
2754
+ chatPlaceholder,
2755
+ topicPlaceholder,
2756
+ chatHelp,
2757
+ disabled,
2758
+ children,
2759
+ footer,
2760
+ onChatIdChange,
2761
+ onTopicIdChange
2762
+ }) {
2763
+ return /* @__PURE__ */ jsxs(
2764
+ "div",
2765
+ {
2766
+ style: {
2767
+ border: "1px solid #e5e7eb",
2768
+ borderRadius: 8,
2769
+ display: "grid",
2770
+ gap: 10,
2771
+ padding: 12
2772
+ },
2773
+ children: [
2774
+ /* @__PURE__ */ jsxs("div", { style: { alignItems: "center", display: "flex", gap: 12, justifyContent: "space-between" }, children: [
2775
+ /* @__PURE__ */ jsx("strong", { children: title }),
2776
+ children ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 12 }, children }) : null
2777
+ ] }),
2778
+ /* @__PURE__ */ jsx("div", { style: { display: "grid", gap: 10 }, children: /* @__PURE__ */ jsxs("div", { style: twoColumnGridStyle, children: [
2779
+ /* @__PURE__ */ jsxs("label", { style: pairedFieldStyle, children: [
2780
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Chat ID" }),
2781
+ /* @__PURE__ */ jsx(
2782
+ "input",
2783
+ {
2784
+ disabled,
2785
+ onChange: (event) => onChatIdChange(event.currentTarget.value),
2786
+ placeholder: chatPlaceholder,
2787
+ style: standardInputStyle,
2788
+ type: "text",
2789
+ value: chatId
2790
+ }
2791
+ ),
2792
+ /* @__PURE__ */ jsx("span", { style: helperTextStyle, children: chatHelp })
2793
+ ] }),
2794
+ /* @__PURE__ */ jsxs("label", { style: pairedFieldStyle, children: [
2795
+ /* @__PURE__ */ jsx("span", { style: { color: "#4b5563", fontSize: 12, fontWeight: 700 }, children: "Topic ID" }),
2796
+ /* @__PURE__ */ jsx(
2797
+ "input",
2798
+ {
2799
+ disabled,
2800
+ onChange: (event) => onTopicIdChange(event.currentTarget.value),
2801
+ placeholder: topicPlaceholder,
2802
+ style: standardInputStyle,
2803
+ type: "text",
2804
+ value: topicId
2805
+ }
2806
+ ),
2807
+ /* @__PURE__ */ jsx("span", { style: helperTextStyle, children: "Optional. Used only when the Chat ID points to a Telegram forum group." })
2808
+ ] })
2809
+ ] }) }),
2810
+ footer ? /* @__PURE__ */ jsx("div", { style: { display: "grid", gap: 10 }, children: footer }) : null
2811
+ ]
2812
+ }
2813
+ );
1445
2814
  }
1446
- //# sourceMappingURL=index.js.map
2815
+ export {
2816
+ TelegramSettingsPage
2817
+ };
2818
+ //# sourceMappingURL=index.js.map