@odience-network/paperclip-plugin-telegram-enhanced 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/acp-bridge.d.ts +35 -0
- package/dist/acp-bridge.js +891 -0
- package/dist/acp-bridge.js.map +1 -0
- package/dist/adapter.d.ts +35 -0
- package/dist/adapter.js +75 -0
- package/dist/adapter.js.map +1 -0
- package/dist/agent-labels.d.ts +12 -0
- package/dist/agent-labels.js +96 -0
- package/dist/agent-labels.js.map +1 -0
- package/dist/allowlist.d.ts +27 -0
- package/dist/allowlist.js +34 -0
- package/dist/allowlist.js.map +1 -0
- package/dist/approval-routing.d.ts +2 -0
- package/dist/approval-routing.js +7 -0
- package/dist/approval-routing.js.map +1 -0
- package/dist/command-registry.d.ts +3 -0
- package/dist/command-registry.js +268 -0
- package/dist/command-registry.js.map +1 -0
- package/dist/commands.d.ts +11 -0
- package/dist/commands.js +516 -0
- package/dist/commands.js.map +1 -0
- package/dist/constants.d.ts +76 -0
- package/dist/constants.js +71 -0
- package/dist/constants.js.map +1 -0
- package/dist/escalation.d.ts +42 -0
- package/dist/escalation.js +252 -0
- package/dist/escalation.js.map +1 -0
- package/dist/file-routing.d.ts +51 -0
- package/dist/file-routing.js +212 -0
- package/dist/file-routing.js.map +1 -0
- package/dist/formatters.d.ts +31 -0
- package/dist/formatters.js +336 -0
- package/dist/formatters.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/interaction-delivery.d.ts +90 -0
- package/dist/interaction-delivery.js +142 -0
- package/dist/interaction-delivery.js.map +1 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +111 -0
- package/dist/manifest.js.map +1 -0
- package/dist/media-pipeline.d.ts +47 -0
- package/dist/media-pipeline.js +162 -0
- package/dist/media-pipeline.js.map +1 -0
- package/dist/notification-filters.d.ts +23 -0
- package/dist/notification-filters.js +93 -0
- package/dist/notification-filters.js.map +1 -0
- package/dist/paperclip-api.d.ts +25 -0
- package/dist/paperclip-api.js +69 -0
- package/dist/paperclip-api.js.map +1 -0
- package/dist/polling-offset.d.ts +22 -0
- package/dist/polling-offset.js +68 -0
- package/dist/polling-offset.js.map +1 -0
- package/dist/secret-ref-validation.d.ts +7 -0
- package/dist/secret-ref-validation.js +49 -0
- package/dist/secret-ref-validation.js.map +1 -0
- package/dist/telegram-api.d.ts +40 -0
- package/dist/telegram-api.js +251 -0
- package/dist/telegram-api.js.map +1 -0
- package/dist/topic-projects.d.ts +2 -0
- package/dist/topic-projects.js +45 -0
- package/dist/topic-projects.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1446 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/watch-registry.d.ts +9 -0
- package/dist/watch-registry.js +272 -0
- package/dist/watch-registry.js.map +1 -0
- package/dist/worker.d.ts +162 -0
- package/dist/worker.js +1520 -0
- package/dist/worker.js.map +1 -0
- package/package.json +59 -0
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
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: [],
|
|
33
|
+
};
|
|
34
|
+
const DEFAULT_CONNECTION_CONFIG = {
|
|
35
|
+
telegramBotTokenRef: "",
|
|
36
|
+
paperclipBaseUrl: "http://localhost:3100",
|
|
37
|
+
paperclipPublicUrl: "",
|
|
38
|
+
};
|
|
39
|
+
const DEFAULT_BOARD_CONFIG = {
|
|
40
|
+
paperclipBoardApiTokenRef: "",
|
|
41
|
+
};
|
|
42
|
+
const DEFAULT_ACCESS_CONFIG = {
|
|
43
|
+
enableCommands: true,
|
|
44
|
+
enableInbound: true,
|
|
45
|
+
allowedTelegramUserIds: [],
|
|
46
|
+
allowedTelegramChatIds: [],
|
|
47
|
+
};
|
|
48
|
+
const DEFAULT_MEDIA_CONFIG = {
|
|
49
|
+
transcriptionApiKeyRef: "",
|
|
50
|
+
briefAgentId: "",
|
|
51
|
+
briefAgentChatIds: [],
|
|
52
|
+
};
|
|
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.",
|
|
58
|
+
};
|
|
59
|
+
const DEFAULT_PROACTIVE_CONFIG = {
|
|
60
|
+
maxSuggestionsPerHourPerCompany: 10,
|
|
61
|
+
watchDeduplicationWindowMs: 86400000,
|
|
62
|
+
};
|
|
63
|
+
const standardInputStyle = {
|
|
64
|
+
border: "1px solid #d1d5db",
|
|
65
|
+
borderRadius: 8,
|
|
66
|
+
fontSize: 14,
|
|
67
|
+
minWidth: 0,
|
|
68
|
+
padding: "9px 10px",
|
|
69
|
+
};
|
|
70
|
+
const helperTextStyle = {
|
|
71
|
+
color: "#6b7280",
|
|
72
|
+
fontSize: 12,
|
|
73
|
+
lineHeight: "16px",
|
|
74
|
+
};
|
|
75
|
+
const twoColumnGridStyle = {
|
|
76
|
+
alignItems: "stretch",
|
|
77
|
+
display: "grid",
|
|
78
|
+
gap: 10,
|
|
79
|
+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
80
|
+
};
|
|
81
|
+
const pairedFieldStyle = {
|
|
82
|
+
display: "grid",
|
|
83
|
+
gap: 5,
|
|
84
|
+
gridTemplateRows: "auto auto minmax(32px, auto)",
|
|
85
|
+
};
|
|
86
|
+
function getErrorMessage(error) {
|
|
87
|
+
return error instanceof Error ? error.message : String(error);
|
|
88
|
+
}
|
|
89
|
+
function asString(value) {
|
|
90
|
+
return typeof value === "string" ? value : "";
|
|
91
|
+
}
|
|
92
|
+
function asBoolean(value, fallback) {
|
|
93
|
+
return typeof value === "boolean" ? value : fallback;
|
|
94
|
+
}
|
|
95
|
+
function asNumber(value, fallback) {
|
|
96
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
97
|
+
}
|
|
98
|
+
function asStringArray(value) {
|
|
99
|
+
return Array.isArray(value)
|
|
100
|
+
? value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
|
|
101
|
+
: [];
|
|
102
|
+
}
|
|
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
|
+
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 : "";
|
|
110
|
+
}
|
|
111
|
+
function asDigestMode(value) {
|
|
112
|
+
return value === "daily" || value === "bidaily" || value === "tridaily" ? value : "off";
|
|
113
|
+
}
|
|
114
|
+
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
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
// Returns a human-readable error if the ops routes are invalid, else null.
|
|
129
|
+
// Expects already-trimmed routes.
|
|
130
|
+
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
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function asEscalationDefaultAction(value) {
|
|
153
|
+
return value === "auto_reply" || value === "close" ? value : "defer";
|
|
154
|
+
}
|
|
155
|
+
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
|
+
};
|
|
184
|
+
}
|
|
185
|
+
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
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function extractBoardConfig(config) {
|
|
193
|
+
return {
|
|
194
|
+
paperclipBoardApiTokenRef: asString(config.paperclipBoardApiTokenRef),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
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
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function extractMediaConfig(config) {
|
|
206
|
+
return {
|
|
207
|
+
transcriptionApiKeyRef: asString(config.transcriptionApiKeyRef),
|
|
208
|
+
briefAgentId: asString(config.briefAgentId),
|
|
209
|
+
briefAgentChatIds: asStringArray(config.briefAgentChatIds),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
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
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function extractProactiveConfig(config) {
|
|
221
|
+
return {
|
|
222
|
+
maxSuggestionsPerHourPerCompany: asNumber(config.maxSuggestionsPerHourPerCompany, DEFAULT_PROACTIVE_CONFIG.maxSuggestionsPerHourPerCompany),
|
|
223
|
+
watchDeduplicationWindowMs: asNumber(config.watchDeduplicationWindowMs, DEFAULT_PROACTIVE_CONFIG.watchDeduplicationWindowMs),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
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);
|
|
262
|
+
}
|
|
263
|
+
return payload;
|
|
264
|
+
}
|
|
265
|
+
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;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
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
|
+
}
|
|
296
|
+
}
|
|
297
|
+
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());
|
|
305
|
+
}
|
|
306
|
+
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);
|
|
318
|
+
}
|
|
319
|
+
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)));
|
|
324
|
+
}
|
|
325
|
+
function waitForDuration(durationMs) {
|
|
326
|
+
return new Promise((resolve) => {
|
|
327
|
+
globalThis.setTimeout(resolve, durationMs);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
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
|
+
});
|
|
340
|
+
}
|
|
341
|
+
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.");
|
|
346
|
+
}
|
|
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);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
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
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
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);
|
|
402
|
+
}
|
|
403
|
+
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 : {};
|
|
406
|
+
}
|
|
407
|
+
async function savePluginConfig(configJson) {
|
|
408
|
+
await fetchHostJson(`/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`, {
|
|
409
|
+
method: "POST",
|
|
410
|
+
body: JSON.stringify({ configJson }),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
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`, {
|
|
423
|
+
method: "POST",
|
|
424
|
+
body: JSON.stringify({ name, value }),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
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
|
+
}
|
|
505
|
+
}
|
|
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
|
+
}
|
|
538
|
+
}
|
|
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
|
+
}
|
|
604
|
+
}
|
|
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
|
+
}
|
|
637
|
+
}
|
|
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
|
+
}
|
|
670
|
+
}
|
|
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
|
+
}
|
|
703
|
+
}
|
|
704
|
+
void loadBoardConfig();
|
|
705
|
+
return () => {
|
|
706
|
+
cancelled = true;
|
|
707
|
+
};
|
|
708
|
+
}, []);
|
|
709
|
+
function updateRoutingField(key, value) {
|
|
710
|
+
setRoutingConfig((current) => ({ ...current, [key]: value }));
|
|
711
|
+
setRoutingMessage(null);
|
|
712
|
+
}
|
|
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);
|
|
722
|
+
}
|
|
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);
|
|
729
|
+
}
|
|
730
|
+
function removeOpsRoute(index) {
|
|
731
|
+
setRoutingConfig((current) => ({
|
|
732
|
+
...current,
|
|
733
|
+
opsRoutes: current.opsRoutes.filter((_, i) => i !== index),
|
|
734
|
+
}));
|
|
735
|
+
setRoutingMessage(null);
|
|
736
|
+
}
|
|
737
|
+
function updateBoardField(key, value) {
|
|
738
|
+
setBoardConfig((current) => ({ ...current, [key]: value }));
|
|
739
|
+
setBoardConfigMessage(null);
|
|
740
|
+
}
|
|
741
|
+
function updateAccessField(key, value) {
|
|
742
|
+
setAccessConfig((current) => ({ ...current, [key]: value }));
|
|
743
|
+
setAccessMessage(null);
|
|
744
|
+
}
|
|
745
|
+
function updateConnectionField(key, value) {
|
|
746
|
+
setConnectionConfig((current) => ({ ...current, [key]: value }));
|
|
747
|
+
setConnectionMessage(null);
|
|
748
|
+
}
|
|
749
|
+
function updateMediaField(key, value) {
|
|
750
|
+
setMediaConfig((current) => ({ ...current, [key]: value }));
|
|
751
|
+
setMediaMessage(null);
|
|
752
|
+
}
|
|
753
|
+
function updateEscalationField(key, value) {
|
|
754
|
+
setEscalationConfig((current) => ({ ...current, [key]: value }));
|
|
755
|
+
setEscalationMessage(null);
|
|
756
|
+
}
|
|
757
|
+
function updateProactiveField(key, value) {
|
|
758
|
+
setProactiveConfig((current) => ({ ...current, [key]: value }));
|
|
759
|
+
setProactiveMessage(null);
|
|
760
|
+
}
|
|
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
|
+
}
|
|
785
|
+
}
|
|
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
|
+
}
|
|
810
|
+
}
|
|
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
|
+
}
|
|
854
|
+
}
|
|
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
|
+
}
|
|
879
|
+
}
|
|
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
|
+
}
|
|
904
|
+
}
|
|
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
|
+
}
|
|
929
|
+
}
|
|
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
|
+
}
|
|
954
|
+
}
|
|
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);
|
|
981
|
+
}
|
|
982
|
+
if (!approvalWindow) {
|
|
983
|
+
throw new Error("Allow pop-ups for Paperclip, then try connecting board access again.");
|
|
984
|
+
}
|
|
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();
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
// Ignore browser close restrictions.
|
|
1015
|
+
}
|
|
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",
|
|
1026
|
+
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",
|
|
1054
|
+
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",
|
|
1070
|
+
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",
|
|
1328
|
+
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" })] })] })] }));
|
|
1357
|
+
}
|
|
1358
|
+
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] }));
|
|
1366
|
+
}
|
|
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 })] }));
|
|
1375
|
+
}
|
|
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 })] }));
|
|
1385
|
+
}
|
|
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 })] }));
|
|
1433
|
+
}
|
|
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 })] }));
|
|
1436
|
+
}
|
|
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] }));
|
|
1445
|
+
}
|
|
1446
|
+
//# sourceMappingURL=index.js.map
|