@odience-network/paperclip-plugin-telegram-enhanced 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-bridge.d.ts +1 -1
- package/dist/acp-bridge.js +4 -1
- package/dist/acp-bridge.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/worker.js +61 -27
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/index.tsx", "../../src/constants.ts"],
|
|
4
|
-
"sourcesContent": ["import { useEffect, useState } from \"react\";\nimport {\n usePluginAction,\n usePluginData,\n type PluginSettingsPageProps,\n} from \"@paperclipai/plugin-sdk/ui\";\nimport { PLUGIN_ID } from \"../constants.js\";\n\ntype BoardAccessRegistration = {\n configured: boolean;\n paperclipBoardApiTokenRef: string | null;\n identity: string | null;\n companyId: string | null;\n updatedAt: string | null;\n};\n\ntype CliAuthChallengeResponse = {\n token?: string;\n boardApiToken?: string;\n approvalUrl?: string;\n approvalPath?: string;\n pollUrl?: string;\n pollPath?: string;\n expiresAt?: string;\n suggestedPollIntervalMs?: number;\n};\n\ntype CliAuthChallengePollResponse = {\n status?: string;\n boardApiToken?: string;\n};\n\ntype CliAuthIdentityResponse = {\n user?: {\n displayName?: string | null;\n name?: string | null;\n login?: string | null;\n email?: string | null;\n } | null;\n displayName?: string | null;\n name?: string | null;\n login?: string | null;\n email?: string | null;\n};\n\ntype Notice = {\n tone: \"success\" | \"error\";\n title: string;\n text?: string;\n};\n\ntype TelegramRoutingConfig = {\n defaultChatId: string;\n topicRouting: boolean;\n maxAgentsPerThread: number;\n notifyOnIssueCreated: boolean;\n notifyOnIssueDone: boolean;\n notifyOnIssueAssigned: boolean;\n onlyNotifyIfAssignedTo: string;\n notifyOnIssueBlocked: boolean;\n notifyOnBoardMention: boolean;\n boardUsernames: string;\n approvalsChatId: string;\n approvalsTopicId: string;\n notifyOnApprovalCreated: boolean;\n onlyNotifyBoardApprovals: boolean;\n errorsChatId: string;\n errorsTopicId: string;\n notifyOnAgentError: boolean;\n notifyOnAgentRunStarted: boolean;\n notifyOnAgentRunFinished: boolean;\n digestChatId: string;\n digestTopicId: string;\n digestMode: \"off\" | \"daily\" | \"bidaily\" | \"tridaily\";\n dailyDigestTime: string;\n bidailySecondTime: string;\n tridailyTimes: string;\n opsRoutes: TelegramOpsRouteForm[];\n};\n\n// TEL-23: per-company ops route. Run-lifecycle notifications for a matching\n// company are diverted to this chat/topic instead of the primary chat.\ntype TelegramOpsRouteForm = {\n name: string;\n enabled: boolean;\n companyId: string;\n companyName: string;\n chatId: string;\n topicId: string;\n};\n\ntype TelegramConnectionConfig = {\n telegramBotTokenRef: string;\n paperclipBaseUrl: string;\n paperclipPublicUrl: string;\n};\n\n// ODIAA-720: masked status of the instance-wide bot connection. The raw token\n// is never returned to the frontend \u2014 only whether a bot is connected and, for\n// the instance-state source, the bot identity reported by Telegram getMe.\ntype BotConnectionRegistration = {\n configured: boolean;\n source: \"instance-state\" | \"config-secret-ref\" | null;\n botUsername: string | null;\n botId: string | null;\n updatedAt: string | null;\n};\n\ntype TelegramBoardConfig = {\n paperclipBoardApiTokenRef: string;\n};\n\ntype TelegramAccessConfig = {\n enableCommands: boolean;\n enableInbound: boolean;\n allowedTelegramUserIds: string[];\n allowedTelegramChatIds: string[];\n};\n\ntype TelegramMediaConfig = {\n transcriptionApiKeyRef: string;\n briefAgentId: string;\n briefAgentChatIds: string[];\n};\n\ntype TelegramEscalationConfig = {\n escalationChatId: string;\n escalationTimeoutMs: number;\n escalationDefaultAction: \"defer\" | \"auto_reply\" | \"close\";\n escalationHoldMessage: string;\n};\n\ntype TelegramProactiveConfig = {\n maxSuggestionsPerHourPerCompany: number;\n watchDeduplicationWindowMs: number;\n};\n\ntype PluginConfigResponse = {\n configJson?: Record<string, unknown> | null;\n} | null;\n\nconst TELEGRAM_PLUGIN_ID = PLUGIN_ID;\n\nconst DEFAULT_ROUTING_CONFIG: TelegramRoutingConfig = {\n defaultChatId: \"\",\n topicRouting: false,\n maxAgentsPerThread: 5,\n notifyOnIssueCreated: true,\n notifyOnIssueDone: true,\n notifyOnIssueAssigned: false,\n onlyNotifyIfAssignedTo: \"\",\n notifyOnIssueBlocked: false,\n notifyOnBoardMention: false,\n boardUsernames: \"\",\n approvalsChatId: \"\",\n approvalsTopicId: \"\",\n notifyOnApprovalCreated: true,\n onlyNotifyBoardApprovals: false,\n errorsChatId: \"\",\n errorsTopicId: \"\",\n notifyOnAgentError: true,\n notifyOnAgentRunStarted: false,\n notifyOnAgentRunFinished: false,\n digestChatId: \"\",\n digestTopicId: \"\",\n digestMode: \"off\",\n dailyDigestTime: \"09:00\",\n bidailySecondTime: \"17:00\",\n tridailyTimes: \"07:00,13:00,19:00\",\n opsRoutes: [],\n};\n\nconst DEFAULT_CONNECTION_CONFIG: TelegramConnectionConfig = {\n telegramBotTokenRef: \"\",\n paperclipBaseUrl: \"http://localhost:3100\",\n paperclipPublicUrl: \"\",\n};\n\nconst DEFAULT_BOARD_CONFIG: TelegramBoardConfig = {\n paperclipBoardApiTokenRef: \"\",\n};\n\nconst DEFAULT_ACCESS_CONFIG: TelegramAccessConfig = {\n enableCommands: true,\n enableInbound: true,\n allowedTelegramUserIds: [],\n allowedTelegramChatIds: [],\n};\n\nconst DEFAULT_MEDIA_CONFIG: TelegramMediaConfig = {\n transcriptionApiKeyRef: \"\",\n briefAgentId: \"\",\n briefAgentChatIds: [],\n};\n\nconst DEFAULT_ESCALATION_CONFIG: TelegramEscalationConfig = {\n escalationChatId: \"\",\n escalationTimeoutMs: 900000,\n escalationDefaultAction: \"defer\",\n escalationHoldMessage: \"Let me check on that - I'll get back to you shortly.\",\n};\n\nconst DEFAULT_PROACTIVE_CONFIG: TelegramProactiveConfig = {\n maxSuggestionsPerHourPerCompany: 10,\n watchDeduplicationWindowMs: 86400000,\n};\n\nconst standardInputStyle = {\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n};\n\nconst helperTextStyle = {\n color: \"#6b7280\",\n fontSize: 12,\n lineHeight: \"16px\",\n};\n\nconst twoColumnGridStyle = {\n alignItems: \"stretch\",\n display: \"grid\",\n gap: 10,\n gridTemplateColumns: \"repeat(2, minmax(0, 1fr))\",\n};\n\nconst pairedFieldStyle = {\n display: \"grid\",\n gap: 5,\n gridTemplateRows: \"auto auto minmax(32px, auto)\",\n};\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction asString(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\nfunction asBoolean(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nfunction asNumber(value: unknown, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nfunction asStringArray(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((item): item is string => typeof item === \"string\").map((item) => item.trim()).filter(Boolean)\n : [];\n}\n\n// board usernames may be persisted as an array (worker-side) or a raw string\n// (this text field). Always render a comma-separated string for the input.\nfunction asBoardUsernamesString(value: unknown): string {\n if (Array.isArray(value)) {\n return value.filter((item): item is string => typeof item === \"string\").map((item) => item.trim()).filter(Boolean).join(\", \");\n }\n return typeof value === \"string\" ? value : \"\";\n}\n\nfunction asDigestMode(value: unknown): TelegramRoutingConfig[\"digestMode\"] {\n return value === \"daily\" || value === \"bidaily\" || value === \"tridaily\" ? value : \"off\";\n}\n\nfunction asOpsRoutes(value: unknown): TelegramOpsRouteForm[] {\n if (!Array.isArray(value)) return [];\n return value\n .filter((item): item is Record<string, unknown> =>\n typeof item === \"object\" && item !== null && !Array.isArray(item),\n )\n .map((item) => ({\n name: asString(item.name),\n enabled: asBoolean(item.enabled, true),\n companyId: asString(item.companyId),\n companyName: asString(item.companyName),\n chatId: asString(item.chatId),\n topicId: asString(item.topicId),\n }));\n}\n\n// Returns a human-readable error if the ops routes are invalid, else null.\n// Expects already-trimmed routes.\nfunction validateOpsRoutes(routes: TelegramOpsRouteForm[]): string | null {\n const seenCompanyIds = new Set<string>();\n for (const route of routes) {\n const label = route.name || route.companyName || route.companyId || \"(unnamed)\";\n if (!route.chatId) {\n return `Ops route \"${label}\" needs a Chat ID.`;\n }\n if (!route.companyId && !route.companyName) {\n return `Ops route \"${label}\" needs a Company ID or Company name to match.`;\n }\n if (route.topicId && !/^\\d+$/.test(route.topicId)) {\n return `Ops route \"${label}\" topic ID must be a numeric Telegram forum topic ID.`;\n }\n if (route.companyId) {\n if (seenCompanyIds.has(route.companyId)) {\n return `Duplicate ops route for Company ID \"${route.companyId}\".`;\n }\n seenCompanyIds.add(route.companyId);\n }\n }\n return null;\n}\n\nfunction asEscalationDefaultAction(value: unknown): TelegramEscalationConfig[\"escalationDefaultAction\"] {\n return value === \"auto_reply\" || value === \"close\" ? value : \"defer\";\n}\n\nfunction extractRoutingConfig(config: Record<string, unknown>): TelegramRoutingConfig {\n return {\n defaultChatId: asString(config.defaultChatId),\n topicRouting: asBoolean(config.topicRouting, DEFAULT_ROUTING_CONFIG.topicRouting),\n maxAgentsPerThread: asNumber(config.maxAgentsPerThread, DEFAULT_ROUTING_CONFIG.maxAgentsPerThread),\n notifyOnIssueCreated: asBoolean(\n config.notifyOnIssueCreated,\n DEFAULT_ROUTING_CONFIG.notifyOnIssueCreated,\n ),\n notifyOnIssueDone: asBoolean(\n config.notifyOnIssueDone,\n DEFAULT_ROUTING_CONFIG.notifyOnIssueDone,\n ),\n notifyOnIssueAssigned: asBoolean(\n config.notifyOnIssueAssigned,\n DEFAULT_ROUTING_CONFIG.notifyOnIssueAssigned,\n ),\n onlyNotifyIfAssignedTo: asString(config.onlyNotifyIfAssignedTo),\n notifyOnIssueBlocked: asBoolean(\n config.notifyOnIssueBlocked,\n DEFAULT_ROUTING_CONFIG.notifyOnIssueBlocked,\n ),\n notifyOnBoardMention: asBoolean(\n config.notifyOnBoardMention,\n DEFAULT_ROUTING_CONFIG.notifyOnBoardMention,\n ),\n boardUsernames: asBoardUsernamesString(config.boardUsernames),\n approvalsChatId: asString(config.approvalsChatId),\n approvalsTopicId: asString(config.approvalsTopicId),\n notifyOnApprovalCreated: asBoolean(\n config.notifyOnApprovalCreated,\n DEFAULT_ROUTING_CONFIG.notifyOnApprovalCreated,\n ),\n onlyNotifyBoardApprovals: asBoolean(\n config.onlyNotifyBoardApprovals,\n DEFAULT_ROUTING_CONFIG.onlyNotifyBoardApprovals,\n ),\n errorsChatId: asString(config.errorsChatId),\n errorsTopicId: asString(config.errorsTopicId),\n notifyOnAgentError: asBoolean(\n config.notifyOnAgentError,\n DEFAULT_ROUTING_CONFIG.notifyOnAgentError,\n ),\n notifyOnAgentRunStarted: asBoolean(\n config.notifyOnAgentRunStarted,\n DEFAULT_ROUTING_CONFIG.notifyOnAgentRunStarted,\n ),\n notifyOnAgentRunFinished: asBoolean(\n config.notifyOnAgentRunFinished,\n DEFAULT_ROUTING_CONFIG.notifyOnAgentRunFinished,\n ),\n digestChatId: asString(config.digestChatId),\n digestTopicId: asString(config.digestTopicId),\n digestMode: asDigestMode(config.digestMode),\n dailyDigestTime: asString(config.dailyDigestTime) || DEFAULT_ROUTING_CONFIG.dailyDigestTime,\n bidailySecondTime: asString(config.bidailySecondTime) || DEFAULT_ROUTING_CONFIG.bidailySecondTime,\n tridailyTimes: asString(config.tridailyTimes) || DEFAULT_ROUTING_CONFIG.tridailyTimes,\n opsRoutes: asOpsRoutes(config.opsRoutes),\n };\n}\n\nfunction extractConnectionConfig(config: Record<string, unknown>): TelegramConnectionConfig {\n return {\n telegramBotTokenRef: asString(config.telegramBotTokenRef),\n paperclipBaseUrl: asString(config.paperclipBaseUrl) || DEFAULT_CONNECTION_CONFIG.paperclipBaseUrl,\n paperclipPublicUrl: asString(config.paperclipPublicUrl),\n };\n}\n\nfunction extractBoardConfig(config: Record<string, unknown>): TelegramBoardConfig {\n return {\n paperclipBoardApiTokenRef: asString(config.paperclipBoardApiTokenRef),\n };\n}\n\nfunction extractAccessConfig(config: Record<string, unknown>): TelegramAccessConfig {\n return {\n enableCommands: asBoolean(config.enableCommands, DEFAULT_ACCESS_CONFIG.enableCommands),\n enableInbound: asBoolean(config.enableInbound, DEFAULT_ACCESS_CONFIG.enableInbound),\n allowedTelegramUserIds: asStringArray(config.allowedTelegramUserIds),\n allowedTelegramChatIds: asStringArray(config.allowedTelegramChatIds),\n };\n}\n\nfunction extractMediaConfig(config: Record<string, unknown>): TelegramMediaConfig {\n return {\n transcriptionApiKeyRef: asString(config.transcriptionApiKeyRef),\n briefAgentId: asString(config.briefAgentId),\n briefAgentChatIds: asStringArray(config.briefAgentChatIds),\n };\n}\n\nfunction extractEscalationConfig(config: Record<string, unknown>): TelegramEscalationConfig {\n return {\n escalationChatId: asString(config.escalationChatId),\n escalationTimeoutMs: asNumber(config.escalationTimeoutMs, DEFAULT_ESCALATION_CONFIG.escalationTimeoutMs),\n escalationDefaultAction: asEscalationDefaultAction(config.escalationDefaultAction),\n escalationHoldMessage: asString(config.escalationHoldMessage) || DEFAULT_ESCALATION_CONFIG.escalationHoldMessage,\n };\n}\n\nfunction extractProactiveConfig(config: Record<string, unknown>): TelegramProactiveConfig {\n return {\n maxSuggestionsPerHourPerCompany: asNumber(\n config.maxSuggestionsPerHourPerCompany,\n DEFAULT_PROACTIVE_CONFIG.maxSuggestionsPerHourPerCompany,\n ),\n watchDeduplicationWindowMs: asNumber(\n config.watchDeduplicationWindowMs,\n DEFAULT_PROACTIVE_CONFIG.watchDeduplicationWindowMs,\n ),\n };\n}\n\nasync function fetchHostJson<T>(input: string, init: RequestInit = {}): Promise<T> {\n const headers = new Headers(init.headers);\n headers.set(\"accept\", \"application/json\");\n\n if (typeof init.body === \"string\" && !headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/json\");\n }\n\n const response = await fetch(input, {\n ...init,\n headers,\n credentials: init.credentials ?? \"same-origin\",\n });\n const rawBody = await response.text();\n const normalizedBody = rawBody.trim();\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n\n if (\n contentType.includes(\"text/html\") ||\n normalizedBody.startsWith(\"<!DOCTYPE html\") ||\n normalizedBody.startsWith(\"<html\")\n ) {\n throw new Error(\"Paperclip returned HTML instead of JSON.\");\n }\n\n let payload: unknown = null;\n if (normalizedBody) {\n try {\n payload = JSON.parse(normalizedBody);\n } catch {\n throw new Error(\"Paperclip returned an unexpected response.\");\n }\n }\n\n if (!response.ok) {\n const message =\n typeof payload === \"object\" &&\n payload !== null &&\n \"error\" in payload &&\n typeof payload.error === \"string\"\n ? payload.error\n : `Request failed with status ${response.status}.`;\n throw new Error(message);\n }\n\n return payload as T;\n}\n\nfunction resolveBrowserOrigin(): string | null {\n if (typeof window === \"undefined\" || typeof window.location?.origin !== \"string\") {\n return null;\n }\n\n const origin = window.location.origin.trim();\n if (!origin || origin === \"null\") {\n return null;\n }\n\n try {\n const normalizedOrigin = new URL(origin);\n if (normalizedOrigin.protocol !== \"http:\" && normalizedOrigin.protocol !== \"https:\") {\n return null;\n }\n return normalizedOrigin.origin;\n } catch {\n return null;\n }\n}\n\nfunction buildPaperclipUrl(input: string): string | null {\n const origin = resolveBrowserOrigin();\n if (!origin || !input.trim() || input.trim().startsWith(\"//\")) {\n return null;\n }\n\n try {\n const candidate = new URL(input.trim(), origin);\n return candidate.origin === origin ? candidate.toString() : null;\n } catch {\n return null;\n }\n}\n\nfunction resolveCliAuthUrl(url?: string, path?: string): string | null {\n if (typeof url === \"string\" && url.trim()) {\n return buildPaperclipUrl(url.trim());\n }\n\n if (typeof path !== \"string\" || !path.trim()) {\n return null;\n }\n\n return buildPaperclipUrl(path.trim());\n}\n\nfunction resolveCliAuthPollUrl(urlOrPath?: string): string | null {\n if (typeof urlOrPath !== \"string\" || !urlOrPath.trim()) {\n return null;\n }\n\n const trimmed = urlOrPath.trim();\n if (/^[a-z][a-z0-9+.-]*:\\/\\//iu.test(trimmed)) {\n return buildPaperclipUrl(trimmed);\n }\n\n const normalizedPath = trimmed.startsWith(\"/api/\")\n ? trimmed\n : `/api${trimmed.startsWith(\"/\") ? \"\" : \"/\"}${trimmed}`;\n\n return buildPaperclipUrl(normalizedPath);\n}\n\nfunction normalizePollIntervalMs(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) {\n return 1500;\n }\n\n return Math.min(5000, Math.max(750, Math.floor(value)));\n}\n\nfunction waitForDuration(durationMs: number): Promise<void> {\n return new Promise((resolve) => {\n globalThis.setTimeout(resolve, durationMs);\n });\n}\n\nasync function requestBoardAccessChallenge(companyId: string): Promise<CliAuthChallengeResponse> {\n return fetchHostJson<CliAuthChallengeResponse>(\"/api/cli-auth/challenges\", {\n method: \"POST\",\n body: JSON.stringify({\n command: \"paperclip plugin telegram settings\",\n clientName: \"Telegram plugin\",\n requestedAccess: \"board\",\n requestedCompanyId: companyId,\n }),\n });\n}\n\nasync function waitForBoardAccessApproval(challenge: CliAuthChallengeResponse): Promise<string> {\n const challengeToken = typeof challenge.token === \"string\" ? challenge.token.trim() : \"\";\n const pollUrl = resolveCliAuthPollUrl(challenge.pollUrl ?? challenge.pollPath);\n if (!challengeToken || !pollUrl) {\n throw new Error(\"Paperclip did not return a trusted board access challenge.\");\n }\n\n const expiresAtTimeMs =\n typeof challenge.expiresAt === \"string\" ? Date.parse(challenge.expiresAt) : Number.NaN;\n const pollIntervalMs = normalizePollIntervalMs(challenge.suggestedPollIntervalMs);\n\n while (true) {\n const pollUrlWithToken = new URL(pollUrl);\n pollUrlWithToken.searchParams.set(\"token\", challengeToken);\n const pollResult = await fetchHostJson<CliAuthChallengePollResponse>(\n pollUrlWithToken.toString(),\n );\n const status =\n typeof pollResult.status === \"string\" ? pollResult.status.trim().toLowerCase() : \"pending\";\n\n if (status === \"approved\") {\n const boardApiToken =\n typeof pollResult.boardApiToken === \"string\" && pollResult.boardApiToken.trim()\n ? pollResult.boardApiToken.trim()\n : typeof challenge.boardApiToken === \"string\" && challenge.boardApiToken.trim()\n ? challenge.boardApiToken.trim()\n : \"\";\n if (!boardApiToken) {\n throw new Error(\"Paperclip approved board access but did not return a usable API token.\");\n }\n\n return boardApiToken;\n }\n\n if (status === \"cancelled\") {\n throw new Error(\"Board access approval was cancelled.\");\n }\n\n if (status === \"expired\") {\n throw new Error(\"Board access approval expired. Start the connection flow again.\");\n }\n\n if (Number.isFinite(expiresAtTimeMs) && Date.now() >= expiresAtTimeMs) {\n throw new Error(\"Board access approval expired. Start the connection flow again.\");\n }\n\n await waitForDuration(pollIntervalMs);\n }\n}\n\nfunction getIdentityLabel(identity: CliAuthIdentityResponse): string | null {\n const candidates = [\n identity.user?.displayName,\n identity.user?.name,\n identity.user?.login,\n identity.user?.email,\n identity.displayName,\n identity.name,\n identity.login,\n identity.email,\n ];\n\n for (const candidate of candidates) {\n if (typeof candidate === \"string\" && candidate.trim()) {\n return candidate.trim();\n }\n }\n\n return null;\n}\n\nasync function fetchBoardAccessIdentity(boardApiToken: string): Promise<string | null> {\n const identity = await fetchHostJson<CliAuthIdentityResponse>(\"/api/cli-auth/me\", {\n headers: {\n authorization: `Bearer ${boardApiToken.trim()}`,\n },\n });\n\n return getIdentityLabel(identity);\n}\n\nasync function fetchPluginConfig(): Promise<Record<string, unknown>> {\n const record = await fetchHostJson<PluginConfigResponse>(\n `/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`,\n );\n return record?.configJson && typeof record.configJson === \"object\" ? record.configJson : {};\n}\n\nasync function savePluginConfig(configJson: Record<string, unknown>): Promise<void> {\n await fetchHostJson(`/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`, {\n method: \"POST\",\n body: JSON.stringify({ configJson }),\n });\n}\n\nasync function resolveOrCreateCompanySecret(\n companyId: string,\n name: string,\n value: string,\n): Promise<{ id: string; name: string }> {\n const existingSecrets = await fetchHostJson<Array<{ id: string; name: string }>>(\n `/api/companies/${encodeURIComponent(companyId)}/secrets`,\n );\n const existing = existingSecrets.find(\n (secret) => secret.name.trim().toLowerCase() === name.trim().toLowerCase(),\n );\n\n if (existing) {\n return fetchHostJson<{ id: string; name: string }>(\n `/api/secrets/${encodeURIComponent(existing.id)}/rotate`,\n {\n method: \"POST\",\n body: JSON.stringify({ value }),\n },\n );\n }\n\n return fetchHostJson<{ id: string; name: string }>(\n `/api/companies/${encodeURIComponent(companyId)}/secrets`,\n {\n method: \"POST\",\n body: JSON.stringify({ name, value }),\n },\n );\n}\n\nexport function TelegramSettingsPage({ context }: PluginSettingsPageProps): React.JSX.Element {\n const boardAccess = usePluginData<BoardAccessRegistration>(\"board-access.read\");\n const updateBoardAccess = usePluginAction(\"board-access.update\");\n // ODIAA-720: instance-wide bot connection.\n const botConnection = usePluginData<BotConnectionRegistration>(\"telegram-connection.read\");\n const updateBotConnection = usePluginAction(\"telegram-connection.update\");\n const clearBotConnection = usePluginAction(\"telegram-connection.clear\");\n const [botTokenInput, setBotTokenInput] = useState(\"\");\n const [botConnecting, setBotConnecting] = useState(false);\n const [botConnectionMessage, setBotConnectionMessage] = useState<Notice | null>(null);\n const [connecting, setConnecting] = useState(false);\n const [notice, setNotice] = useState<Notice | null>(null);\n const [routingConfig, setRoutingConfig] = useState<TelegramRoutingConfig>(DEFAULT_ROUTING_CONFIG);\n const [routingSnapshot, setRoutingSnapshot] = useState<TelegramRoutingConfig>(DEFAULT_ROUTING_CONFIG);\n const [routingLoading, setRoutingLoading] = useState(true);\n const [routingSaving, setRoutingSaving] = useState(false);\n const [routingMessage, setRoutingMessage] = useState<Notice | null>(null);\n const [connectionConfig, setConnectionConfig] = useState<TelegramConnectionConfig>(DEFAULT_CONNECTION_CONFIG);\n const [connectionSnapshot, setConnectionSnapshot] = useState<TelegramConnectionConfig>(DEFAULT_CONNECTION_CONFIG);\n const [connectionLoading, setConnectionLoading] = useState(true);\n const [connectionSaving, setConnectionSaving] = useState(false);\n const [connectionMessage, setConnectionMessage] = useState<Notice | null>(null);\n const [boardConfig, setBoardConfig] = useState<TelegramBoardConfig>(DEFAULT_BOARD_CONFIG);\n const [boardSnapshot, setBoardSnapshot] = useState<TelegramBoardConfig>(DEFAULT_BOARD_CONFIG);\n const [boardConfigLoading, setBoardConfigLoading] = useState(true);\n const [boardConfigSaving, setBoardConfigSaving] = useState(false);\n const [boardConfigMessage, setBoardConfigMessage] = useState<Notice | null>(null);\n const [accessConfig, setAccessConfig] = useState<TelegramAccessConfig>(DEFAULT_ACCESS_CONFIG);\n const [accessSnapshot, setAccessSnapshot] = useState<TelegramAccessConfig>(DEFAULT_ACCESS_CONFIG);\n const [accessLoading, setAccessLoading] = useState(true);\n const [accessSaving, setAccessSaving] = useState(false);\n const [accessMessage, setAccessMessage] = useState<Notice | null>(null);\n const [mediaConfig, setMediaConfig] = useState<TelegramMediaConfig>(DEFAULT_MEDIA_CONFIG);\n const [mediaSnapshot, setMediaSnapshot] = useState<TelegramMediaConfig>(DEFAULT_MEDIA_CONFIG);\n const [mediaLoading, setMediaLoading] = useState(true);\n const [mediaSaving, setMediaSaving] = useState(false);\n const [mediaMessage, setMediaMessage] = useState<Notice | null>(null);\n const [escalationConfig, setEscalationConfig] = useState<TelegramEscalationConfig>(DEFAULT_ESCALATION_CONFIG);\n const [escalationSnapshot, setEscalationSnapshot] = useState<TelegramEscalationConfig>(DEFAULT_ESCALATION_CONFIG);\n const [escalationLoading, setEscalationLoading] = useState(true);\n const [escalationSaving, setEscalationSaving] = useState(false);\n const [escalationMessage, setEscalationMessage] = useState<Notice | null>(null);\n const [proactiveConfig, setProactiveConfig] = useState<TelegramProactiveConfig>(DEFAULT_PROACTIVE_CONFIG);\n const [proactiveSnapshot, setProactiveSnapshot] = useState<TelegramProactiveConfig>(DEFAULT_PROACTIVE_CONFIG);\n const [proactiveLoading, setProactiveLoading] = useState(true);\n const [proactiveSaving, setProactiveSaving] = useState(false);\n const [proactiveMessage, setProactiveMessage] = useState<Notice | null>(null);\n const companyId = context.companyId ?? \"\";\n const companyLabel = context.companyPrefix?.trim() || \"this company\";\n const configured = Boolean(boardAccess.data?.configured);\n const identity = boardAccess.data?.identity?.trim() || null;\n const routingDirty = JSON.stringify(routingConfig) !== JSON.stringify(routingSnapshot);\n const connectionDirty = JSON.stringify(connectionConfig) !== JSON.stringify(connectionSnapshot);\n const boardConfigDirty = JSON.stringify(boardConfig) !== JSON.stringify(boardSnapshot);\n const accessDirty = JSON.stringify(accessConfig) !== JSON.stringify(accessSnapshot);\n const mediaDirty = JSON.stringify(mediaConfig) !== JSON.stringify(mediaSnapshot);\n const escalationDirty = JSON.stringify(escalationConfig) !== JSON.stringify(escalationSnapshot);\n const proactiveDirty = JSON.stringify(proactiveConfig) !== JSON.stringify(proactiveSnapshot);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadRoutingConfig(): Promise<void> {\n setRoutingLoading(true);\n setRoutingMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextRoutingConfig = extractRoutingConfig(config);\n setRoutingConfig(nextRoutingConfig);\n setRoutingSnapshot(nextRoutingConfig);\n } catch (error) {\n if (!cancelled) {\n setRoutingMessage({\n tone: \"error\",\n title: \"Routing settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setRoutingLoading(false);\n }\n }\n }\n\n void loadRoutingConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadProactiveConfig(): Promise<void> {\n setProactiveLoading(true);\n setProactiveMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextProactiveConfig = extractProactiveConfig(config);\n setProactiveConfig(nextProactiveConfig);\n setProactiveSnapshot(nextProactiveConfig);\n } catch (error) {\n if (!cancelled) {\n setProactiveMessage({\n tone: \"error\",\n title: \"Proactive suggestion settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setProactiveLoading(false);\n }\n }\n }\n\n void loadProactiveConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadEscalationConfig(): Promise<void> {\n setEscalationLoading(true);\n setEscalationMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextEscalationConfig = extractEscalationConfig(config);\n setEscalationConfig(nextEscalationConfig);\n setEscalationSnapshot(nextEscalationConfig);\n } catch (error) {\n if (!cancelled) {\n setEscalationMessage({\n tone: \"error\",\n title: \"Human escalation settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setEscalationLoading(false);\n }\n }\n }\n\n void loadEscalationConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadMediaConfig(): Promise<void> {\n setMediaLoading(true);\n setMediaMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextMediaConfig = extractMediaConfig(config);\n setMediaConfig(nextMediaConfig);\n setMediaSnapshot(nextMediaConfig);\n } catch (error) {\n if (!cancelled) {\n setMediaMessage({\n tone: \"error\",\n title: \"Media intake settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setMediaLoading(false);\n }\n }\n }\n\n void loadMediaConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadAccessConfig(): Promise<void> {\n setAccessLoading(true);\n setAccessMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextAccessConfig = extractAccessConfig(config);\n setAccessConfig(nextAccessConfig);\n setAccessSnapshot(nextAccessConfig);\n } catch (error) {\n if (!cancelled) {\n setAccessMessage({\n tone: \"error\",\n title: \"Access settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setAccessLoading(false);\n }\n }\n }\n\n void loadAccessConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadConnectionConfig(): Promise<void> {\n setConnectionLoading(true);\n setConnectionMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextConnectionConfig = extractConnectionConfig(config);\n setConnectionConfig(nextConnectionConfig);\n setConnectionSnapshot(nextConnectionConfig);\n } catch (error) {\n if (!cancelled) {\n setConnectionMessage({\n tone: \"error\",\n title: \"Connection settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setConnectionLoading(false);\n }\n }\n }\n\n void loadConnectionConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadBoardConfig(): Promise<void> {\n setBoardConfigLoading(true);\n setBoardConfigMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextBoardConfig = extractBoardConfig(config);\n setBoardConfig(nextBoardConfig);\n setBoardSnapshot(nextBoardConfig);\n } catch (error) {\n if (!cancelled) {\n setBoardConfigMessage({\n tone: \"error\",\n title: \"Board fallback setting could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setBoardConfigLoading(false);\n }\n }\n }\n\n void loadBoardConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n function updateRoutingField<K extends keyof TelegramRoutingConfig>(\n key: K,\n value: TelegramRoutingConfig[K],\n ): void {\n setRoutingConfig((current) => ({ ...current, [key]: value }));\n setRoutingMessage(null);\n }\n\n function addOpsRoute(): void {\n setRoutingConfig((current) => ({\n ...current,\n opsRoutes: [\n ...current.opsRoutes,\n { name: \"\", enabled: true, companyId: \"\", companyName: \"\", chatId: \"\", topicId: \"\" },\n ],\n }));\n setRoutingMessage(null);\n }\n\n function updateOpsRoute<K extends keyof TelegramOpsRouteForm>(\n index: number,\n key: K,\n value: TelegramOpsRouteForm[K],\n ): void {\n setRoutingConfig((current) => ({\n ...current,\n opsRoutes: current.opsRoutes.map((route, i) =>\n i === index ? { ...route, [key]: value } : route,\n ),\n }));\n setRoutingMessage(null);\n }\n\n function removeOpsRoute(index: number): void {\n setRoutingConfig((current) => ({\n ...current,\n opsRoutes: current.opsRoutes.filter((_, i) => i !== index),\n }));\n setRoutingMessage(null);\n }\n\n function updateBoardField<K extends keyof TelegramBoardConfig>(\n key: K,\n value: TelegramBoardConfig[K],\n ): void {\n setBoardConfig((current) => ({ ...current, [key]: value }));\n setBoardConfigMessage(null);\n }\n\n function updateAccessField<K extends keyof TelegramAccessConfig>(\n key: K,\n value: TelegramAccessConfig[K],\n ): void {\n setAccessConfig((current) => ({ ...current, [key]: value }));\n setAccessMessage(null);\n }\n\n function updateConnectionField<K extends keyof TelegramConnectionConfig>(\n key: K,\n value: TelegramConnectionConfig[K],\n ): void {\n setConnectionConfig((current) => ({ ...current, [key]: value }));\n setConnectionMessage(null);\n }\n\n function updateMediaField<K extends keyof TelegramMediaConfig>(\n key: K,\n value: TelegramMediaConfig[K],\n ): void {\n setMediaConfig((current) => ({ ...current, [key]: value }));\n setMediaMessage(null);\n }\n\n function updateEscalationField<K extends keyof TelegramEscalationConfig>(\n key: K,\n value: TelegramEscalationConfig[K],\n ): void {\n setEscalationConfig((current) => ({ ...current, [key]: value }));\n setEscalationMessage(null);\n }\n\n function updateProactiveField<K extends keyof TelegramProactiveConfig>(\n key: K,\n value: TelegramProactiveConfig[K],\n ): void {\n setProactiveConfig((current) => ({ ...current, [key]: value }));\n setProactiveMessage(null);\n }\n\n async function handleSaveBoardConfig(): Promise<void> {\n setBoardConfigSaving(true);\n setBoardConfigMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...boardConfig };\n await savePluginConfig(nextConfig);\n setBoardSnapshot(boardConfig);\n setBoardConfigMessage({\n tone: \"success\",\n title: \"Board fallback saved\",\n text: \"The connection workflow remains preferred. This secret reference is used only as a manual fallback.\",\n });\n } catch (error) {\n setBoardConfigMessage({\n tone: \"error\",\n title: \"Board fallback could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setBoardConfigSaving(false);\n }\n }\n\n async function handleSaveAccessConfig(): Promise<void> {\n setAccessSaving(true);\n setAccessMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...accessConfig };\n await savePluginConfig(nextConfig);\n setAccessSnapshot(accessConfig);\n setAccessMessage({\n tone: \"success\",\n title: \"Bot access settings saved\",\n text: \"If the worker has already cached Telegram updates, restart the plugin if the new allowlist behavior is not picked up immediately.\",\n });\n } catch (error) {\n setAccessMessage({\n tone: \"error\",\n title: \"Bot access settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setAccessSaving(false);\n }\n }\n\n async function handleSaveRoutingConfig(): Promise<void> {\n setRoutingSaving(true);\n setRoutingMessage(null);\n try {\n // Drop blank rows the operator added but never filled in, then validate\n // the rest: every ops route needs a chat ID and a company match key\n // (companyId or companyName), and companyId must be unique across routes.\n const trimmedRoutes = routingConfig.opsRoutes.map((route) => ({\n name: route.name.trim(),\n enabled: route.enabled,\n companyId: route.companyId.trim(),\n companyName: route.companyName.trim(),\n chatId: route.chatId.trim(),\n topicId: route.topicId.trim(),\n }));\n const opsRoutes = trimmedRoutes.filter(\n (route) => route.companyId || route.companyName || route.chatId || route.name,\n );\n\n const opsRouteError = validateOpsRoutes(opsRoutes);\n if (opsRouteError) {\n setRoutingMessage({ tone: \"error\", title: \"Ops route is invalid\", text: opsRouteError });\n return;\n }\n\n const sanitizedRouting = { ...routingConfig, opsRoutes };\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...sanitizedRouting };\n await savePluginConfig(nextConfig);\n setRoutingConfig(sanitizedRouting);\n setRoutingSnapshot(sanitizedRouting);\n setRoutingMessage({\n tone: \"success\",\n title: \"Notification routing saved\",\n text: \"Refresh the page if another browser tab edited these settings at the same time.\",\n });\n } catch (error) {\n setRoutingMessage({\n tone: \"error\",\n title: \"Notification routing could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setRoutingSaving(false);\n }\n }\n\n // ODIAA-720: connect the bot instance-wide. The raw token is sent to the\n // worker action (which validates it via Telegram getMe and stores it in\n // instance-scoped state); the frontend keeps only the masked registration.\n // We then re-save the plugin config so the host reloads the worker and it\n // begins polling with the freshly connected token.\n async function handleConnectBot(): Promise<void> {\n const token = botTokenInput.trim();\n if (!token) {\n setBotConnectionMessage({ tone: \"error\", title: \"Enter a bot token first\" });\n return;\n }\n setBotConnecting(true);\n setBotConnectionMessage(null);\n try {\n const result = (await updateBotConnection({ token })) as BotConnectionRegistration;\n // Touch the plugin config so the host reloads the worker and starts polling.\n try {\n await savePluginConfig(await fetchPluginConfig());\n } catch {\n // Non-fatal: the token is connected; the worker will pick it up on its\n // next reload even if this touch fails.\n }\n setBotTokenInput(\"\");\n await botConnection.refresh?.();\n const who = result?.botUsername ? `@${result.botUsername}` : \"your bot\";\n setBotConnectionMessage({\n tone: \"success\",\n title: `Connected ${who} instance-wide`,\n text: \"The bot token is stored once for the whole instance \u2014 every company can now reach the board through this bot. No company secret required.\",\n });\n } catch (error) {\n setBotConnectionMessage({\n tone: \"error\",\n title: \"Could not connect the bot\",\n text: getErrorMessage(error),\n });\n } finally {\n setBotConnecting(false);\n }\n }\n\n async function handleDisconnectBot(): Promise<void> {\n setBotConnecting(true);\n setBotConnectionMessage(null);\n try {\n await clearBotConnection({});\n await botConnection.refresh?.();\n setBotConnectionMessage({\n tone: \"success\",\n title: \"Bot disconnected\",\n text: \"The stored instance token was cleared. The plugin will idle until a bot is reconnected.\",\n });\n } catch (error) {\n setBotConnectionMessage({\n tone: \"error\",\n title: \"Could not disconnect the bot\",\n text: getErrorMessage(error),\n });\n } finally {\n setBotConnecting(false);\n }\n }\n\n async function handleSaveConnectionConfig(): Promise<void> {\n setConnectionSaving(true);\n setConnectionMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...connectionConfig };\n await savePluginConfig(nextConfig);\n setConnectionSnapshot(connectionConfig);\n setConnectionMessage({\n tone: \"success\",\n title: \"Connection settings saved\",\n text: \"These settings control the bot token and the Paperclip URLs used by Telegram messages and approval actions.\",\n });\n } catch (error) {\n setConnectionMessage({\n tone: \"error\",\n title: \"Connection settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setConnectionSaving(false);\n }\n }\n\n async function handleSaveMediaConfig(): Promise<void> {\n setMediaSaving(true);\n setMediaMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...mediaConfig };\n await savePluginConfig(nextConfig);\n setMediaSnapshot(mediaConfig);\n setMediaMessage({\n tone: \"success\",\n title: \"Media intake settings saved\",\n text: \"Media in configured intake chats is routed to the Brief Agent. Media in other chats can still go to active topic agent sessions.\",\n });\n } catch (error) {\n setMediaMessage({\n tone: \"error\",\n title: \"Media intake settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setMediaSaving(false);\n }\n }\n\n async function handleSaveEscalationConfig(): Promise<void> {\n setEscalationSaving(true);\n setEscalationMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...escalationConfig };\n await savePluginConfig(nextConfig);\n setEscalationSnapshot(escalationConfig);\n setEscalationMessage({\n tone: \"success\",\n title: \"Human escalation settings saved\",\n text: \"Escalations are sent to the configured Telegram chat when an agent invokes the human handoff tool.\",\n });\n } catch (error) {\n setEscalationMessage({\n tone: \"error\",\n title: \"Human escalation settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setEscalationSaving(false);\n }\n }\n\n async function handleSaveProactiveConfig(): Promise<void> {\n setProactiveSaving(true);\n setProactiveMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...proactiveConfig };\n await savePluginConfig(nextConfig);\n setProactiveSnapshot(proactiveConfig);\n setProactiveMessage({\n tone: \"success\",\n title: \"Proactive suggestion settings saved\",\n text: \"These limits apply when the scheduled watch job evaluates registered watches and sends Telegram suggestions.\",\n });\n } catch (error) {\n setProactiveMessage({\n tone: \"error\",\n title: \"Proactive suggestion settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setProactiveSaving(false);\n }\n }\n\n async function handleConnectBoardAccess(): Promise<void> {\n if (!companyId) {\n setNotice({\n tone: \"error\",\n title: \"Open company settings first\",\n text: \"Board access tokens are saved as company secrets, so this flow needs a company context.\",\n });\n return;\n }\n\n setConnecting(true);\n setNotice(null);\n let approvalWindow: Window | null = null;\n\n try {\n if (typeof window !== \"undefined\") {\n approvalWindow = window.open(\"about:blank\", \"_blank\");\n }\n\n const challenge = await requestBoardAccessChallenge(companyId);\n const approvalUrl = resolveCliAuthUrl(challenge.approvalUrl, challenge.approvalPath);\n if (!approvalUrl) {\n throw new Error(\"Paperclip did not return a trusted board approval URL.\");\n }\n\n if (!approvalWindow && typeof window !== \"undefined\") {\n approvalWindow = window.open(approvalUrl, \"_blank\");\n } else {\n approvalWindow?.location.replace(approvalUrl);\n }\n\n if (!approvalWindow) {\n throw new Error(\"Allow pop-ups for Paperclip, then try connecting board access again.\");\n }\n\n const boardApiToken = await waitForBoardAccessApproval(challenge);\n const nextIdentity = await fetchBoardAccessIdentity(boardApiToken);\n const secretName = `telegram_board_api_${companyId.replace(/[^a-z0-9]+/gi, \"_\").toLowerCase()}`;\n const secret = await resolveOrCreateCompanySecret(companyId, secretName, boardApiToken);\n\n await updateBoardAccess({\n companyId,\n paperclipBoardApiTokenRef: secret.id,\n identity: nextIdentity,\n });\n await boardAccess.refresh();\n\n setNotice({\n tone: \"success\",\n title: nextIdentity ? `Connected as ${nextIdentity}` : \"Board access connected\",\n text: \"Telegram approval actions can now authenticate with Paperclip.\",\n });\n } catch (error) {\n setNotice({\n tone: \"error\",\n title: \"Board access could not be connected\",\n text: getErrorMessage(error),\n });\n } finally {\n setConnecting(false);\n try {\n approvalWindow?.close();\n } catch {\n // Ignore browser close restrictions.\n }\n }\n }\n\n return (\n <main style={{ display: \"grid\", gap: 24, padding: 24, color: \"#111827\" }}>\n <section style={{ display: \"grid\", gap: 8 }}>\n <h1 style={{ fontSize: 24, lineHeight: \"32px\", margin: 0 }}>Telegram Bot</h1>\n <p style={{ color: \"#6b7280\", margin: 0, maxWidth: 760 }}>\n Configure Telegram connection, access control, notification routing, media intake, escalation, and proactive suggestion behavior.\n </p>\n </section>\n\n {notice ? (\n <div\n style={{\n border: `1px solid ${notice.tone === \"success\" ? \"#99f6e4\" : \"#fecaca\"}`,\n borderRadius: 8,\n background: notice.tone === \"success\" ? \"#f0fdfa\" : \"#fef2f2\",\n color: notice.tone === \"success\" ? \"#115e59\" : \"#991b1b\",\n padding: 14,\n }}\n >\n <strong>{notice.title}</strong>\n {notice.text ? <p style={{ margin: \"6px 0 0\" }}>{notice.text}</p> : null}\n </div>\n ) : null}\n\n {/* ODIAA-720: instance-wide bot connection. The token is configured once\n for the whole instance \u2014 no per-company secret required. */}\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Bot Connection</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Connect your Telegram bot once for the whole instance. Every company can then reach the board through this bot \u2014 no per-company secret needed. The token is validated with Telegram and stored securely server-side; it is never shown again here.\n </p>\n </div>\n\n {botConnection.loading ? (\n <p style={{ color: \"#6b7280\", margin: 0 }}>Checking bot connection\u2026</p>\n ) : botConnection.data?.configured ? (\n <div\n style={{\n background: \"#f0fdf4\",\n border: \"1px solid #bbf7d0\",\n borderRadius: 8,\n color: \"#166534\",\n display: \"grid\",\n gap: 4,\n padding: 14,\n }}\n >\n <strong>\n {botConnection.data.source === \"instance-state\"\n ? `Connected${botConnection.data.botUsername ? ` as @${botConnection.data.botUsername}` : \"\"} (instance-wide)`\n : \"Connected via legacy secret reference\"}\n </strong>\n <span style={{ fontSize: 13 }}>\n {botConnection.data.source === \"instance-state\"\n ? \"This bot serves every company on the instance.\"\n : \"Using the advanced telegramBotTokenRef secret below. Reconnect above to switch to the instance-wide token store.\"}\n </span>\n </div>\n ) : (\n <div\n style={{\n background: \"#fffbeb\",\n border: \"1px solid #fde68a\",\n borderRadius: 8,\n color: \"#92400e\",\n padding: 14,\n }}\n >\n <strong>No bot connected.</strong> Paste a bot token from @BotFather below to connect.\n </div>\n )}\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <TextField\n disabled={botConnecting}\n label=\"Telegram bot token\"\n onChange={(value) => setBotTokenInput(value)}\n placeholder=\"123456789:AA\u2026 (from @BotFather)\"\n type=\"password\"\n value={botTokenInput}\n >\n Pasted once and stored server-side for the whole instance. Leave blank to keep the current connection.\n </TextField>\n </div>\n\n {botConnectionMessage ? <NoticeBlock notice={botConnectionMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n {botConnection.data?.configured && botConnection.data.source === \"instance-state\" ? (\n <button\n disabled={botConnecting}\n onClick={() => void handleDisconnectBot()}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: botConnecting ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n >\n Disconnect\n </button>\n ) : null}\n <button\n disabled={botConnecting || !botTokenInput.trim()}\n onClick={() => void handleConnectBot()}\n style={{\n background: botConnecting || !botTokenInput.trim() ? \"#9ca3af\" : \"#111827\",\n border: \"none\",\n borderRadius: 8,\n color: \"white\",\n cursor: botConnecting || !botTokenInput.trim() ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n >\n {botConnecting ? \"Connecting\u2026\" : \"Connect bot\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Connection & URLs</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Paperclip URLs used by the Telegram worker. The bot token is configured above in <strong>Bot Connection</strong>; the secret-ref field below is an advanced fallback for legacy installs.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <TextField\n disabled={connectionLoading || connectionSaving}\n label=\"Telegram bot token secret ref (advanced / legacy)\"\n onChange={(value) => updateConnectionField(\"telegramBotTokenRef\", value)}\n placeholder=\"Secret UUID from Paperclip settings\"\n value={connectionConfig.telegramBotTokenRef}\n >\n Optional fallback. Secret UUID for your bot token. Prefer <strong>Bot Connection</strong> above \u2014 secret refs are company-scoped and are disabled on recent paperclipai master (post-#5429).\n </TextField>\n <TextField\n disabled={connectionLoading || connectionSaving}\n label=\"Paperclip API URL\"\n onChange={(value) => updateConnectionField(\"paperclipBaseUrl\", value)}\n placeholder=\"http://localhost:3100\"\n value={connectionConfig.paperclipBaseUrl}\n >\n Internal Paperclip API URL used by the plugin for actions such as approvals and comments. Keep localhost for same-server deployments.\n </TextField>\n <TextField\n disabled={connectionLoading || connectionSaving}\n label=\"Paperclip public URL\"\n onChange={(value) => updateConnectionField(\"paperclipPublicUrl\", value)}\n placeholder=\"https://paperclip.example.com\"\n value={connectionConfig.paperclipPublicUrl}\n >\n Public URL used in Telegram links. Leave empty to fall back to the API URL.\n </TextField>\n </div>\n\n {connectionMessage ? <NoticeBlock notice={connectionMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={connectionLoading || connectionSaving}\n onClick={() => {\n setConnectionConfig(connectionSnapshot);\n setConnectionMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: connectionLoading || connectionSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={connectionLoading || connectionSaving || !connectionDirty}\n onClick={() => {\n void handleSaveConnectionConfig();\n }}\n style={{\n background: connectionLoading || connectionSaving || !connectionDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: connectionLoading || connectionSaving || !connectionDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {connectionSaving ? \"Saving...\" : \"Save connection\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ alignItems: \"start\", display: \"flex\", gap: 16, justifyContent: \"space-between\" }}>\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Board Access Connection</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Telegram approval buttons need board access when Paperclip requires authenticated approval mutations.\n </p>\n </div>\n <span\n style={{\n background: configured ? \"#ccfbf1\" : \"#f3f4f6\",\n borderRadius: 999,\n color: configured ? \"#0f766e\" : \"#4b5563\",\n fontSize: 12,\n fontWeight: 700,\n padding: \"5px 10px\",\n whiteSpace: \"nowrap\",\n }}\n >\n {connecting ? \"Connecting\" : configured ? \"Connected\" : \"Not connected\"}\n </span>\n </div>\n\n <div\n style={{\n alignItems: \"center\",\n background: \"#f9fafb\",\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"flex\",\n gap: 16,\n justifyContent: \"space-between\",\n padding: 14,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <strong>\n {!companyId\n ? \"Open this page inside a company\"\n : configured\n ? identity\n ? `Connected as ${identity}`\n : `Connected for ${companyLabel}`\n : `Connect board access for ${companyLabel}`}\n </strong>\n <span style={{ color: \"#6b7280\" }}>\n {configured\n ? \"The board token is stored as a Paperclip secret; the plugin keeps only the secret reference.\"\n : \"This opens a Paperclip approval page, then saves the resulting board token as a company secret.\"}\n </span>\n </div>\n <button\n disabled={!companyId || connecting || boardAccess.loading}\n onClick={() => {\n void handleConnectBoardAccess();\n }}\n style={{\n background: !companyId || connecting || boardAccess.loading ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: !companyId || connecting || boardAccess.loading ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 190,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {connecting ? \"Waiting for approval...\" : configured ? \"Reconnect board access\" : \"Connect board access\"}\n </button>\n </div>\n\n <div style={{ borderTop: \"1px solid #e5e7eb\", display: \"grid\", gap: 12, paddingTop: 14 }}>\n <TextField\n disabled={boardConfigLoading || boardConfigSaving}\n label=\"Board API token secret ref fallback\"\n onChange={(value) => updateBoardField(\"paperclipBoardApiTokenRef\", value)}\n placeholder=\"Optional Paperclip secret UUID\"\n value={boardConfig.paperclipBoardApiTokenRef}\n >\n 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.\n </TextField>\n\n {boardConfigMessage ? <NoticeBlock notice={boardConfigMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={boardConfigLoading || boardConfigSaving}\n onClick={() => {\n setBoardConfig(boardSnapshot);\n setBoardConfigMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: boardConfigLoading || boardConfigSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={boardConfigLoading || boardConfigSaving || !boardConfigDirty}\n onClick={() => {\n void handleSaveBoardConfig();\n }}\n style={{\n background: boardConfigLoading || boardConfigSaving || !boardConfigDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: boardConfigLoading || boardConfigSaving || !boardConfigDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {boardConfigSaving ? \"Saving...\" : \"Save fallback\"}\n </button>\n </div>\n </div>\n\n {boardAccess.error ? (\n <p style={{ color: \"#991b1b\", margin: 0 }}>\n Could not read board access state: {boardAccess.error.message}\n </p>\n ) : null}\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Bot Interaction & Access Control</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Controls who can use the bot interactively. Empty allowlists are permissive; set both user and chat IDs for strict private-group access.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <CheckboxField\n checked={accessConfig.enableCommands}\n disabled={accessLoading || accessSaving}\n label=\"Enable bot commands\"\n onChange={(value) => updateAccessField(\"enableCommands\", value)}\n >\n Allow Telegram users to run commands such as /status, /issues, and /agents. Use allowlists when commands are enabled.\n </CheckboxField>\n <CheckboxField\n checked={accessConfig.enableInbound}\n disabled={accessLoading || accessSaving}\n label=\"Enable inbound replies\"\n onChange={(value) => updateAccessField(\"enableInbound\", value)}\n >\n Route Telegram replies to Paperclip issue comments when a message replies to a bot notification. Use allowlists when inbound replies are enabled.\n </CheckboxField>\n <ArrayField\n disabled={accessLoading || accessSaving}\n emptyValueLabel=\"No user IDs configured\"\n label=\"Allowed Telegram user IDs\"\n newItemLabel=\"Add user ID\"\n onChange={(value) => updateAccessField(\"allowedTelegramUserIds\", value)}\n placeholder=\"6395513943\"\n value={accessConfig.allowedTelegramUserIds}\n >\n Optional. One Telegram user ID per line. Leave empty to allow any user. Applies to commands, inbound replies, media intake, and button callbacks.\n </ArrayField>\n <ArrayField\n disabled={accessLoading || accessSaving}\n emptyValueLabel=\"No chat IDs configured\"\n label=\"Allowed Telegram chat IDs\"\n newItemLabel=\"Add chat ID\"\n onChange={(value) => updateAccessField(\"allowedTelegramChatIds\", value)}\n placeholder=\"-1003800613668\"\n value={accessConfig.allowedTelegramChatIds}\n >\n 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.\n </ArrayField>\n </div>\n\n {accessMessage ? <NoticeBlock notice={accessMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={accessLoading || accessSaving}\n onClick={() => {\n setAccessConfig(accessSnapshot);\n setAccessMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: accessLoading || accessSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={accessLoading || accessSaving || !accessDirty}\n onClick={() => {\n void handleSaveAccessConfig();\n }}\n style={{\n background: accessLoading || accessSaving || !accessDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: accessLoading || accessSaving || !accessDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {accessSaving ? \"Saving...\" : \"Save access\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Notification Routing & Forum Topics</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n 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.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 10,\n padding: 12,\n }}\n >\n <strong>Default route</strong>\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Fallback Chat ID</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"defaultChatId\", event.currentTarget.value)}\n placeholder=\"Default chat ID\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.defaultChatId}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Used when a notification type leaves its Chat ID empty and no company-specific chat is connected.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.topicRouting}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"topicRouting\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Forum topic routing\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Route project-linked notifications to Telegram forum topics mapped with /connect_topic.\n </span>\n </label>\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Max agents per forum topic</span>\n <input\n disabled={routingLoading || routingSaving}\n min={1}\n onChange={(event) => updateRoutingField(\"maxAgentsPerThread\", Number(event.currentTarget.value))}\n placeholder=\"3\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n maxWidth: 180,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"number\"\n value={routingConfig.maxAgentsPerThread}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Maximum concurrent agent sessions allowed inside one Telegram forum topic. This applies to /acp agent sessions, not notification delivery.\n </span>\n </label>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 10,\n padding: 12,\n }}\n >\n <strong>Issues</strong>\n <div style={{ display: \"grid\", gap: 10 }}>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnIssueCreated}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnIssueCreated\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Created\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send a Telegram notification when a new issue is created.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnIssueDone}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnIssueDone\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Completed\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send a Telegram notification when an issue is completed.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnIssueAssigned}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnIssueAssigned\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Assignment changes\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send a Telegram notification when an issue assignee changes.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnIssueBlocked}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnIssueBlocked\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Blocked\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Notify when an issue becomes blocked and is owned by a human/board user. Agent-only blocks are ignored to reduce noise.\n </span>\n </label>\n </div>\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Only when assigned to user ID</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"onlyNotifyIfAssignedTo\", event.currentTarget.value)}\n placeholder=\"Paperclip user ID\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.onlyNotifyIfAssignedTo}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Optional. Restricts assignment-change notifications to issues assigned to this Paperclip user.\n </span>\n </label>\n <div style={{ display: \"grid\", gap: 10 }}>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnBoardMention}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnBoardMention\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Board mentions\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Notify when an issue comment @-mentions one of the board usernames below. Matching is case-insensitive and word-boundary aware.\n </span>\n </label>\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Board usernames</span>\n <input\n disabled={routingLoading || routingSaving || !routingConfig.notifyOnBoardMention}\n onChange={(event) => updateRoutingField(\"boardUsernames\", event.currentTarget.value)}\n placeholder=\"ceo, board (comma-separated, no @)\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.boardUsernames}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Comma- or space-separated handles. A comment forwards only when it contains <code>@<handle></code> for one of these.\n </span>\n </label>\n </div>\n </section>\n\n <RoutingRow\n title=\"Approvals\"\n chatId={routingConfig.approvalsChatId}\n topicId={routingConfig.approvalsTopicId}\n chatPlaceholder=\"Approvals chat ID\"\n topicPlaceholder=\"Approvals topic ID\"\n disabled={routingLoading || routingSaving}\n onChatIdChange={(value) => updateRoutingField(\"approvalsChatId\", value)}\n onTopicIdChange={(value) => updateRoutingField(\"approvalsTopicId\", value)}\n chatHelp=\"Leave empty to use the default route for approval notifications.\"\n footer={\n <>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnApprovalCreated}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnApprovalCreated\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Enabled\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send Telegram notifications when approval requests are created.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.onlyNotifyBoardApprovals}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"onlyNotifyBoardApprovals\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Board requests only\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Ignore internal approvals and notify only when an agent requests Board approval.\n </span>\n </label>\n </>\n }\n />\n\n <RoutingRow\n title=\"Errors\"\n chatId={routingConfig.errorsChatId}\n topicId={routingConfig.errorsTopicId}\n chatPlaceholder=\"Errors chat ID\"\n topicPlaceholder=\"Errors topic ID\"\n disabled={routingLoading || routingSaving}\n onChatIdChange={(value) => updateRoutingField(\"errorsChatId\", value)}\n onTopicIdChange={(value) => updateRoutingField(\"errorsTopicId\", value)}\n chatHelp=\"Leave empty to use the default route for agent error notifications.\"\n footer={\n <>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnAgentError}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnAgentError\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Errors enabled\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send Telegram notifications when an agent run reports an error.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnAgentRunStarted}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnAgentRunStarted\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Run started\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n 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.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnAgentRunFinished}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnAgentRunFinished\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Run finished\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n 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.\n </span>\n </label>\n </>\n }\n />\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 10,\n padding: 12,\n }}\n >\n <div style={{ alignItems: \"center\", display: \"flex\", justifyContent: \"space-between\" }}>\n <strong>Ops routes</strong>\n <button\n disabled={routingLoading || routingSaving}\n onClick={() => addOpsRoute()}\n style={{\n background: \"#111827\",\n border: \"none\",\n borderRadius: 8,\n color: \"#fff\",\n cursor: routingLoading || routingSaving ? \"not-allowed\" : \"pointer\",\n fontSize: 13,\n fontWeight: 600,\n padding: \"6px 12px\",\n }}\n type=\"button\"\n >\n Add ops route\n </button>\n </div>\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Divert run-lifecycle (run started / run finished) notifications for a specific company\n to a dedicated ops chat, keeping the primary chat for important signals. The first\n enabled route matching by Company ID (or Company name) wins; if none match, ops events\n fall back to the default chat.\n </span>\n {routingConfig.opsRoutes.length === 0 ? (\n <span style={{ color: \"#9ca3af\", fontSize: 12, fontStyle: \"italic\" }}>\n No ops routes configured.\n </span>\n ) : (\n <div style={{ display: \"grid\", gap: 12 }}>\n {routingConfig.opsRoutes.map((route, index) => (\n <div\n key={index}\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 8,\n padding: 10,\n }}\n >\n <div style={{ alignItems: \"center\", display: \"flex\", gap: 12, justifyContent: \"space-between\" }}>\n <label style={{ alignItems: \"center\", color: \"#374151\", display: \"flex\", fontSize: 13, gap: 8 }}>\n <input\n checked={route.enabled}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"enabled\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Enabled\n </label>\n <button\n disabled={routingLoading || routingSaving}\n onClick={() => removeOpsRoute(index)}\n style={{\n background: \"transparent\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#b91c1c\",\n cursor: routingLoading || routingSaving ? \"not-allowed\" : \"pointer\",\n fontSize: 12,\n fontWeight: 600,\n padding: \"4px 10px\",\n }}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Name (optional)</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"name\", event.currentTarget.value)}\n placeholder=\"e.g. Acme Ops\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.name}\n />\n </label>\n <div style={{ display: \"grid\", gap: 8, gridTemplateColumns: \"repeat(2, minmax(0, 1fr))\" }}>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Company ID</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"companyId\", event.currentTarget.value)}\n placeholder=\"Company UUID\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.companyId}\n />\n </label>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Company name</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"companyName\", event.currentTarget.value)}\n placeholder=\"Fallback match by name\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.companyName}\n />\n </label>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Chat ID</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"chatId\", event.currentTarget.value)}\n placeholder=\"Ops chat ID\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.chatId}\n />\n </label>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Topic ID (optional)</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"topicId\", event.currentTarget.value)}\n placeholder=\"Forum topic ID\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.topicId}\n />\n </label>\n </div>\n </div>\n ))}\n </div>\n )}\n </section>\n\n <RoutingRow\n title=\"Digests\"\n chatId={routingConfig.digestChatId}\n topicId={routingConfig.digestTopicId}\n chatPlaceholder=\"Digest chat ID\"\n topicPlaceholder=\"Digest topic ID\"\n disabled={routingLoading || routingSaving}\n onChatIdChange={(value) => updateRoutingField(\"digestChatId\", value)}\n onTopicIdChange={(value) => updateRoutingField(\"digestTopicId\", value)}\n chatHelp=\"Leave empty to use the company/default route for digest notifications.\"\n footer={\n <div style={{ display: \"grid\", gap: 10 }}>\n <label style={{ display: \"grid\", gap: 6 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Mode</span>\n <select\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"digestMode\", event.currentTarget.value as TelegramRoutingConfig[\"digestMode\"])}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n maxWidth: 280,\n padding: \"9px 10px\",\n }}\n value={routingConfig.digestMode}\n >\n <option value=\"off\">Off</option>\n <option value=\"daily\">Daily</option>\n <option value=\"bidaily\">Bidaily</option>\n <option value=\"tridaily\">Tridaily</option>\n </select>\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Off disables digest notifications. Times are UTC.\n </span>\n </label>\n <div style={{ alignItems: \"stretch\", display: \"grid\", gap: 10, gridTemplateColumns: \"repeat(3, minmax(0, 1fr))\" }}>\n <label style={{ display: \"grid\", gap: 5, gridTemplateRows: \"auto auto minmax(32px, auto)\" }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Daily time</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"dailyDigestTime\", event.currentTarget.value)}\n placeholder=\"09:00\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.dailyDigestTime}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12, lineHeight: \"16px\" }}>\n Used for daily mode and as the first bidaily slot.\n </span>\n </label>\n <label style={{ display: \"grid\", gap: 5, gridTemplateRows: \"auto auto minmax(32px, auto)\" }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Bidaily second time</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"bidailySecondTime\", event.currentTarget.value)}\n placeholder=\"17:00\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.bidailySecondTime}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12, lineHeight: \"16px\" }}>\n Second send time when bidaily mode is selected.\n </span>\n </label>\n <label style={{ display: \"grid\", gap: 5, gridTemplateRows: \"auto auto minmax(32px, auto)\" }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Tridaily times</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"tridailyTimes\", event.currentTarget.value)}\n placeholder=\"07:00,13:00,19:00\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.tridailyTimes}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12, lineHeight: \"16px\" }}>\n Three comma-separated UTC times for tridaily mode.\n </span>\n </label>\n </div>\n </div>\n }\n />\n </div>\n\n {routingMessage ? <NoticeBlock notice={routingMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={routingLoading || routingSaving}\n onClick={() => {\n setRoutingConfig(routingSnapshot);\n setRoutingMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: routingLoading || routingSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={routingLoading || routingSaving || !routingDirty}\n onClick={() => {\n void handleSaveRoutingConfig();\n }}\n style={{\n background: routingLoading || routingSaving || !routingDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: routingLoading || routingSaving || !routingDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {routingSaving ? \"Saving...\" : \"Save routing\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Media Intake / Brief Agent</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Routes Telegram voice, audio, documents, and photos either to a Brief Agent intake flow or to active agent sessions inside forum topics.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <TextField\n disabled={mediaLoading || mediaSaving}\n label=\"Transcription API key secret ref\"\n onChange={(value) => updateMediaField(\"transcriptionApiKeyRef\", value)}\n placeholder=\"OpenAI API key secret UUID\"\n value={mediaConfig.transcriptionApiKeyRef}\n >\n 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.\n </TextField>\n <TextField\n disabled={mediaLoading || mediaSaving}\n label=\"Brief Agent ID\"\n onChange={(value) => updateMediaField(\"briefAgentId\", value)}\n placeholder=\"Paperclip agent ID\"\n value={mediaConfig.briefAgentId}\n >\n Agent ID that processes media intake briefs. Leave empty to disable the dedicated Brief Agent intake flow.\n </TextField>\n <ArrayField\n disabled={mediaLoading || mediaSaving}\n emptyValueLabel=\"No intake chat IDs configured\"\n label=\"Brief Agent intake chat IDs\"\n newItemLabel=\"Add intake chat ID\"\n onChange={(value) => updateMediaField(\"briefAgentChatIds\", value)}\n placeholder=\"-1003800613668\"\n value={mediaConfig.briefAgentChatIds}\n >\n 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.\n </ArrayField>\n </div>\n\n {mediaMessage ? <NoticeBlock notice={mediaMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={mediaLoading || mediaSaving}\n onClick={() => {\n setMediaConfig(mediaSnapshot);\n setMediaMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: mediaLoading || mediaSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={mediaLoading || mediaSaving || !mediaDirty}\n onClick={() => {\n void handleSaveMediaConfig();\n }}\n style={{\n background: mediaLoading || mediaSaving || !mediaDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: mediaLoading || mediaSaving || !mediaDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {mediaSaving ? \"Saving...\" : \"Save media intake\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Human Escalation</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Controls where human handoff requests go and what the bot tells the original Telegram user while waiting.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <TextField\n disabled={escalationLoading || escalationSaving}\n label=\"Escalation Chat ID\"\n onChange={(value) => updateEscalationField(\"escalationChatId\", value)}\n placeholder=\"-1003800613668\"\n value={escalationConfig.escalationChatId}\n >\n Telegram chat ID where escalations are sent for human review. Leave empty to log escalations without forwarding them to Telegram.\n </TextField>\n <div style={twoColumnGridStyle}>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Escalation timeout (ms)</span>\n <input\n disabled={escalationLoading || escalationSaving}\n min={0}\n onChange={(event) => updateEscalationField(\"escalationTimeoutMs\", Number(event.currentTarget.value))}\n placeholder=\"900000\"\n style={standardInputStyle}\n type=\"number\"\n value={escalationConfig.escalationTimeoutMs}\n />\n <span style={helperTextStyle}>\n How long to wait for a human response. Default is 900000 ms, or 15 minutes.\n </span>\n </label>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Default action on timeout</span>\n <select\n disabled={escalationLoading || escalationSaving}\n onChange={(event) => updateEscalationField(\"escalationDefaultAction\", event.currentTarget.value as TelegramEscalationConfig[\"escalationDefaultAction\"])}\n style={standardInputStyle}\n value={escalationConfig.escalationDefaultAction}\n >\n <option value=\"defer\">Defer</option>\n <option value=\"auto_reply\">Auto reply</option>\n <option value=\"close\">Close</option>\n </select>\n <span style={helperTextStyle}>\n Defer does nothing, auto reply sends the suggested reply, and close ends the escalation path.\n </span>\n </label>\n </div>\n <TextAreaField\n disabled={escalationLoading || escalationSaving}\n label=\"Hold message\"\n onChange={(value) => updateEscalationField(\"escalationHoldMessage\", value)}\n placeholder=\"Let me check on that - I'll get back to you shortly.\"\n rows={3}\n value={escalationConfig.escalationHoldMessage}\n >\n Message sent to the original Telegram user when their conversation is escalated to a human.\n </TextAreaField>\n </div>\n\n {escalationMessage ? <NoticeBlock notice={escalationMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={escalationLoading || escalationSaving}\n onClick={() => {\n setEscalationConfig(escalationSnapshot);\n setEscalationMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: escalationLoading || escalationSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={escalationLoading || escalationSaving || !escalationDirty}\n onClick={() => {\n void handleSaveEscalationConfig();\n }}\n style={{\n background: escalationLoading || escalationSaving || !escalationDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: escalationLoading || escalationSaving || !escalationDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {escalationSaving ? \"Saving...\" : \"Save escalation\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Proactive Suggestions</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Controls the scheduled watch system that sends Telegram suggestions when registered watches match Paperclip activity.\n </p>\n </div>\n\n <div style={twoColumnGridStyle}>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Suggestion rate limit</span>\n <input\n disabled={proactiveLoading || proactiveSaving}\n min={0}\n onChange={(event) => updateProactiveField(\"maxSuggestionsPerHourPerCompany\", Number(event.currentTarget.value))}\n placeholder=\"10\"\n style={standardInputStyle}\n type=\"number\"\n value={proactiveConfig.maxSuggestionsPerHourPerCompany}\n />\n <span style={helperTextStyle}>\n Maximum proactive suggestions sent per company per hour. Set to 0 to suppress watch suggestions without deleting watches.\n </span>\n </label>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Watch deduplication window (ms)</span>\n <input\n disabled={proactiveLoading || proactiveSaving}\n min={0}\n onChange={(event) => updateProactiveField(\"watchDeduplicationWindowMs\", Number(event.currentTarget.value))}\n placeholder=\"86400000\"\n style={standardInputStyle}\n type=\"number\"\n value={proactiveConfig.watchDeduplicationWindowMs}\n />\n <span style={helperTextStyle}>\n Suppresses repeat suggestions for the same watch/entity pair within this window. Default is 86400000 ms, or 24 hours.\n </span>\n </label>\n </div>\n\n <div\n style={{\n background: \"#f9fafb\",\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n color: \"#4b5563\",\n display: \"grid\",\n fontSize: 13,\n gap: 4,\n padding: 12,\n }}\n >\n <strong style={{ color: \"#374151\" }}>Watch controls</strong>\n <span>\n 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.\n </span>\n </div>\n\n {proactiveMessage ? <NoticeBlock notice={proactiveMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={proactiveLoading || proactiveSaving}\n onClick={() => {\n setProactiveConfig(proactiveSnapshot);\n setProactiveMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: proactiveLoading || proactiveSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={proactiveLoading || proactiveSaving || !proactiveDirty}\n onClick={() => {\n void handleSaveProactiveConfig();\n }}\n style={{\n background: proactiveLoading || proactiveSaving || !proactiveDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: proactiveLoading || proactiveSaving || !proactiveDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {proactiveSaving ? \"Saving...\" : \"Save suggestions\"}\n </button>\n </div>\n </section>\n </main>\n );\n}\n\nfunction NoticeBlock({ notice }: { notice: Notice }): React.JSX.Element {\n return (\n <div\n style={{\n border: `1px solid ${notice.tone === \"success\" ? \"#99f6e4\" : \"#fecaca\"}`,\n borderRadius: 8,\n background: notice.tone === \"success\" ? \"#f0fdfa\" : \"#fef2f2\",\n color: notice.tone === \"success\" ? \"#115e59\" : \"#991b1b\",\n padding: 12,\n }}\n >\n <strong>{notice.title}</strong>\n {notice.text ? <p style={{ margin: \"6px 0 0\" }}>{notice.text}</p> : null}\n </div>\n );\n}\n\nfunction TextField({\n label,\n value,\n placeholder,\n disabled,\n children,\n onChange,\n type = \"text\",\n}: {\n label: string;\n value: string;\n placeholder: string;\n disabled: boolean;\n children: React.ReactNode;\n onChange(value: string): void;\n type?: \"text\" | \"password\";\n}): React.JSX.Element {\n return (\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>{label}</span>\n <input\n autoComplete={type === \"password\" ? \"off\" : undefined}\n disabled={disabled}\n onChange={(event) => onChange(event.currentTarget.value)}\n placeholder={placeholder}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type={type}\n value={value}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>{children}</span>\n </label>\n );\n}\n\nfunction TextAreaField({\n label,\n value,\n placeholder,\n rows = 3,\n disabled,\n children,\n onChange,\n}: {\n label: string;\n value: string;\n placeholder: string;\n rows?: number;\n disabled: boolean;\n children: React.ReactNode;\n onChange(value: string): void;\n}): React.JSX.Element {\n return (\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>{label}</span>\n <textarea\n disabled={disabled}\n onChange={(event) => onChange(event.currentTarget.value)}\n placeholder={placeholder}\n rows={rows}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n resize: \"vertical\",\n }}\n value={value}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>{children}</span>\n </label>\n );\n}\n\nfunction ArrayField({\n label,\n value,\n placeholder,\n disabled,\n emptyValueLabel,\n newItemLabel,\n children,\n onChange,\n}: {\n label: string;\n value: string[];\n placeholder: string;\n disabled: boolean;\n emptyValueLabel: string;\n newItemLabel: string;\n children: React.ReactNode;\n onChange(value: string[]): void;\n}): React.JSX.Element {\n function updateItem(index: number, nextValue: string): void {\n const next = [...value];\n next[index] = nextValue;\n onChange(next);\n }\n\n function removeItem(index: number): void {\n onChange(value.filter((_, itemIndex) => itemIndex !== index));\n }\n\n function addItem(): void {\n onChange([...value, \"\"]);\n }\n\n return (\n <div style={{ display: \"grid\", gap: 7 }}>\n <div style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>{label}</div>\n <div style={{ display: \"grid\", gap: 8 }}>\n {value.length === 0 ? (\n <div\n style={{\n border: \"1px dashed #d1d5db\",\n borderRadius: 8,\n color: \"#6b7280\",\n fontSize: 13,\n padding: \"9px 10px\",\n }}\n >\n {emptyValueLabel}\n </div>\n ) : null}\n {value.map((item, index) => (\n <div key={index} style={{ alignItems: \"center\", display: \"grid\", gap: 8, gridTemplateColumns: \"minmax(0, 1fr) auto\" }}>\n <input\n disabled={disabled}\n onBlur={() => {\n const cleaned = value.map((entry) => entry.trim()).filter(Boolean);\n if (JSON.stringify(cleaned) !== JSON.stringify(value)) {\n onChange(cleaned);\n }\n }}\n onChange={(event) => updateItem(index, event.currentTarget.value)}\n placeholder={placeholder}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={item}\n />\n <button\n disabled={disabled}\n onClick={() => removeItem(index)}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"9px 12px\",\n }}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </div>\n <button\n disabled={disabled}\n onClick={addItem}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n justifySelf: \"start\",\n padding: \"9px 12px\",\n }}\n type=\"button\"\n >\n {newItemLabel}\n </button>\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>{children}</span>\n </div>\n );\n}\n\nfunction CheckboxField({\n label,\n checked,\n disabled,\n children,\n onChange,\n}: {\n label: string;\n checked: boolean;\n disabled: boolean;\n children: React.ReactNode;\n onChange(value: boolean): void;\n}): React.JSX.Element {\n return (\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={checked}\n disabled={disabled}\n onChange={(event) => onChange(event.currentTarget.checked)}\n type=\"checkbox\"\n />\n {label}\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>{children}</span>\n </label>\n );\n}\n\nfunction RoutingRow({\n title,\n chatId,\n topicId,\n chatPlaceholder,\n topicPlaceholder,\n chatHelp,\n disabled,\n children,\n footer,\n onChatIdChange,\n onTopicIdChange,\n}: {\n title: string;\n chatId: string;\n topicId: string;\n chatPlaceholder: string;\n topicPlaceholder: string;\n chatHelp: string;\n disabled: boolean;\n children?: React.ReactNode;\n footer?: React.ReactNode;\n onChatIdChange(value: string): void;\n onTopicIdChange(value: string): void;\n}): React.JSX.Element {\n return (\n <div\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 10,\n padding: 12,\n }}\n >\n <div style={{ alignItems: \"center\", display: \"flex\", gap: 12, justifyContent: \"space-between\" }}>\n <strong>{title}</strong>\n {children ? <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 12 }}>{children}</div> : null}\n </div>\n <div style={{ display: \"grid\", gap: 10 }}>\n <div style={twoColumnGridStyle}>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Chat ID</span>\n <input\n disabled={disabled}\n onChange={(event) => onChatIdChange(event.currentTarget.value)}\n placeholder={chatPlaceholder}\n style={standardInputStyle}\n type=\"text\"\n value={chatId}\n />\n <span style={helperTextStyle}>{chatHelp}</span>\n </label>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Topic ID</span>\n <input\n disabled={disabled}\n onChange={(event) => onTopicIdChange(event.currentTarget.value)}\n placeholder={topicPlaceholder}\n style={standardInputStyle}\n type=\"text\"\n value={topicId}\n />\n <span style={helperTextStyle}>\n Optional. Used only when the Chat ID points to a Telegram forum group.\n </span>\n </label>\n </div>\n </div>\n {footer ? <div style={{ display: \"grid\", gap: 10 }}>{footer}</div> : null}\n </div>\n );\n}\n", "export const PLUGIN_ID = \"paperclip-plugin-telegram-enhanced\";\nexport const PLUGIN_VERSION = \"0.3.0\";\nexport const MAX_AGENTS_PER_THREAD = 5;\n\nexport const DEFAULT_CONFIG = {\n telegramBotTokenRef: \"\",\n defaultChatId: \"\",\n approvalsChatId: \"\",\n approvalsTopicId: \"\",\n errorsChatId: \"\",\n errorsTopicId: \"\",\n digestChatId: \"\",\n digestTopicId: \"\",\n paperclipBaseUrl: \"http://localhost:3100\",\n paperclipBoardApiTokenRef: \"\",\n paperclipPublicUrl: \"\",\n notifyOnIssueCreated: true,\n notifyOnIssueDone: true,\n notifyOnIssueAssigned: false,\n onlyNotifyIfAssignedTo: \"\",\n notifyOnApprovalCreated: true,\n onlyNotifyBoardApprovals: false,\n notifyOnAgentError: true,\n notifyOnAgentRunStarted: false,\n notifyOnAgentRunFinished: false,\n enableCommands: true,\n enableInbound: true,\n allowedTelegramUserIds: [] as string[],\n allowedTelegramChatIds: [] as string[],\n // Project-key file routing (ant013 TEL-23): route outbound markdown documents\n // to a Telegram chat/topic by Paperclip project key.\n fileRoutes: [] as Array<{\n name: string;\n enabled: boolean;\n projectKey: string;\n chatId: string;\n topicId?: string;\n }>,\n // TEL-23 (ant013): route run-lifecycle / \"ops\" chatter to dedicated ops chats\n // per company, keeping the primary chat reserved for important signals.\n opsRoutes: [] as Array<{\n name: string;\n enabled: boolean;\n companyId?: string;\n companyName?: string;\n chatId: string;\n topicId?: string;\n }>,\n digestMode: \"off\" as \"off\" | \"daily\" | \"bidaily\" | \"tridaily\",\n dailyDigestTime: \"09:00\",\n bidailySecondTime: \"17:00\",\n tridailyTimes: \"07:00,13:00,19:00\",\n topicRouting: false,\n maxAgentsPerThread: MAX_AGENTS_PER_THREAD,\n escalationChatId: \"\",\n escalationTimeoutMs: 900000,\n escalationDefaultAction: \"defer\",\n escalationHoldMessage: \"Let me check on that - I'll get back to you shortly.\",\n // Phase 3: Media Pipeline\n briefAgentId: \"\",\n briefAgentChatIds: [] as string[],\n transcriptionApiKeyRef: \"\",\n // Phase 5: Proactive Suggestions\n maxSuggestionsPerHourPerCompany: 10,\n watchDeduplicationWindowMs: 86400000, // 24h\n} as const;\n\nexport const AGENT_ERROR_DEDUPLICATION_WINDOW_MS = 30 * 60 * 1000;\n\nexport const MAX_CONVERSATION_TURNS = 50;\nexport const DEFAULT_CONVERSATION_TURNS = 10;\n\nexport const METRIC_NAMES = {\n sent: \"telegram_notifications_sent\",\n failed: \"telegram_notification_failures\",\n commandsHandled: \"telegram_commands_handled\",\n inboundRouted: \"telegram_inbound_routed\",\n escalationsCreated: \"telegram_escalations_created\",\n escalationsResolved: \"telegram_escalations_resolved\",\n escalationsTimedOut: \"telegram_escalations_timed_out\",\n mediaProcessed: \"telegram_media_processed\",\n commandsExecuted: \"telegram_custom_commands_executed\",\n suggestionsEmitted: \"telegram_suggestions_emitted\",\n} as const;\n\n// Cross-plugin ACP event names\nexport const ACP_SPAWN_EVENT = \"acp-spawn\";\nexport const ACP_OUTPUT_EVENT = \"plugin.paperclip-plugin-acp.output\";\n"],
|
|
4
|
+
"sourcesContent": ["import { useEffect, useState } from \"react\";\nimport {\n usePluginAction,\n usePluginData,\n type PluginSettingsPageProps,\n} from \"@paperclipai/plugin-sdk/ui\";\nimport { PLUGIN_ID } from \"../constants.js\";\n\ntype BoardAccessRegistration = {\n configured: boolean;\n paperclipBoardApiTokenRef: string | null;\n identity: string | null;\n companyId: string | null;\n updatedAt: string | null;\n};\n\ntype CliAuthChallengeResponse = {\n token?: string;\n boardApiToken?: string;\n approvalUrl?: string;\n approvalPath?: string;\n pollUrl?: string;\n pollPath?: string;\n expiresAt?: string;\n suggestedPollIntervalMs?: number;\n};\n\ntype CliAuthChallengePollResponse = {\n status?: string;\n boardApiToken?: string;\n};\n\ntype CliAuthIdentityResponse = {\n user?: {\n displayName?: string | null;\n name?: string | null;\n login?: string | null;\n email?: string | null;\n } | null;\n displayName?: string | null;\n name?: string | null;\n login?: string | null;\n email?: string | null;\n};\n\ntype Notice = {\n tone: \"success\" | \"error\";\n title: string;\n text?: string;\n};\n\ntype TelegramRoutingConfig = {\n defaultChatId: string;\n topicRouting: boolean;\n maxAgentsPerThread: number;\n notifyOnIssueCreated: boolean;\n notifyOnIssueDone: boolean;\n notifyOnIssueAssigned: boolean;\n onlyNotifyIfAssignedTo: string;\n notifyOnIssueBlocked: boolean;\n notifyOnBoardMention: boolean;\n boardUsernames: string;\n approvalsChatId: string;\n approvalsTopicId: string;\n notifyOnApprovalCreated: boolean;\n onlyNotifyBoardApprovals: boolean;\n errorsChatId: string;\n errorsTopicId: string;\n notifyOnAgentError: boolean;\n notifyOnAgentRunStarted: boolean;\n notifyOnAgentRunFinished: boolean;\n digestChatId: string;\n digestTopicId: string;\n digestMode: \"off\" | \"daily\" | \"bidaily\" | \"tridaily\";\n dailyDigestTime: string;\n bidailySecondTime: string;\n tridailyTimes: string;\n opsRoutes: TelegramOpsRouteForm[];\n};\n\n// TEL-23: per-company ops route. Run-lifecycle notifications for a matching\n// company are diverted to this chat/topic instead of the primary chat.\ntype TelegramOpsRouteForm = {\n name: string;\n enabled: boolean;\n companyId: string;\n companyName: string;\n chatId: string;\n topicId: string;\n};\n\ntype TelegramConnectionConfig = {\n telegramBotTokenRef: string;\n paperclipBaseUrl: string;\n paperclipPublicUrl: string;\n};\n\n// ODIAA-720: masked status of the instance-wide bot connection. The raw token\n// is never returned to the frontend \u2014 only whether a bot is connected and, for\n// the instance-state source, the bot identity reported by Telegram getMe.\ntype BotConnectionRegistration = {\n configured: boolean;\n source: \"instance-state\" | \"config-secret-ref\" | null;\n botUsername: string | null;\n botId: string | null;\n updatedAt: string | null;\n};\n\ntype TelegramBoardConfig = {\n paperclipBoardApiTokenRef: string;\n};\n\ntype TelegramAccessConfig = {\n enableCommands: boolean;\n enableInbound: boolean;\n allowedTelegramUserIds: string[];\n allowedTelegramChatIds: string[];\n};\n\ntype TelegramMediaConfig = {\n transcriptionApiKeyRef: string;\n briefAgentId: string;\n briefAgentChatIds: string[];\n};\n\ntype TelegramEscalationConfig = {\n escalationChatId: string;\n escalationTimeoutMs: number;\n escalationDefaultAction: \"defer\" | \"auto_reply\" | \"close\";\n escalationHoldMessage: string;\n};\n\ntype TelegramProactiveConfig = {\n maxSuggestionsPerHourPerCompany: number;\n watchDeduplicationWindowMs: number;\n};\n\ntype PluginConfigResponse = {\n configJson?: Record<string, unknown> | null;\n} | null;\n\nconst TELEGRAM_PLUGIN_ID = PLUGIN_ID;\n\nconst DEFAULT_ROUTING_CONFIG: TelegramRoutingConfig = {\n defaultChatId: \"\",\n topicRouting: false,\n maxAgentsPerThread: 5,\n notifyOnIssueCreated: true,\n notifyOnIssueDone: true,\n notifyOnIssueAssigned: false,\n onlyNotifyIfAssignedTo: \"\",\n notifyOnIssueBlocked: false,\n notifyOnBoardMention: false,\n boardUsernames: \"\",\n approvalsChatId: \"\",\n approvalsTopicId: \"\",\n notifyOnApprovalCreated: true,\n onlyNotifyBoardApprovals: false,\n errorsChatId: \"\",\n errorsTopicId: \"\",\n notifyOnAgentError: true,\n notifyOnAgentRunStarted: false,\n notifyOnAgentRunFinished: false,\n digestChatId: \"\",\n digestTopicId: \"\",\n digestMode: \"off\",\n dailyDigestTime: \"09:00\",\n bidailySecondTime: \"17:00\",\n tridailyTimes: \"07:00,13:00,19:00\",\n opsRoutes: [],\n};\n\nconst DEFAULT_CONNECTION_CONFIG: TelegramConnectionConfig = {\n telegramBotTokenRef: \"\",\n paperclipBaseUrl: \"http://localhost:3100\",\n paperclipPublicUrl: \"\",\n};\n\nconst DEFAULT_BOARD_CONFIG: TelegramBoardConfig = {\n paperclipBoardApiTokenRef: \"\",\n};\n\nconst DEFAULT_ACCESS_CONFIG: TelegramAccessConfig = {\n enableCommands: true,\n enableInbound: true,\n allowedTelegramUserIds: [],\n allowedTelegramChatIds: [],\n};\n\nconst DEFAULT_MEDIA_CONFIG: TelegramMediaConfig = {\n transcriptionApiKeyRef: \"\",\n briefAgentId: \"\",\n briefAgentChatIds: [],\n};\n\nconst DEFAULT_ESCALATION_CONFIG: TelegramEscalationConfig = {\n escalationChatId: \"\",\n escalationTimeoutMs: 900000,\n escalationDefaultAction: \"defer\",\n escalationHoldMessage: \"Let me check on that - I'll get back to you shortly.\",\n};\n\nconst DEFAULT_PROACTIVE_CONFIG: TelegramProactiveConfig = {\n maxSuggestionsPerHourPerCompany: 10,\n watchDeduplicationWindowMs: 86400000,\n};\n\nconst standardInputStyle = {\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n};\n\nconst helperTextStyle = {\n color: \"#6b7280\",\n fontSize: 12,\n lineHeight: \"16px\",\n};\n\nconst twoColumnGridStyle = {\n alignItems: \"stretch\",\n display: \"grid\",\n gap: 10,\n gridTemplateColumns: \"repeat(2, minmax(0, 1fr))\",\n};\n\nconst pairedFieldStyle = {\n display: \"grid\",\n gap: 5,\n gridTemplateRows: \"auto auto minmax(32px, auto)\",\n};\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction asString(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\nfunction asBoolean(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nfunction asNumber(value: unknown, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nfunction asStringArray(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((item): item is string => typeof item === \"string\").map((item) => item.trim()).filter(Boolean)\n : [];\n}\n\n// board usernames may be persisted as an array (worker-side) or a raw string\n// (this text field). Always render a comma-separated string for the input.\nfunction asBoardUsernamesString(value: unknown): string {\n if (Array.isArray(value)) {\n return value.filter((item): item is string => typeof item === \"string\").map((item) => item.trim()).filter(Boolean).join(\", \");\n }\n return typeof value === \"string\" ? value : \"\";\n}\n\nfunction asDigestMode(value: unknown): TelegramRoutingConfig[\"digestMode\"] {\n return value === \"daily\" || value === \"bidaily\" || value === \"tridaily\" ? value : \"off\";\n}\n\nfunction asOpsRoutes(value: unknown): TelegramOpsRouteForm[] {\n if (!Array.isArray(value)) return [];\n return value\n .filter((item): item is Record<string, unknown> =>\n typeof item === \"object\" && item !== null && !Array.isArray(item),\n )\n .map((item) => ({\n name: asString(item.name),\n enabled: asBoolean(item.enabled, true),\n companyId: asString(item.companyId),\n companyName: asString(item.companyName),\n chatId: asString(item.chatId),\n topicId: asString(item.topicId),\n }));\n}\n\n// Returns a human-readable error if the ops routes are invalid, else null.\n// Expects already-trimmed routes.\nfunction validateOpsRoutes(routes: TelegramOpsRouteForm[]): string | null {\n const seenCompanyIds = new Set<string>();\n for (const route of routes) {\n const label = route.name || route.companyName || route.companyId || \"(unnamed)\";\n if (!route.chatId) {\n return `Ops route \"${label}\" needs a Chat ID.`;\n }\n if (!route.companyId && !route.companyName) {\n return `Ops route \"${label}\" needs a Company ID or Company name to match.`;\n }\n if (route.topicId && !/^\\d+$/.test(route.topicId)) {\n return `Ops route \"${label}\" topic ID must be a numeric Telegram forum topic ID.`;\n }\n if (route.companyId) {\n if (seenCompanyIds.has(route.companyId)) {\n return `Duplicate ops route for Company ID \"${route.companyId}\".`;\n }\n seenCompanyIds.add(route.companyId);\n }\n }\n return null;\n}\n\nfunction asEscalationDefaultAction(value: unknown): TelegramEscalationConfig[\"escalationDefaultAction\"] {\n return value === \"auto_reply\" || value === \"close\" ? value : \"defer\";\n}\n\nfunction extractRoutingConfig(config: Record<string, unknown>): TelegramRoutingConfig {\n return {\n defaultChatId: asString(config.defaultChatId),\n topicRouting: asBoolean(config.topicRouting, DEFAULT_ROUTING_CONFIG.topicRouting),\n maxAgentsPerThread: asNumber(config.maxAgentsPerThread, DEFAULT_ROUTING_CONFIG.maxAgentsPerThread),\n notifyOnIssueCreated: asBoolean(\n config.notifyOnIssueCreated,\n DEFAULT_ROUTING_CONFIG.notifyOnIssueCreated,\n ),\n notifyOnIssueDone: asBoolean(\n config.notifyOnIssueDone,\n DEFAULT_ROUTING_CONFIG.notifyOnIssueDone,\n ),\n notifyOnIssueAssigned: asBoolean(\n config.notifyOnIssueAssigned,\n DEFAULT_ROUTING_CONFIG.notifyOnIssueAssigned,\n ),\n onlyNotifyIfAssignedTo: asString(config.onlyNotifyIfAssignedTo),\n notifyOnIssueBlocked: asBoolean(\n config.notifyOnIssueBlocked,\n DEFAULT_ROUTING_CONFIG.notifyOnIssueBlocked,\n ),\n notifyOnBoardMention: asBoolean(\n config.notifyOnBoardMention,\n DEFAULT_ROUTING_CONFIG.notifyOnBoardMention,\n ),\n boardUsernames: asBoardUsernamesString(config.boardUsernames),\n approvalsChatId: asString(config.approvalsChatId),\n approvalsTopicId: asString(config.approvalsTopicId),\n notifyOnApprovalCreated: asBoolean(\n config.notifyOnApprovalCreated,\n DEFAULT_ROUTING_CONFIG.notifyOnApprovalCreated,\n ),\n onlyNotifyBoardApprovals: asBoolean(\n config.onlyNotifyBoardApprovals,\n DEFAULT_ROUTING_CONFIG.onlyNotifyBoardApprovals,\n ),\n errorsChatId: asString(config.errorsChatId),\n errorsTopicId: asString(config.errorsTopicId),\n notifyOnAgentError: asBoolean(\n config.notifyOnAgentError,\n DEFAULT_ROUTING_CONFIG.notifyOnAgentError,\n ),\n notifyOnAgentRunStarted: asBoolean(\n config.notifyOnAgentRunStarted,\n DEFAULT_ROUTING_CONFIG.notifyOnAgentRunStarted,\n ),\n notifyOnAgentRunFinished: asBoolean(\n config.notifyOnAgentRunFinished,\n DEFAULT_ROUTING_CONFIG.notifyOnAgentRunFinished,\n ),\n digestChatId: asString(config.digestChatId),\n digestTopicId: asString(config.digestTopicId),\n digestMode: asDigestMode(config.digestMode),\n dailyDigestTime: asString(config.dailyDigestTime) || DEFAULT_ROUTING_CONFIG.dailyDigestTime,\n bidailySecondTime: asString(config.bidailySecondTime) || DEFAULT_ROUTING_CONFIG.bidailySecondTime,\n tridailyTimes: asString(config.tridailyTimes) || DEFAULT_ROUTING_CONFIG.tridailyTimes,\n opsRoutes: asOpsRoutes(config.opsRoutes),\n };\n}\n\nfunction extractConnectionConfig(config: Record<string, unknown>): TelegramConnectionConfig {\n return {\n telegramBotTokenRef: asString(config.telegramBotTokenRef),\n paperclipBaseUrl: asString(config.paperclipBaseUrl) || DEFAULT_CONNECTION_CONFIG.paperclipBaseUrl,\n paperclipPublicUrl: asString(config.paperclipPublicUrl),\n };\n}\n\nfunction extractBoardConfig(config: Record<string, unknown>): TelegramBoardConfig {\n return {\n paperclipBoardApiTokenRef: asString(config.paperclipBoardApiTokenRef),\n };\n}\n\nfunction extractAccessConfig(config: Record<string, unknown>): TelegramAccessConfig {\n return {\n enableCommands: asBoolean(config.enableCommands, DEFAULT_ACCESS_CONFIG.enableCommands),\n enableInbound: asBoolean(config.enableInbound, DEFAULT_ACCESS_CONFIG.enableInbound),\n allowedTelegramUserIds: asStringArray(config.allowedTelegramUserIds),\n allowedTelegramChatIds: asStringArray(config.allowedTelegramChatIds),\n };\n}\n\nfunction extractMediaConfig(config: Record<string, unknown>): TelegramMediaConfig {\n return {\n transcriptionApiKeyRef: asString(config.transcriptionApiKeyRef),\n briefAgentId: asString(config.briefAgentId),\n briefAgentChatIds: asStringArray(config.briefAgentChatIds),\n };\n}\n\nfunction extractEscalationConfig(config: Record<string, unknown>): TelegramEscalationConfig {\n return {\n escalationChatId: asString(config.escalationChatId),\n escalationTimeoutMs: asNumber(config.escalationTimeoutMs, DEFAULT_ESCALATION_CONFIG.escalationTimeoutMs),\n escalationDefaultAction: asEscalationDefaultAction(config.escalationDefaultAction),\n escalationHoldMessage: asString(config.escalationHoldMessage) || DEFAULT_ESCALATION_CONFIG.escalationHoldMessage,\n };\n}\n\nfunction extractProactiveConfig(config: Record<string, unknown>): TelegramProactiveConfig {\n return {\n maxSuggestionsPerHourPerCompany: asNumber(\n config.maxSuggestionsPerHourPerCompany,\n DEFAULT_PROACTIVE_CONFIG.maxSuggestionsPerHourPerCompany,\n ),\n watchDeduplicationWindowMs: asNumber(\n config.watchDeduplicationWindowMs,\n DEFAULT_PROACTIVE_CONFIG.watchDeduplicationWindowMs,\n ),\n };\n}\n\nasync function fetchHostJson<T>(input: string, init: RequestInit = {}): Promise<T> {\n const headers = new Headers(init.headers);\n headers.set(\"accept\", \"application/json\");\n\n if (typeof init.body === \"string\" && !headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/json\");\n }\n\n const response = await fetch(input, {\n ...init,\n headers,\n credentials: init.credentials ?? \"same-origin\",\n });\n const rawBody = await response.text();\n const normalizedBody = rawBody.trim();\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n\n if (\n contentType.includes(\"text/html\") ||\n normalizedBody.startsWith(\"<!DOCTYPE html\") ||\n normalizedBody.startsWith(\"<html\")\n ) {\n throw new Error(\"Paperclip returned HTML instead of JSON.\");\n }\n\n let payload: unknown = null;\n if (normalizedBody) {\n try {\n payload = JSON.parse(normalizedBody);\n } catch {\n throw new Error(\"Paperclip returned an unexpected response.\");\n }\n }\n\n if (!response.ok) {\n const message =\n typeof payload === \"object\" &&\n payload !== null &&\n \"error\" in payload &&\n typeof payload.error === \"string\"\n ? payload.error\n : `Request failed with status ${response.status}.`;\n throw new Error(message);\n }\n\n return payload as T;\n}\n\nfunction resolveBrowserOrigin(): string | null {\n if (typeof window === \"undefined\" || typeof window.location?.origin !== \"string\") {\n return null;\n }\n\n const origin = window.location.origin.trim();\n if (!origin || origin === \"null\") {\n return null;\n }\n\n try {\n const normalizedOrigin = new URL(origin);\n if (normalizedOrigin.protocol !== \"http:\" && normalizedOrigin.protocol !== \"https:\") {\n return null;\n }\n return normalizedOrigin.origin;\n } catch {\n return null;\n }\n}\n\nfunction buildPaperclipUrl(input: string): string | null {\n const origin = resolveBrowserOrigin();\n if (!origin || !input.trim() || input.trim().startsWith(\"//\")) {\n return null;\n }\n\n try {\n const candidate = new URL(input.trim(), origin);\n return candidate.origin === origin ? candidate.toString() : null;\n } catch {\n return null;\n }\n}\n\nfunction resolveCliAuthUrl(url?: string, path?: string): string | null {\n if (typeof url === \"string\" && url.trim()) {\n return buildPaperclipUrl(url.trim());\n }\n\n if (typeof path !== \"string\" || !path.trim()) {\n return null;\n }\n\n return buildPaperclipUrl(path.trim());\n}\n\nfunction resolveCliAuthPollUrl(urlOrPath?: string): string | null {\n if (typeof urlOrPath !== \"string\" || !urlOrPath.trim()) {\n return null;\n }\n\n const trimmed = urlOrPath.trim();\n if (/^[a-z][a-z0-9+.-]*:\\/\\//iu.test(trimmed)) {\n return buildPaperclipUrl(trimmed);\n }\n\n const normalizedPath = trimmed.startsWith(\"/api/\")\n ? trimmed\n : `/api${trimmed.startsWith(\"/\") ? \"\" : \"/\"}${trimmed}`;\n\n return buildPaperclipUrl(normalizedPath);\n}\n\nfunction normalizePollIntervalMs(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) {\n return 1500;\n }\n\n return Math.min(5000, Math.max(750, Math.floor(value)));\n}\n\nfunction waitForDuration(durationMs: number): Promise<void> {\n return new Promise((resolve) => {\n globalThis.setTimeout(resolve, durationMs);\n });\n}\n\nasync function requestBoardAccessChallenge(companyId: string): Promise<CliAuthChallengeResponse> {\n return fetchHostJson<CliAuthChallengeResponse>(\"/api/cli-auth/challenges\", {\n method: \"POST\",\n body: JSON.stringify({\n command: \"paperclip plugin telegram settings\",\n clientName: \"Telegram plugin\",\n requestedAccess: \"board\",\n requestedCompanyId: companyId,\n }),\n });\n}\n\nasync function waitForBoardAccessApproval(challenge: CliAuthChallengeResponse): Promise<string> {\n const challengeToken = typeof challenge.token === \"string\" ? challenge.token.trim() : \"\";\n const pollUrl = resolveCliAuthPollUrl(challenge.pollUrl ?? challenge.pollPath);\n if (!challengeToken || !pollUrl) {\n throw new Error(\"Paperclip did not return a trusted board access challenge.\");\n }\n\n const expiresAtTimeMs =\n typeof challenge.expiresAt === \"string\" ? Date.parse(challenge.expiresAt) : Number.NaN;\n const pollIntervalMs = normalizePollIntervalMs(challenge.suggestedPollIntervalMs);\n\n while (true) {\n const pollUrlWithToken = new URL(pollUrl);\n pollUrlWithToken.searchParams.set(\"token\", challengeToken);\n const pollResult = await fetchHostJson<CliAuthChallengePollResponse>(\n pollUrlWithToken.toString(),\n );\n const status =\n typeof pollResult.status === \"string\" ? pollResult.status.trim().toLowerCase() : \"pending\";\n\n if (status === \"approved\") {\n const boardApiToken =\n typeof pollResult.boardApiToken === \"string\" && pollResult.boardApiToken.trim()\n ? pollResult.boardApiToken.trim()\n : typeof challenge.boardApiToken === \"string\" && challenge.boardApiToken.trim()\n ? challenge.boardApiToken.trim()\n : \"\";\n if (!boardApiToken) {\n throw new Error(\"Paperclip approved board access but did not return a usable API token.\");\n }\n\n return boardApiToken;\n }\n\n if (status === \"cancelled\") {\n throw new Error(\"Board access approval was cancelled.\");\n }\n\n if (status === \"expired\") {\n throw new Error(\"Board access approval expired. Start the connection flow again.\");\n }\n\n if (Number.isFinite(expiresAtTimeMs) && Date.now() >= expiresAtTimeMs) {\n throw new Error(\"Board access approval expired. Start the connection flow again.\");\n }\n\n await waitForDuration(pollIntervalMs);\n }\n}\n\nfunction getIdentityLabel(identity: CliAuthIdentityResponse): string | null {\n const candidates = [\n identity.user?.displayName,\n identity.user?.name,\n identity.user?.login,\n identity.user?.email,\n identity.displayName,\n identity.name,\n identity.login,\n identity.email,\n ];\n\n for (const candidate of candidates) {\n if (typeof candidate === \"string\" && candidate.trim()) {\n return candidate.trim();\n }\n }\n\n return null;\n}\n\nasync function fetchBoardAccessIdentity(boardApiToken: string): Promise<string | null> {\n const identity = await fetchHostJson<CliAuthIdentityResponse>(\"/api/cli-auth/me\", {\n headers: {\n authorization: `Bearer ${boardApiToken.trim()}`,\n },\n });\n\n return getIdentityLabel(identity);\n}\n\nasync function fetchPluginConfig(): Promise<Record<string, unknown>> {\n const record = await fetchHostJson<PluginConfigResponse>(\n `/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`,\n );\n return record?.configJson && typeof record.configJson === \"object\" ? record.configJson : {};\n}\n\nasync function savePluginConfig(configJson: Record<string, unknown>): Promise<void> {\n await fetchHostJson(`/api/plugins/${encodeURIComponent(TELEGRAM_PLUGIN_ID)}/config`, {\n method: \"POST\",\n body: JSON.stringify({ configJson }),\n });\n}\n\nasync function resolveOrCreateCompanySecret(\n companyId: string,\n name: string,\n value: string,\n): Promise<{ id: string; name: string }> {\n const existingSecrets = await fetchHostJson<Array<{ id: string; name: string }>>(\n `/api/companies/${encodeURIComponent(companyId)}/secrets`,\n );\n const existing = existingSecrets.find(\n (secret) => secret.name.trim().toLowerCase() === name.trim().toLowerCase(),\n );\n\n if (existing) {\n return fetchHostJson<{ id: string; name: string }>(\n `/api/secrets/${encodeURIComponent(existing.id)}/rotate`,\n {\n method: \"POST\",\n body: JSON.stringify({ value }),\n },\n );\n }\n\n return fetchHostJson<{ id: string; name: string }>(\n `/api/companies/${encodeURIComponent(companyId)}/secrets`,\n {\n method: \"POST\",\n body: JSON.stringify({ name, value }),\n },\n );\n}\n\nexport function TelegramSettingsPage({ context }: PluginSettingsPageProps): React.JSX.Element {\n const boardAccess = usePluginData<BoardAccessRegistration>(\"board-access.read\");\n const updateBoardAccess = usePluginAction(\"board-access.update\");\n // ODIAA-720: instance-wide bot connection.\n const botConnection = usePluginData<BotConnectionRegistration>(\"telegram-connection.read\");\n const updateBotConnection = usePluginAction(\"telegram-connection.update\");\n const clearBotConnection = usePluginAction(\"telegram-connection.clear\");\n const [botTokenInput, setBotTokenInput] = useState(\"\");\n const [botConnecting, setBotConnecting] = useState(false);\n const [botConnectionMessage, setBotConnectionMessage] = useState<Notice | null>(null);\n const [connecting, setConnecting] = useState(false);\n const [notice, setNotice] = useState<Notice | null>(null);\n const [routingConfig, setRoutingConfig] = useState<TelegramRoutingConfig>(DEFAULT_ROUTING_CONFIG);\n const [routingSnapshot, setRoutingSnapshot] = useState<TelegramRoutingConfig>(DEFAULT_ROUTING_CONFIG);\n const [routingLoading, setRoutingLoading] = useState(true);\n const [routingSaving, setRoutingSaving] = useState(false);\n const [routingMessage, setRoutingMessage] = useState<Notice | null>(null);\n const [connectionConfig, setConnectionConfig] = useState<TelegramConnectionConfig>(DEFAULT_CONNECTION_CONFIG);\n const [connectionSnapshot, setConnectionSnapshot] = useState<TelegramConnectionConfig>(DEFAULT_CONNECTION_CONFIG);\n const [connectionLoading, setConnectionLoading] = useState(true);\n const [connectionSaving, setConnectionSaving] = useState(false);\n const [connectionMessage, setConnectionMessage] = useState<Notice | null>(null);\n const [boardConfig, setBoardConfig] = useState<TelegramBoardConfig>(DEFAULT_BOARD_CONFIG);\n const [boardSnapshot, setBoardSnapshot] = useState<TelegramBoardConfig>(DEFAULT_BOARD_CONFIG);\n const [boardConfigLoading, setBoardConfigLoading] = useState(true);\n const [boardConfigSaving, setBoardConfigSaving] = useState(false);\n const [boardConfigMessage, setBoardConfigMessage] = useState<Notice | null>(null);\n const [accessConfig, setAccessConfig] = useState<TelegramAccessConfig>(DEFAULT_ACCESS_CONFIG);\n const [accessSnapshot, setAccessSnapshot] = useState<TelegramAccessConfig>(DEFAULT_ACCESS_CONFIG);\n const [accessLoading, setAccessLoading] = useState(true);\n const [accessSaving, setAccessSaving] = useState(false);\n const [accessMessage, setAccessMessage] = useState<Notice | null>(null);\n const [mediaConfig, setMediaConfig] = useState<TelegramMediaConfig>(DEFAULT_MEDIA_CONFIG);\n const [mediaSnapshot, setMediaSnapshot] = useState<TelegramMediaConfig>(DEFAULT_MEDIA_CONFIG);\n const [mediaLoading, setMediaLoading] = useState(true);\n const [mediaSaving, setMediaSaving] = useState(false);\n const [mediaMessage, setMediaMessage] = useState<Notice | null>(null);\n const [escalationConfig, setEscalationConfig] = useState<TelegramEscalationConfig>(DEFAULT_ESCALATION_CONFIG);\n const [escalationSnapshot, setEscalationSnapshot] = useState<TelegramEscalationConfig>(DEFAULT_ESCALATION_CONFIG);\n const [escalationLoading, setEscalationLoading] = useState(true);\n const [escalationSaving, setEscalationSaving] = useState(false);\n const [escalationMessage, setEscalationMessage] = useState<Notice | null>(null);\n const [proactiveConfig, setProactiveConfig] = useState<TelegramProactiveConfig>(DEFAULT_PROACTIVE_CONFIG);\n const [proactiveSnapshot, setProactiveSnapshot] = useState<TelegramProactiveConfig>(DEFAULT_PROACTIVE_CONFIG);\n const [proactiveLoading, setProactiveLoading] = useState(true);\n const [proactiveSaving, setProactiveSaving] = useState(false);\n const [proactiveMessage, setProactiveMessage] = useState<Notice | null>(null);\n const companyId = context.companyId ?? \"\";\n const companyLabel = context.companyPrefix?.trim() || \"this company\";\n const configured = Boolean(boardAccess.data?.configured);\n const identity = boardAccess.data?.identity?.trim() || null;\n const routingDirty = JSON.stringify(routingConfig) !== JSON.stringify(routingSnapshot);\n const connectionDirty = JSON.stringify(connectionConfig) !== JSON.stringify(connectionSnapshot);\n const boardConfigDirty = JSON.stringify(boardConfig) !== JSON.stringify(boardSnapshot);\n const accessDirty = JSON.stringify(accessConfig) !== JSON.stringify(accessSnapshot);\n const mediaDirty = JSON.stringify(mediaConfig) !== JSON.stringify(mediaSnapshot);\n const escalationDirty = JSON.stringify(escalationConfig) !== JSON.stringify(escalationSnapshot);\n const proactiveDirty = JSON.stringify(proactiveConfig) !== JSON.stringify(proactiveSnapshot);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadRoutingConfig(): Promise<void> {\n setRoutingLoading(true);\n setRoutingMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextRoutingConfig = extractRoutingConfig(config);\n setRoutingConfig(nextRoutingConfig);\n setRoutingSnapshot(nextRoutingConfig);\n } catch (error) {\n if (!cancelled) {\n setRoutingMessage({\n tone: \"error\",\n title: \"Routing settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setRoutingLoading(false);\n }\n }\n }\n\n void loadRoutingConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadProactiveConfig(): Promise<void> {\n setProactiveLoading(true);\n setProactiveMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextProactiveConfig = extractProactiveConfig(config);\n setProactiveConfig(nextProactiveConfig);\n setProactiveSnapshot(nextProactiveConfig);\n } catch (error) {\n if (!cancelled) {\n setProactiveMessage({\n tone: \"error\",\n title: \"Proactive suggestion settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setProactiveLoading(false);\n }\n }\n }\n\n void loadProactiveConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadEscalationConfig(): Promise<void> {\n setEscalationLoading(true);\n setEscalationMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextEscalationConfig = extractEscalationConfig(config);\n setEscalationConfig(nextEscalationConfig);\n setEscalationSnapshot(nextEscalationConfig);\n } catch (error) {\n if (!cancelled) {\n setEscalationMessage({\n tone: \"error\",\n title: \"Human escalation settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setEscalationLoading(false);\n }\n }\n }\n\n void loadEscalationConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadMediaConfig(): Promise<void> {\n setMediaLoading(true);\n setMediaMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextMediaConfig = extractMediaConfig(config);\n setMediaConfig(nextMediaConfig);\n setMediaSnapshot(nextMediaConfig);\n } catch (error) {\n if (!cancelled) {\n setMediaMessage({\n tone: \"error\",\n title: \"Media intake settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setMediaLoading(false);\n }\n }\n }\n\n void loadMediaConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadAccessConfig(): Promise<void> {\n setAccessLoading(true);\n setAccessMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextAccessConfig = extractAccessConfig(config);\n setAccessConfig(nextAccessConfig);\n setAccessSnapshot(nextAccessConfig);\n } catch (error) {\n if (!cancelled) {\n setAccessMessage({\n tone: \"error\",\n title: \"Access settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setAccessLoading(false);\n }\n }\n }\n\n void loadAccessConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadConnectionConfig(): Promise<void> {\n setConnectionLoading(true);\n setConnectionMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextConnectionConfig = extractConnectionConfig(config);\n setConnectionConfig(nextConnectionConfig);\n setConnectionSnapshot(nextConnectionConfig);\n } catch (error) {\n if (!cancelled) {\n setConnectionMessage({\n tone: \"error\",\n title: \"Connection settings could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setConnectionLoading(false);\n }\n }\n }\n\n void loadConnectionConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n\n async function loadBoardConfig(): Promise<void> {\n setBoardConfigLoading(true);\n setBoardConfigMessage(null);\n try {\n const config = await fetchPluginConfig();\n if (cancelled) return;\n const nextBoardConfig = extractBoardConfig(config);\n setBoardConfig(nextBoardConfig);\n setBoardSnapshot(nextBoardConfig);\n } catch (error) {\n if (!cancelled) {\n setBoardConfigMessage({\n tone: \"error\",\n title: \"Board fallback setting could not be loaded\",\n text: getErrorMessage(error),\n });\n }\n } finally {\n if (!cancelled) {\n setBoardConfigLoading(false);\n }\n }\n }\n\n void loadBoardConfig();\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n function updateRoutingField<K extends keyof TelegramRoutingConfig>(\n key: K,\n value: TelegramRoutingConfig[K],\n ): void {\n setRoutingConfig((current) => ({ ...current, [key]: value }));\n setRoutingMessage(null);\n }\n\n function addOpsRoute(): void {\n setRoutingConfig((current) => ({\n ...current,\n opsRoutes: [\n ...current.opsRoutes,\n { name: \"\", enabled: true, companyId: \"\", companyName: \"\", chatId: \"\", topicId: \"\" },\n ],\n }));\n setRoutingMessage(null);\n }\n\n function updateOpsRoute<K extends keyof TelegramOpsRouteForm>(\n index: number,\n key: K,\n value: TelegramOpsRouteForm[K],\n ): void {\n setRoutingConfig((current) => ({\n ...current,\n opsRoutes: current.opsRoutes.map((route, i) =>\n i === index ? { ...route, [key]: value } : route,\n ),\n }));\n setRoutingMessage(null);\n }\n\n function removeOpsRoute(index: number): void {\n setRoutingConfig((current) => ({\n ...current,\n opsRoutes: current.opsRoutes.filter((_, i) => i !== index),\n }));\n setRoutingMessage(null);\n }\n\n function updateBoardField<K extends keyof TelegramBoardConfig>(\n key: K,\n value: TelegramBoardConfig[K],\n ): void {\n setBoardConfig((current) => ({ ...current, [key]: value }));\n setBoardConfigMessage(null);\n }\n\n function updateAccessField<K extends keyof TelegramAccessConfig>(\n key: K,\n value: TelegramAccessConfig[K],\n ): void {\n setAccessConfig((current) => ({ ...current, [key]: value }));\n setAccessMessage(null);\n }\n\n function updateConnectionField<K extends keyof TelegramConnectionConfig>(\n key: K,\n value: TelegramConnectionConfig[K],\n ): void {\n setConnectionConfig((current) => ({ ...current, [key]: value }));\n setConnectionMessage(null);\n }\n\n function updateMediaField<K extends keyof TelegramMediaConfig>(\n key: K,\n value: TelegramMediaConfig[K],\n ): void {\n setMediaConfig((current) => ({ ...current, [key]: value }));\n setMediaMessage(null);\n }\n\n function updateEscalationField<K extends keyof TelegramEscalationConfig>(\n key: K,\n value: TelegramEscalationConfig[K],\n ): void {\n setEscalationConfig((current) => ({ ...current, [key]: value }));\n setEscalationMessage(null);\n }\n\n function updateProactiveField<K extends keyof TelegramProactiveConfig>(\n key: K,\n value: TelegramProactiveConfig[K],\n ): void {\n setProactiveConfig((current) => ({ ...current, [key]: value }));\n setProactiveMessage(null);\n }\n\n async function handleSaveBoardConfig(): Promise<void> {\n setBoardConfigSaving(true);\n setBoardConfigMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...boardConfig };\n await savePluginConfig(nextConfig);\n setBoardSnapshot(boardConfig);\n setBoardConfigMessage({\n tone: \"success\",\n title: \"Board fallback saved\",\n text: \"The connection workflow remains preferred. This secret reference is used only as a manual fallback.\",\n });\n } catch (error) {\n setBoardConfigMessage({\n tone: \"error\",\n title: \"Board fallback could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setBoardConfigSaving(false);\n }\n }\n\n async function handleSaveAccessConfig(): Promise<void> {\n setAccessSaving(true);\n setAccessMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...accessConfig };\n await savePluginConfig(nextConfig);\n setAccessSnapshot(accessConfig);\n setAccessMessage({\n tone: \"success\",\n title: \"Bot access settings saved\",\n text: \"If the worker has already cached Telegram updates, restart the plugin if the new allowlist behavior is not picked up immediately.\",\n });\n } catch (error) {\n setAccessMessage({\n tone: \"error\",\n title: \"Bot access settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setAccessSaving(false);\n }\n }\n\n async function handleSaveRoutingConfig(): Promise<void> {\n setRoutingSaving(true);\n setRoutingMessage(null);\n try {\n // Drop blank rows the operator added but never filled in, then validate\n // the rest: every ops route needs a chat ID and a company match key\n // (companyId or companyName), and companyId must be unique across routes.\n const trimmedRoutes = routingConfig.opsRoutes.map((route) => ({\n name: route.name.trim(),\n enabled: route.enabled,\n companyId: route.companyId.trim(),\n companyName: route.companyName.trim(),\n chatId: route.chatId.trim(),\n topicId: route.topicId.trim(),\n }));\n const opsRoutes = trimmedRoutes.filter(\n (route) => route.companyId || route.companyName || route.chatId || route.name,\n );\n\n const opsRouteError = validateOpsRoutes(opsRoutes);\n if (opsRouteError) {\n setRoutingMessage({ tone: \"error\", title: \"Ops route is invalid\", text: opsRouteError });\n return;\n }\n\n const sanitizedRouting = { ...routingConfig, opsRoutes };\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...sanitizedRouting };\n await savePluginConfig(nextConfig);\n setRoutingConfig(sanitizedRouting);\n setRoutingSnapshot(sanitizedRouting);\n setRoutingMessage({\n tone: \"success\",\n title: \"Notification routing saved\",\n text: \"Refresh the page if another browser tab edited these settings at the same time.\",\n });\n } catch (error) {\n setRoutingMessage({\n tone: \"error\",\n title: \"Notification routing could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setRoutingSaving(false);\n }\n }\n\n // ODIAA-720: connect the bot instance-wide. The raw token is sent to the\n // worker action (which validates it via Telegram getMe and stores it in\n // instance-scoped state); the frontend keeps only the masked registration.\n // We then re-save the plugin config so the host reloads the worker and it\n // begins polling with the freshly connected token.\n async function handleConnectBot(): Promise<void> {\n const token = botTokenInput.trim();\n if (!token) {\n setBotConnectionMessage({ tone: \"error\", title: \"Enter a bot token first\" });\n return;\n }\n setBotConnecting(true);\n setBotConnectionMessage(null);\n try {\n const result = (await updateBotConnection({ token })) as BotConnectionRegistration;\n // Touch the plugin config so the host reloads the worker and starts polling.\n try {\n await savePluginConfig(await fetchPluginConfig());\n } catch {\n // Non-fatal: the token is connected; the worker will pick it up on its\n // next reload even if this touch fails.\n }\n setBotTokenInput(\"\");\n await botConnection.refresh?.();\n const who = result?.botUsername ? `@${result.botUsername}` : \"your bot\";\n setBotConnectionMessage({\n tone: \"success\",\n title: `Connected ${who} instance-wide`,\n text: \"The bot token is stored once for the whole instance \u2014 every company can now reach the board through this bot. No company secret required.\",\n });\n } catch (error) {\n setBotConnectionMessage({\n tone: \"error\",\n title: \"Could not connect the bot\",\n text: getErrorMessage(error),\n });\n } finally {\n setBotConnecting(false);\n }\n }\n\n async function handleDisconnectBot(): Promise<void> {\n setBotConnecting(true);\n setBotConnectionMessage(null);\n try {\n await clearBotConnection({});\n await botConnection.refresh?.();\n setBotConnectionMessage({\n tone: \"success\",\n title: \"Bot disconnected\",\n text: \"The stored instance token was cleared. The plugin will idle until a bot is reconnected.\",\n });\n } catch (error) {\n setBotConnectionMessage({\n tone: \"error\",\n title: \"Could not disconnect the bot\",\n text: getErrorMessage(error),\n });\n } finally {\n setBotConnecting(false);\n }\n }\n\n async function handleSaveConnectionConfig(): Promise<void> {\n setConnectionSaving(true);\n setConnectionMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...connectionConfig };\n await savePluginConfig(nextConfig);\n setConnectionSnapshot(connectionConfig);\n setConnectionMessage({\n tone: \"success\",\n title: \"Connection settings saved\",\n text: \"These settings control the bot token and the Paperclip URLs used by Telegram messages and approval actions.\",\n });\n } catch (error) {\n setConnectionMessage({\n tone: \"error\",\n title: \"Connection settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setConnectionSaving(false);\n }\n }\n\n async function handleSaveMediaConfig(): Promise<void> {\n setMediaSaving(true);\n setMediaMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...mediaConfig };\n await savePluginConfig(nextConfig);\n setMediaSnapshot(mediaConfig);\n setMediaMessage({\n tone: \"success\",\n title: \"Media intake settings saved\",\n text: \"Media in configured intake chats is routed to the Brief Agent. Media in other chats can still go to active topic agent sessions.\",\n });\n } catch (error) {\n setMediaMessage({\n tone: \"error\",\n title: \"Media intake settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setMediaSaving(false);\n }\n }\n\n async function handleSaveEscalationConfig(): Promise<void> {\n setEscalationSaving(true);\n setEscalationMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...escalationConfig };\n await savePluginConfig(nextConfig);\n setEscalationSnapshot(escalationConfig);\n setEscalationMessage({\n tone: \"success\",\n title: \"Human escalation settings saved\",\n text: \"Escalations are sent to the configured Telegram chat when an agent invokes the human handoff tool.\",\n });\n } catch (error) {\n setEscalationMessage({\n tone: \"error\",\n title: \"Human escalation settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setEscalationSaving(false);\n }\n }\n\n async function handleSaveProactiveConfig(): Promise<void> {\n setProactiveSaving(true);\n setProactiveMessage(null);\n try {\n const currentConfig = await fetchPluginConfig();\n const nextConfig = { ...currentConfig, ...proactiveConfig };\n await savePluginConfig(nextConfig);\n setProactiveSnapshot(proactiveConfig);\n setProactiveMessage({\n tone: \"success\",\n title: \"Proactive suggestion settings saved\",\n text: \"These limits apply when the scheduled watch job evaluates registered watches and sends Telegram suggestions.\",\n });\n } catch (error) {\n setProactiveMessage({\n tone: \"error\",\n title: \"Proactive suggestion settings could not be saved\",\n text: getErrorMessage(error),\n });\n } finally {\n setProactiveSaving(false);\n }\n }\n\n async function handleConnectBoardAccess(): Promise<void> {\n if (!companyId) {\n setNotice({\n tone: \"error\",\n title: \"Open company settings first\",\n text: \"Board access tokens are saved as company secrets, so this flow needs a company context.\",\n });\n return;\n }\n\n setConnecting(true);\n setNotice(null);\n let approvalWindow: Window | null = null;\n\n try {\n if (typeof window !== \"undefined\") {\n approvalWindow = window.open(\"about:blank\", \"_blank\");\n }\n\n const challenge = await requestBoardAccessChallenge(companyId);\n const approvalUrl = resolveCliAuthUrl(challenge.approvalUrl, challenge.approvalPath);\n if (!approvalUrl) {\n throw new Error(\"Paperclip did not return a trusted board approval URL.\");\n }\n\n if (!approvalWindow && typeof window !== \"undefined\") {\n approvalWindow = window.open(approvalUrl, \"_blank\");\n } else {\n approvalWindow?.location.replace(approvalUrl);\n }\n\n if (!approvalWindow) {\n throw new Error(\"Allow pop-ups for Paperclip, then try connecting board access again.\");\n }\n\n const boardApiToken = await waitForBoardAccessApproval(challenge);\n const nextIdentity = await fetchBoardAccessIdentity(boardApiToken);\n const secretName = `telegram_board_api_${companyId.replace(/[^a-z0-9]+/gi, \"_\").toLowerCase()}`;\n const secret = await resolveOrCreateCompanySecret(companyId, secretName, boardApiToken);\n\n await updateBoardAccess({\n companyId,\n paperclipBoardApiTokenRef: secret.id,\n identity: nextIdentity,\n });\n await boardAccess.refresh();\n\n setNotice({\n tone: \"success\",\n title: nextIdentity ? `Connected as ${nextIdentity}` : \"Board access connected\",\n text: \"Telegram approval actions can now authenticate with Paperclip.\",\n });\n } catch (error) {\n setNotice({\n tone: \"error\",\n title: \"Board access could not be connected\",\n text: getErrorMessage(error),\n });\n } finally {\n setConnecting(false);\n try {\n approvalWindow?.close();\n } catch {\n // Ignore browser close restrictions.\n }\n }\n }\n\n return (\n <main style={{ display: \"grid\", gap: 24, padding: 24, color: \"#111827\" }}>\n <section style={{ display: \"grid\", gap: 8 }}>\n <h1 style={{ fontSize: 24, lineHeight: \"32px\", margin: 0 }}>Telegram Bot</h1>\n <p style={{ color: \"#6b7280\", margin: 0, maxWidth: 760 }}>\n Configure Telegram connection, access control, notification routing, media intake, escalation, and proactive suggestion behavior.\n </p>\n </section>\n\n {notice ? (\n <div\n style={{\n border: `1px solid ${notice.tone === \"success\" ? \"#99f6e4\" : \"#fecaca\"}`,\n borderRadius: 8,\n background: notice.tone === \"success\" ? \"#f0fdfa\" : \"#fef2f2\",\n color: notice.tone === \"success\" ? \"#115e59\" : \"#991b1b\",\n padding: 14,\n }}\n >\n <strong>{notice.title}</strong>\n {notice.text ? <p style={{ margin: \"6px 0 0\" }}>{notice.text}</p> : null}\n </div>\n ) : null}\n\n {/* ODIAA-720: instance-wide bot connection. The token is configured once\n for the whole instance \u2014 no per-company secret required. */}\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Bot Connection</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Connect your Telegram bot once for the whole instance. Every company can then reach the board through this bot \u2014 no per-company secret needed. The token is validated with Telegram and stored securely server-side; it is never shown again here.\n </p>\n </div>\n\n {botConnection.loading ? (\n <p style={{ color: \"#6b7280\", margin: 0 }}>Checking bot connection\u2026</p>\n ) : botConnection.data?.configured ? (\n <div\n style={{\n background: \"#f0fdf4\",\n border: \"1px solid #bbf7d0\",\n borderRadius: 8,\n color: \"#166534\",\n display: \"grid\",\n gap: 4,\n padding: 14,\n }}\n >\n <strong>\n {botConnection.data.source === \"instance-state\"\n ? `Connected${botConnection.data.botUsername ? ` as @${botConnection.data.botUsername}` : \"\"} (instance-wide)`\n : \"Connected via legacy secret reference\"}\n </strong>\n <span style={{ fontSize: 13 }}>\n {botConnection.data.source === \"instance-state\"\n ? \"This bot serves every company on the instance.\"\n : \"Using the advanced telegramBotTokenRef secret below. Reconnect above to switch to the instance-wide token store.\"}\n </span>\n </div>\n ) : (\n <div\n style={{\n background: \"#fffbeb\",\n border: \"1px solid #fde68a\",\n borderRadius: 8,\n color: \"#92400e\",\n padding: 14,\n }}\n >\n <strong>No bot connected.</strong> Paste a bot token from @BotFather below to connect.\n </div>\n )}\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <TextField\n disabled={botConnecting}\n label=\"Telegram bot token\"\n onChange={(value) => setBotTokenInput(value)}\n placeholder=\"123456789:AA\u2026 (from @BotFather)\"\n type=\"password\"\n value={botTokenInput}\n >\n Pasted once and stored server-side for the whole instance. Leave blank to keep the current connection.\n </TextField>\n </div>\n\n {botConnectionMessage ? <NoticeBlock notice={botConnectionMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n {botConnection.data?.configured && botConnection.data.source === \"instance-state\" ? (\n <button\n disabled={botConnecting}\n onClick={() => void handleDisconnectBot()}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: botConnecting ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n >\n Disconnect\n </button>\n ) : null}\n <button\n disabled={botConnecting || !botTokenInput.trim()}\n onClick={() => void handleConnectBot()}\n style={{\n background: botConnecting || !botTokenInput.trim() ? \"#9ca3af\" : \"#111827\",\n border: \"none\",\n borderRadius: 8,\n color: \"white\",\n cursor: botConnecting || !botTokenInput.trim() ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n >\n {botConnecting ? \"Connecting\u2026\" : \"Connect bot\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Connection & URLs</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Paperclip URLs used by the Telegram worker. The bot token is configured above in <strong>Bot Connection</strong>; the secret-ref field below is an advanced fallback for legacy installs.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <TextField\n disabled={connectionLoading || connectionSaving}\n label=\"Telegram bot token secret ref (advanced / legacy)\"\n onChange={(value) => updateConnectionField(\"telegramBotTokenRef\", value)}\n placeholder=\"Secret UUID from Paperclip settings\"\n value={connectionConfig.telegramBotTokenRef}\n >\n Optional fallback. Secret UUID for your bot token. Prefer <strong>Bot Connection</strong> above \u2014 secret refs are company-scoped and are disabled on recent paperclipai master (post-#5429).\n </TextField>\n <TextField\n disabled={connectionLoading || connectionSaving}\n label=\"Paperclip API URL\"\n onChange={(value) => updateConnectionField(\"paperclipBaseUrl\", value)}\n placeholder=\"http://localhost:3100\"\n value={connectionConfig.paperclipBaseUrl}\n >\n Internal Paperclip API URL used by the plugin for actions such as approvals and comments. Keep localhost for same-server deployments.\n </TextField>\n <TextField\n disabled={connectionLoading || connectionSaving}\n label=\"Paperclip public URL\"\n onChange={(value) => updateConnectionField(\"paperclipPublicUrl\", value)}\n placeholder=\"https://paperclip.example.com\"\n value={connectionConfig.paperclipPublicUrl}\n >\n Public URL used in Telegram links. Leave empty to fall back to the API URL.\n </TextField>\n </div>\n\n {connectionMessage ? <NoticeBlock notice={connectionMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={connectionLoading || connectionSaving}\n onClick={() => {\n setConnectionConfig(connectionSnapshot);\n setConnectionMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: connectionLoading || connectionSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={connectionLoading || connectionSaving || !connectionDirty}\n onClick={() => {\n void handleSaveConnectionConfig();\n }}\n style={{\n background: connectionLoading || connectionSaving || !connectionDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: connectionLoading || connectionSaving || !connectionDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {connectionSaving ? \"Saving...\" : \"Save connection\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ alignItems: \"start\", display: \"flex\", gap: 16, justifyContent: \"space-between\" }}>\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Board Access Connection</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Telegram approval buttons need board access when Paperclip requires authenticated approval mutations.\n </p>\n </div>\n <span\n style={{\n background: configured ? \"#ccfbf1\" : \"#f3f4f6\",\n borderRadius: 999,\n color: configured ? \"#0f766e\" : \"#4b5563\",\n fontSize: 12,\n fontWeight: 700,\n padding: \"5px 10px\",\n whiteSpace: \"nowrap\",\n }}\n >\n {connecting ? \"Connecting\" : configured ? \"Connected\" : \"Not connected\"}\n </span>\n </div>\n\n <div\n style={{\n alignItems: \"center\",\n background: \"#f9fafb\",\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"flex\",\n gap: 16,\n justifyContent: \"space-between\",\n padding: 14,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <strong>\n {!companyId\n ? \"Open this page inside a company\"\n : configured\n ? identity\n ? `Connected as ${identity}`\n : `Connected for ${companyLabel}`\n : `Connect board access for ${companyLabel}`}\n </strong>\n <span style={{ color: \"#6b7280\" }}>\n {configured\n ? \"The board token is stored as a Paperclip secret; the plugin keeps only the secret reference.\"\n : \"This opens a Paperclip approval page, then saves the resulting board token as a company secret.\"}\n </span>\n </div>\n <button\n disabled={!companyId || connecting || boardAccess.loading}\n onClick={() => {\n void handleConnectBoardAccess();\n }}\n style={{\n background: !companyId || connecting || boardAccess.loading ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: !companyId || connecting || boardAccess.loading ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 190,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {connecting ? \"Waiting for approval...\" : configured ? \"Reconnect board access\" : \"Connect board access\"}\n </button>\n </div>\n\n <div style={{ borderTop: \"1px solid #e5e7eb\", display: \"grid\", gap: 12, paddingTop: 14 }}>\n <TextField\n disabled={boardConfigLoading || boardConfigSaving}\n label=\"Board API token secret ref fallback\"\n onChange={(value) => updateBoardField(\"paperclipBoardApiTokenRef\", value)}\n placeholder=\"Optional Paperclip secret UUID\"\n value={boardConfig.paperclipBoardApiTokenRef}\n >\n 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.\n </TextField>\n\n {boardConfigMessage ? <NoticeBlock notice={boardConfigMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={boardConfigLoading || boardConfigSaving}\n onClick={() => {\n setBoardConfig(boardSnapshot);\n setBoardConfigMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: boardConfigLoading || boardConfigSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={boardConfigLoading || boardConfigSaving || !boardConfigDirty}\n onClick={() => {\n void handleSaveBoardConfig();\n }}\n style={{\n background: boardConfigLoading || boardConfigSaving || !boardConfigDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: boardConfigLoading || boardConfigSaving || !boardConfigDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {boardConfigSaving ? \"Saving...\" : \"Save fallback\"}\n </button>\n </div>\n </div>\n\n {boardAccess.error ? (\n <p style={{ color: \"#991b1b\", margin: 0 }}>\n Could not read board access state: {boardAccess.error.message}\n </p>\n ) : null}\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Bot Interaction & Access Control</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Controls who can use the bot interactively. Empty allowlists are permissive; set both user and chat IDs for strict private-group access.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <CheckboxField\n checked={accessConfig.enableCommands}\n disabled={accessLoading || accessSaving}\n label=\"Enable bot commands\"\n onChange={(value) => updateAccessField(\"enableCommands\", value)}\n >\n Allow Telegram users to run commands such as /status, /issues, and /agents. Use allowlists when commands are enabled.\n </CheckboxField>\n <CheckboxField\n checked={accessConfig.enableInbound}\n disabled={accessLoading || accessSaving}\n label=\"Enable inbound replies\"\n onChange={(value) => updateAccessField(\"enableInbound\", value)}\n >\n Route Telegram replies to Paperclip issue comments when a message replies to a bot notification. Use allowlists when inbound replies are enabled.\n </CheckboxField>\n <ArrayField\n disabled={accessLoading || accessSaving}\n emptyValueLabel=\"No user IDs configured\"\n label=\"Allowed Telegram user IDs\"\n newItemLabel=\"Add user ID\"\n onChange={(value) => updateAccessField(\"allowedTelegramUserIds\", value)}\n placeholder=\"6395513943\"\n value={accessConfig.allowedTelegramUserIds}\n >\n Optional. One Telegram user ID per line. Leave empty to allow any user. Applies to commands, inbound replies, media intake, and button callbacks.\n </ArrayField>\n <ArrayField\n disabled={accessLoading || accessSaving}\n emptyValueLabel=\"No chat IDs configured\"\n label=\"Allowed Telegram chat IDs\"\n newItemLabel=\"Add chat ID\"\n onChange={(value) => updateAccessField(\"allowedTelegramChatIds\", value)}\n placeholder=\"-1003800613668\"\n value={accessConfig.allowedTelegramChatIds}\n >\n 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.\n </ArrayField>\n </div>\n\n {accessMessage ? <NoticeBlock notice={accessMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={accessLoading || accessSaving}\n onClick={() => {\n setAccessConfig(accessSnapshot);\n setAccessMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: accessLoading || accessSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={accessLoading || accessSaving || !accessDirty}\n onClick={() => {\n void handleSaveAccessConfig();\n }}\n style={{\n background: accessLoading || accessSaving || !accessDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: accessLoading || accessSaving || !accessDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {accessSaving ? \"Saving...\" : \"Save access\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Notification Routing & Forum Topics</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n 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.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 10,\n padding: 12,\n }}\n >\n <strong>Default route</strong>\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Fallback Chat ID</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"defaultChatId\", event.currentTarget.value)}\n placeholder=\"Default chat ID\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.defaultChatId}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Used when a notification type leaves its Chat ID empty and no company-specific chat is connected.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.topicRouting}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"topicRouting\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Forum topic routing\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Route project-linked notifications to Telegram forum topics mapped with /connect_topic.\n </span>\n </label>\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Max agents per forum topic</span>\n <input\n disabled={routingLoading || routingSaving}\n min={1}\n onChange={(event) => updateRoutingField(\"maxAgentsPerThread\", Number(event.currentTarget.value))}\n placeholder=\"3\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n maxWidth: 180,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"number\"\n value={routingConfig.maxAgentsPerThread}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Maximum concurrent agent sessions allowed inside one Telegram forum topic. This applies to /acp agent sessions, not notification delivery.\n </span>\n </label>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 10,\n padding: 12,\n }}\n >\n <strong>Issues</strong>\n <div style={{ display: \"grid\", gap: 10 }}>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnIssueCreated}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnIssueCreated\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Created\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send a Telegram notification when a new issue is created.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnIssueDone}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnIssueDone\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Completed\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send a Telegram notification when an issue is completed.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnIssueAssigned}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnIssueAssigned\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Assignment changes\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send a Telegram notification when an issue assignee changes.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnIssueBlocked}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnIssueBlocked\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Blocked\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Notify when an issue becomes blocked and is owned by a human/board user. Agent-only blocks are ignored to reduce noise.\n </span>\n </label>\n </div>\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Only when assigned to user ID</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"onlyNotifyIfAssignedTo\", event.currentTarget.value)}\n placeholder=\"Paperclip user ID\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.onlyNotifyIfAssignedTo}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Optional. Restricts assignment-change notifications to issues assigned to this Paperclip user.\n </span>\n </label>\n <div style={{ display: \"grid\", gap: 10 }}>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnBoardMention}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnBoardMention\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Board mentions\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Notify when an issue comment @-mentions one of the board usernames below. Matching is case-insensitive and word-boundary aware.\n </span>\n </label>\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Board usernames</span>\n <input\n disabled={routingLoading || routingSaving || !routingConfig.notifyOnBoardMention}\n onChange={(event) => updateRoutingField(\"boardUsernames\", event.currentTarget.value)}\n placeholder=\"ceo, board (comma-separated, no @)\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.boardUsernames}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Comma- or space-separated handles. A comment forwards only when it contains <code>@<handle></code> for one of these.\n </span>\n </label>\n </div>\n </section>\n\n <RoutingRow\n title=\"Approvals\"\n chatId={routingConfig.approvalsChatId}\n topicId={routingConfig.approvalsTopicId}\n chatPlaceholder=\"Approvals chat ID\"\n topicPlaceholder=\"Approvals topic ID\"\n disabled={routingLoading || routingSaving}\n onChatIdChange={(value) => updateRoutingField(\"approvalsChatId\", value)}\n onTopicIdChange={(value) => updateRoutingField(\"approvalsTopicId\", value)}\n chatHelp=\"Leave empty to use the default route for approval notifications.\"\n footer={\n <>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnApprovalCreated}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnApprovalCreated\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Enabled\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send Telegram notifications when approval requests are created.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.onlyNotifyBoardApprovals}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"onlyNotifyBoardApprovals\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Board requests only\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Ignore internal approvals and notify only when an agent requests Board approval.\n </span>\n </label>\n </>\n }\n />\n\n <RoutingRow\n title=\"Errors\"\n chatId={routingConfig.errorsChatId}\n topicId={routingConfig.errorsTopicId}\n chatPlaceholder=\"Errors chat ID\"\n topicPlaceholder=\"Errors topic ID\"\n disabled={routingLoading || routingSaving}\n onChatIdChange={(value) => updateRoutingField(\"errorsChatId\", value)}\n onTopicIdChange={(value) => updateRoutingField(\"errorsTopicId\", value)}\n chatHelp=\"Leave empty to use the default route for agent error notifications.\"\n footer={\n <>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnAgentError}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnAgentError\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Errors enabled\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n Send Telegram notifications when an agent run reports an error.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnAgentRunStarted}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnAgentRunStarted\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Run started\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n 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.\n </span>\n </label>\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={routingConfig.notifyOnAgentRunFinished}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"notifyOnAgentRunFinished\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Run finished\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>\n 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.\n </span>\n </label>\n </>\n }\n />\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 10,\n padding: 12,\n }}\n >\n <div style={{ alignItems: \"center\", display: \"flex\", justifyContent: \"space-between\" }}>\n <strong>Ops routes</strong>\n <button\n disabled={routingLoading || routingSaving}\n onClick={() => addOpsRoute()}\n style={{\n background: \"#111827\",\n border: \"none\",\n borderRadius: 8,\n color: \"#fff\",\n cursor: routingLoading || routingSaving ? \"not-allowed\" : \"pointer\",\n fontSize: 13,\n fontWeight: 600,\n padding: \"6px 12px\",\n }}\n type=\"button\"\n >\n Add ops route\n </button>\n </div>\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Divert run-lifecycle (run started / run finished) notifications for a specific company\n to a dedicated ops chat, keeping the primary chat for important signals. The first\n enabled route matching by Company ID (or Company name) wins; if none match, ops events\n fall back to the default chat.\n </span>\n {routingConfig.opsRoutes.length === 0 ? (\n <span style={{ color: \"#9ca3af\", fontSize: 12, fontStyle: \"italic\" }}>\n No ops routes configured.\n </span>\n ) : (\n <div style={{ display: \"grid\", gap: 12 }}>\n {routingConfig.opsRoutes.map((route, index) => (\n <div\n key={index}\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 8,\n padding: 10,\n }}\n >\n <div style={{ alignItems: \"center\", display: \"flex\", gap: 12, justifyContent: \"space-between\" }}>\n <label style={{ alignItems: \"center\", color: \"#374151\", display: \"flex\", fontSize: 13, gap: 8 }}>\n <input\n checked={route.enabled}\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"enabled\", event.currentTarget.checked)}\n type=\"checkbox\"\n />\n Enabled\n </label>\n <button\n disabled={routingLoading || routingSaving}\n onClick={() => removeOpsRoute(index)}\n style={{\n background: \"transparent\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#b91c1c\",\n cursor: routingLoading || routingSaving ? \"not-allowed\" : \"pointer\",\n fontSize: 12,\n fontWeight: 600,\n padding: \"4px 10px\",\n }}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Name (optional)</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"name\", event.currentTarget.value)}\n placeholder=\"e.g. Acme Ops\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.name}\n />\n </label>\n <div style={{ display: \"grid\", gap: 8, gridTemplateColumns: \"repeat(2, minmax(0, 1fr))\" }}>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Company ID</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"companyId\", event.currentTarget.value)}\n placeholder=\"Company UUID\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.companyId}\n />\n </label>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Company name</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"companyName\", event.currentTarget.value)}\n placeholder=\"Fallback match by name\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.companyName}\n />\n </label>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Chat ID</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"chatId\", event.currentTarget.value)}\n placeholder=\"Ops chat ID\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.chatId}\n />\n </label>\n <label style={{ display: \"grid\", gap: 4 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Topic ID (optional)</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateOpsRoute(index, \"topicId\", event.currentTarget.value)}\n placeholder=\"Forum topic ID\"\n style={{ border: \"1px solid #d1d5db\", borderRadius: 8, fontSize: 14, minWidth: 0, padding: \"9px 10px\" }}\n type=\"text\"\n value={route.topicId}\n />\n </label>\n </div>\n </div>\n ))}\n </div>\n )}\n </section>\n\n <RoutingRow\n title=\"Digests\"\n chatId={routingConfig.digestChatId}\n topicId={routingConfig.digestTopicId}\n chatPlaceholder=\"Digest chat ID\"\n topicPlaceholder=\"Digest topic ID\"\n disabled={routingLoading || routingSaving}\n onChatIdChange={(value) => updateRoutingField(\"digestChatId\", value)}\n onTopicIdChange={(value) => updateRoutingField(\"digestTopicId\", value)}\n chatHelp=\"Leave empty to use the company/default route for digest notifications.\"\n footer={\n <div style={{ display: \"grid\", gap: 10 }}>\n <label style={{ display: \"grid\", gap: 6 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Mode</span>\n <select\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"digestMode\", event.currentTarget.value as TelegramRoutingConfig[\"digestMode\"])}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n maxWidth: 280,\n padding: \"9px 10px\",\n }}\n value={routingConfig.digestMode}\n >\n <option value=\"off\">Off</option>\n <option value=\"daily\">Daily</option>\n <option value=\"bidaily\">Bidaily</option>\n <option value=\"tridaily\">Tridaily</option>\n </select>\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>\n Off disables digest notifications. Times are UTC.\n </span>\n </label>\n <div style={{ alignItems: \"stretch\", display: \"grid\", gap: 10, gridTemplateColumns: \"repeat(3, minmax(0, 1fr))\" }}>\n <label style={{ display: \"grid\", gap: 5, gridTemplateRows: \"auto auto minmax(32px, auto)\" }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Daily time</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"dailyDigestTime\", event.currentTarget.value)}\n placeholder=\"09:00\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.dailyDigestTime}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12, lineHeight: \"16px\" }}>\n Used for daily mode and as the first bidaily slot.\n </span>\n </label>\n <label style={{ display: \"grid\", gap: 5, gridTemplateRows: \"auto auto minmax(32px, auto)\" }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Bidaily second time</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"bidailySecondTime\", event.currentTarget.value)}\n placeholder=\"17:00\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.bidailySecondTime}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12, lineHeight: \"16px\" }}>\n Second send time when bidaily mode is selected.\n </span>\n </label>\n <label style={{ display: \"grid\", gap: 5, gridTemplateRows: \"auto auto minmax(32px, auto)\" }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Tridaily times</span>\n <input\n disabled={routingLoading || routingSaving}\n onChange={(event) => updateRoutingField(\"tridailyTimes\", event.currentTarget.value)}\n placeholder=\"07:00,13:00,19:00\"\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={routingConfig.tridailyTimes}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12, lineHeight: \"16px\" }}>\n Three comma-separated UTC times for tridaily mode.\n </span>\n </label>\n </div>\n </div>\n }\n />\n </div>\n\n {routingMessage ? <NoticeBlock notice={routingMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={routingLoading || routingSaving}\n onClick={() => {\n setRoutingConfig(routingSnapshot);\n setRoutingMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: routingLoading || routingSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={routingLoading || routingSaving || !routingDirty}\n onClick={() => {\n void handleSaveRoutingConfig();\n }}\n style={{\n background: routingLoading || routingSaving || !routingDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: routingLoading || routingSaving || !routingDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {routingSaving ? \"Saving...\" : \"Save routing\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Media Intake / Brief Agent</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Routes Telegram voice, audio, documents, and photos either to a Brief Agent intake flow or to active agent sessions inside forum topics.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <TextField\n disabled={mediaLoading || mediaSaving}\n label=\"Transcription API key secret ref\"\n onChange={(value) => updateMediaField(\"transcriptionApiKeyRef\", value)}\n placeholder=\"OpenAI API key secret UUID\"\n value={mediaConfig.transcriptionApiKeyRef}\n >\n 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.\n </TextField>\n <TextField\n disabled={mediaLoading || mediaSaving}\n label=\"Brief Agent ID\"\n onChange={(value) => updateMediaField(\"briefAgentId\", value)}\n placeholder=\"Paperclip agent ID\"\n value={mediaConfig.briefAgentId}\n >\n Agent ID that processes media intake briefs. Leave empty to disable the dedicated Brief Agent intake flow.\n </TextField>\n <ArrayField\n disabled={mediaLoading || mediaSaving}\n emptyValueLabel=\"No intake chat IDs configured\"\n label=\"Brief Agent intake chat IDs\"\n newItemLabel=\"Add intake chat ID\"\n onChange={(value) => updateMediaField(\"briefAgentChatIds\", value)}\n placeholder=\"-1003800613668\"\n value={mediaConfig.briefAgentChatIds}\n >\n 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.\n </ArrayField>\n </div>\n\n {mediaMessage ? <NoticeBlock notice={mediaMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={mediaLoading || mediaSaving}\n onClick={() => {\n setMediaConfig(mediaSnapshot);\n setMediaMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: mediaLoading || mediaSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={mediaLoading || mediaSaving || !mediaDirty}\n onClick={() => {\n void handleSaveMediaConfig();\n }}\n style={{\n background: mediaLoading || mediaSaving || !mediaDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: mediaLoading || mediaSaving || !mediaDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {mediaSaving ? \"Saving...\" : \"Save media intake\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Human Escalation</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Controls where human handoff requests go and what the bot tells the original Telegram user while waiting.\n </p>\n </div>\n\n <div style={{ display: \"grid\", gap: 12 }}>\n <TextField\n disabled={escalationLoading || escalationSaving}\n label=\"Escalation Chat ID\"\n onChange={(value) => updateEscalationField(\"escalationChatId\", value)}\n placeholder=\"-1003800613668\"\n value={escalationConfig.escalationChatId}\n >\n Telegram chat ID where escalations are sent for human review. Leave empty to log escalations without forwarding them to Telegram.\n </TextField>\n <div style={twoColumnGridStyle}>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Escalation timeout (ms)</span>\n <input\n disabled={escalationLoading || escalationSaving}\n min={0}\n onChange={(event) => updateEscalationField(\"escalationTimeoutMs\", Number(event.currentTarget.value))}\n placeholder=\"900000\"\n style={standardInputStyle}\n type=\"number\"\n value={escalationConfig.escalationTimeoutMs}\n />\n <span style={helperTextStyle}>\n How long to wait for a human response. Default is 900000 ms, or 15 minutes.\n </span>\n </label>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Default action on timeout</span>\n <select\n disabled={escalationLoading || escalationSaving}\n onChange={(event) => updateEscalationField(\"escalationDefaultAction\", event.currentTarget.value as TelegramEscalationConfig[\"escalationDefaultAction\"])}\n style={standardInputStyle}\n value={escalationConfig.escalationDefaultAction}\n >\n <option value=\"defer\">Defer</option>\n <option value=\"auto_reply\">Auto reply</option>\n <option value=\"close\">Close</option>\n </select>\n <span style={helperTextStyle}>\n Defer does nothing, auto reply sends the suggested reply, and close ends the escalation path.\n </span>\n </label>\n </div>\n <TextAreaField\n disabled={escalationLoading || escalationSaving}\n label=\"Hold message\"\n onChange={(value) => updateEscalationField(\"escalationHoldMessage\", value)}\n placeholder=\"Let me check on that - I'll get back to you shortly.\"\n rows={3}\n value={escalationConfig.escalationHoldMessage}\n >\n Message sent to the original Telegram user when their conversation is escalated to a human.\n </TextAreaField>\n </div>\n\n {escalationMessage ? <NoticeBlock notice={escalationMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={escalationLoading || escalationSaving}\n onClick={() => {\n setEscalationConfig(escalationSnapshot);\n setEscalationMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: escalationLoading || escalationSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={escalationLoading || escalationSaving || !escalationDirty}\n onClick={() => {\n void handleSaveEscalationConfig();\n }}\n style={{\n background: escalationLoading || escalationSaving || !escalationDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: escalationLoading || escalationSaving || !escalationDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {escalationSaving ? \"Saving...\" : \"Save escalation\"}\n </button>\n </div>\n </section>\n\n <section\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 18,\n padding: 18,\n }}\n >\n <div style={{ display: \"grid\", gap: 4 }}>\n <h2 style={{ fontSize: 18, fontWeight: 700, lineHeight: \"28px\", margin: 0 }}>Proactive Suggestions</h2>\n <p style={{ color: \"#6b7280\", margin: 0 }}>\n Controls the scheduled watch system that sends Telegram suggestions when registered watches match Paperclip activity.\n </p>\n </div>\n\n <div style={twoColumnGridStyle}>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Suggestion rate limit</span>\n <input\n disabled={proactiveLoading || proactiveSaving}\n min={0}\n onChange={(event) => updateProactiveField(\"maxSuggestionsPerHourPerCompany\", Number(event.currentTarget.value))}\n placeholder=\"10\"\n style={standardInputStyle}\n type=\"number\"\n value={proactiveConfig.maxSuggestionsPerHourPerCompany}\n />\n <span style={helperTextStyle}>\n Maximum proactive suggestions sent per company per hour. Set to 0 to suppress watch suggestions without deleting watches.\n </span>\n </label>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Watch deduplication window (ms)</span>\n <input\n disabled={proactiveLoading || proactiveSaving}\n min={0}\n onChange={(event) => updateProactiveField(\"watchDeduplicationWindowMs\", Number(event.currentTarget.value))}\n placeholder=\"86400000\"\n style={standardInputStyle}\n type=\"number\"\n value={proactiveConfig.watchDeduplicationWindowMs}\n />\n <span style={helperTextStyle}>\n Suppresses repeat suggestions for the same watch/entity pair within this window. Default is 86400000 ms, or 24 hours.\n </span>\n </label>\n </div>\n\n <div\n style={{\n background: \"#f9fafb\",\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n color: \"#4b5563\",\n display: \"grid\",\n fontSize: 13,\n gap: 4,\n padding: 12,\n }}\n >\n <strong style={{ color: \"#374151\" }}>Watch controls</strong>\n <span>\n 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.\n </span>\n </div>\n\n {proactiveMessage ? <NoticeBlock notice={proactiveMessage} /> : null}\n\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\" }}>\n <button\n disabled={proactiveLoading || proactiveSaving}\n onClick={() => {\n setProactiveConfig(proactiveSnapshot);\n setProactiveMessage(null);\n }}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: proactiveLoading || proactiveSaving ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n Reset\n </button>\n <button\n disabled={proactiveLoading || proactiveSaving || !proactiveDirty}\n onClick={() => {\n void handleSaveProactiveConfig();\n }}\n style={{\n background: proactiveLoading || proactiveSaving || !proactiveDirty ? \"#9ca3af\" : \"#111827\",\n border: 0,\n borderRadius: 8,\n color: \"white\",\n cursor: proactiveLoading || proactiveSaving || !proactiveDirty ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n minWidth: 160,\n padding: \"10px 14px\",\n }}\n type=\"button\"\n >\n {proactiveSaving ? \"Saving...\" : \"Save suggestions\"}\n </button>\n </div>\n </section>\n </main>\n );\n}\n\nfunction NoticeBlock({ notice }: { notice: Notice }): React.JSX.Element {\n return (\n <div\n style={{\n border: `1px solid ${notice.tone === \"success\" ? \"#99f6e4\" : \"#fecaca\"}`,\n borderRadius: 8,\n background: notice.tone === \"success\" ? \"#f0fdfa\" : \"#fef2f2\",\n color: notice.tone === \"success\" ? \"#115e59\" : \"#991b1b\",\n padding: 12,\n }}\n >\n <strong>{notice.title}</strong>\n {notice.text ? <p style={{ margin: \"6px 0 0\" }}>{notice.text}</p> : null}\n </div>\n );\n}\n\nfunction TextField({\n label,\n value,\n placeholder,\n disabled,\n children,\n onChange,\n type = \"text\",\n}: {\n label: string;\n value: string;\n placeholder: string;\n disabled: boolean;\n children: React.ReactNode;\n onChange(value: string): void;\n type?: \"text\" | \"password\";\n}): React.JSX.Element {\n return (\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>{label}</span>\n <input\n autoComplete={type === \"password\" ? \"off\" : undefined}\n disabled={disabled}\n onChange={(event) => onChange(event.currentTarget.value)}\n placeholder={placeholder}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type={type}\n value={value}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>{children}</span>\n </label>\n );\n}\n\nfunction TextAreaField({\n label,\n value,\n placeholder,\n rows = 3,\n disabled,\n children,\n onChange,\n}: {\n label: string;\n value: string;\n placeholder: string;\n rows?: number;\n disabled: boolean;\n children: React.ReactNode;\n onChange(value: string): void;\n}): React.JSX.Element {\n return (\n <label style={{ display: \"grid\", gap: 5 }}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>{label}</span>\n <textarea\n disabled={disabled}\n onChange={(event) => onChange(event.currentTarget.value)}\n placeholder={placeholder}\n rows={rows}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n resize: \"vertical\",\n }}\n value={value}\n />\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>{children}</span>\n </label>\n );\n}\n\nfunction ArrayField({\n label,\n value,\n placeholder,\n disabled,\n emptyValueLabel,\n newItemLabel,\n children,\n onChange,\n}: {\n label: string;\n value: string[];\n placeholder: string;\n disabled: boolean;\n emptyValueLabel: string;\n newItemLabel: string;\n children: React.ReactNode;\n onChange(value: string[]): void;\n}): React.JSX.Element {\n function updateItem(index: number, nextValue: string): void {\n const next = [...value];\n next[index] = nextValue;\n onChange(next);\n }\n\n function removeItem(index: number): void {\n onChange(value.filter((_, itemIndex) => itemIndex !== index));\n }\n\n function addItem(): void {\n onChange([...value, \"\"]);\n }\n\n return (\n <div style={{ display: \"grid\", gap: 7 }}>\n <div style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>{label}</div>\n <div style={{ display: \"grid\", gap: 8 }}>\n {value.length === 0 ? (\n <div\n style={{\n border: \"1px dashed #d1d5db\",\n borderRadius: 8,\n color: \"#6b7280\",\n fontSize: 13,\n padding: \"9px 10px\",\n }}\n >\n {emptyValueLabel}\n </div>\n ) : null}\n {value.map((item, index) => (\n <div key={index} style={{ alignItems: \"center\", display: \"grid\", gap: 8, gridTemplateColumns: \"minmax(0, 1fr) auto\" }}>\n <input\n disabled={disabled}\n onBlur={() => {\n const cleaned = value.map((entry) => entry.trim()).filter(Boolean);\n if (JSON.stringify(cleaned) !== JSON.stringify(value)) {\n onChange(cleaned);\n }\n }}\n onChange={(event) => updateItem(index, event.currentTarget.value)}\n placeholder={placeholder}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 14,\n minWidth: 0,\n padding: \"9px 10px\",\n }}\n type=\"text\"\n value={item}\n />\n <button\n disabled={disabled}\n onClick={() => removeItem(index)}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n padding: \"9px 12px\",\n }}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </div>\n <button\n disabled={disabled}\n onClick={addItem}\n style={{\n background: \"white\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n color: \"#374151\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n fontWeight: 700,\n justifySelf: \"start\",\n padding: \"9px 12px\",\n }}\n type=\"button\"\n >\n {newItemLabel}\n </button>\n <span style={{ color: \"#6b7280\", fontSize: 12 }}>{children}</span>\n </div>\n );\n}\n\nfunction CheckboxField({\n label,\n checked,\n disabled,\n children,\n onChange,\n}: {\n label: string;\n checked: boolean;\n disabled: boolean;\n children: React.ReactNode;\n onChange(value: boolean): void;\n}): React.JSX.Element {\n return (\n <label style={{ color: \"#374151\", display: \"grid\", gap: 3, fontSize: 13 }}>\n <span style={{ alignItems: \"center\", display: \"flex\", gap: 8 }}>\n <input\n checked={checked}\n disabled={disabled}\n onChange={(event) => onChange(event.currentTarget.checked)}\n type=\"checkbox\"\n />\n {label}\n </span>\n <span style={{ color: \"#6b7280\", fontSize: 12, marginLeft: 22 }}>{children}</span>\n </label>\n );\n}\n\nfunction RoutingRow({\n title,\n chatId,\n topicId,\n chatPlaceholder,\n topicPlaceholder,\n chatHelp,\n disabled,\n children,\n footer,\n onChatIdChange,\n onTopicIdChange,\n}: {\n title: string;\n chatId: string;\n topicId: string;\n chatPlaceholder: string;\n topicPlaceholder: string;\n chatHelp: string;\n disabled: boolean;\n children?: React.ReactNode;\n footer?: React.ReactNode;\n onChatIdChange(value: string): void;\n onTopicIdChange(value: string): void;\n}): React.JSX.Element {\n return (\n <div\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: 8,\n display: \"grid\",\n gap: 10,\n padding: 12,\n }}\n >\n <div style={{ alignItems: \"center\", display: \"flex\", gap: 12, justifyContent: \"space-between\" }}>\n <strong>{title}</strong>\n {children ? <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 12 }}>{children}</div> : null}\n </div>\n <div style={{ display: \"grid\", gap: 10 }}>\n <div style={twoColumnGridStyle}>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Chat ID</span>\n <input\n disabled={disabled}\n onChange={(event) => onChatIdChange(event.currentTarget.value)}\n placeholder={chatPlaceholder}\n style={standardInputStyle}\n type=\"text\"\n value={chatId}\n />\n <span style={helperTextStyle}>{chatHelp}</span>\n </label>\n <label style={pairedFieldStyle}>\n <span style={{ color: \"#4b5563\", fontSize: 12, fontWeight: 700 }}>Topic ID</span>\n <input\n disabled={disabled}\n onChange={(event) => onTopicIdChange(event.currentTarget.value)}\n placeholder={topicPlaceholder}\n style={standardInputStyle}\n type=\"text\"\n value={topicId}\n />\n <span style={helperTextStyle}>\n Optional. Used only when the Chat ID points to a Telegram forum group.\n </span>\n </label>\n </div>\n </div>\n {footer ? <div style={{ display: \"grid\", gap: 10 }}>{footer}</div> : null}\n </div>\n );\n}\n", "export const PLUGIN_ID = \"paperclip-plugin-telegram-enhanced\";\nexport const PLUGIN_VERSION = \"0.3.1\";\nexport const MAX_AGENTS_PER_THREAD = 5;\n\nexport const DEFAULT_CONFIG = {\n telegramBotTokenRef: \"\",\n defaultChatId: \"\",\n approvalsChatId: \"\",\n approvalsTopicId: \"\",\n errorsChatId: \"\",\n errorsTopicId: \"\",\n digestChatId: \"\",\n digestTopicId: \"\",\n paperclipBaseUrl: \"http://localhost:3100\",\n paperclipBoardApiTokenRef: \"\",\n paperclipPublicUrl: \"\",\n notifyOnIssueCreated: true,\n notifyOnIssueDone: true,\n notifyOnIssueAssigned: false,\n onlyNotifyIfAssignedTo: \"\",\n notifyOnApprovalCreated: true,\n onlyNotifyBoardApprovals: false,\n notifyOnAgentError: true,\n notifyOnAgentRunStarted: false,\n notifyOnAgentRunFinished: false,\n enableCommands: true,\n enableInbound: true,\n allowedTelegramUserIds: [] as string[],\n allowedTelegramChatIds: [] as string[],\n // Project-key file routing (ant013 TEL-23): route outbound markdown documents\n // to a Telegram chat/topic by Paperclip project key.\n fileRoutes: [] as Array<{\n name: string;\n enabled: boolean;\n projectKey: string;\n chatId: string;\n topicId?: string;\n }>,\n // TEL-23 (ant013): route run-lifecycle / \"ops\" chatter to dedicated ops chats\n // per company, keeping the primary chat reserved for important signals.\n opsRoutes: [] as Array<{\n name: string;\n enabled: boolean;\n companyId?: string;\n companyName?: string;\n chatId: string;\n topicId?: string;\n }>,\n digestMode: \"off\" as \"off\" | \"daily\" | \"bidaily\" | \"tridaily\",\n dailyDigestTime: \"09:00\",\n bidailySecondTime: \"17:00\",\n tridailyTimes: \"07:00,13:00,19:00\",\n topicRouting: false,\n maxAgentsPerThread: MAX_AGENTS_PER_THREAD,\n escalationChatId: \"\",\n escalationTimeoutMs: 900000,\n escalationDefaultAction: \"defer\",\n escalationHoldMessage: \"Let me check on that - I'll get back to you shortly.\",\n // Phase 3: Media Pipeline\n briefAgentId: \"\",\n briefAgentChatIds: [] as string[],\n transcriptionApiKeyRef: \"\",\n // Phase 5: Proactive Suggestions\n maxSuggestionsPerHourPerCompany: 10,\n watchDeduplicationWindowMs: 86400000, // 24h\n} as const;\n\nexport const AGENT_ERROR_DEDUPLICATION_WINDOW_MS = 30 * 60 * 1000;\n\nexport const MAX_CONVERSATION_TURNS = 50;\nexport const DEFAULT_CONVERSATION_TURNS = 10;\n\nexport const METRIC_NAMES = {\n sent: \"telegram_notifications_sent\",\n failed: \"telegram_notification_failures\",\n commandsHandled: \"telegram_commands_handled\",\n inboundRouted: \"telegram_inbound_routed\",\n escalationsCreated: \"telegram_escalations_created\",\n escalationsResolved: \"telegram_escalations_resolved\",\n escalationsTimedOut: \"telegram_escalations_timed_out\",\n mediaProcessed: \"telegram_media_processed\",\n commandsExecuted: \"telegram_custom_commands_executed\",\n suggestionsEmitted: \"telegram_suggestions_emitted\",\n} as const;\n\n// Cross-plugin ACP event names\nexport const ACP_SPAWN_EVENT = \"acp-spawn\";\nexport const ACP_OUTPUT_EVENT = \"plugin.paperclip-plugin-acp.output\";\n"],
|
|
5
5
|
"mappings": ";AAAA,SAAS,WAAW,gBAAgB;AACpC;AAAA,EACE;AAAA,EACA;AAAA,OAEK;;;ACLA,IAAM,YAAY;AAmElB,IAAM,sCAAsC,KAAK,KAAK;;;ADuzCvD,SA6qBQ,UA5qBN,KADF;AA7uCN,IAAM,qBAAqB;AAE3B,IAAM,yBAAgD;AAAA,EACpD,eAAe;AAAA,EACf,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,uBAAuB;AAAA,EACvB,wBAAwB;AAAA,EACxB,sBAAsB;AAAA,EACtB,sBAAsB;AAAA,EACtB,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,yBAAyB;AAAA,EACzB,0BAA0B;AAAA,EAC1B,cAAc;AAAA,EACd,eAAe;AAAA,EACf,oBAAoB;AAAA,EACpB,yBAAyB;AAAA,EACzB,0BAA0B;AAAA,EAC1B,cAAc;AAAA,EACd,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,WAAW,CAAC;AACd;AAEA,IAAM,4BAAsD;AAAA,EAC1D,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,oBAAoB;AACtB;AAEA,IAAM,uBAA4C;AAAA,EAChD,2BAA2B;AAC7B;AAEA,IAAM,wBAA8C;AAAA,EAClD,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,wBAAwB,CAAC;AAAA,EACzB,wBAAwB,CAAC;AAC3B;AAEA,IAAM,uBAA4C;AAAA,EAChD,wBAAwB;AAAA,EACxB,cAAc;AAAA,EACd,mBAAmB,CAAC;AACtB;AAEA,IAAM,4BAAsD;AAAA,EAC1D,kBAAkB;AAAA,EAClB,qBAAqB;AAAA,EACrB,yBAAyB;AAAA,EACzB,uBAAuB;AACzB;AAEA,IAAM,2BAAoD;AAAA,EACxD,iCAAiC;AAAA,EACjC,4BAA4B;AAC9B;AAEA,IAAM,qBAAqB;AAAA,EACzB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,UAAU;AAAA,EACV,UAAU;AAAA,EACV,SAAS;AACX;AAEA,IAAM,kBAAkB;AAAA,EACtB,OAAO;AAAA,EACP,UAAU;AAAA,EACV,YAAY;AACd;AAEA,IAAM,qBAAqB;AAAA,EACzB,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,KAAK;AAAA,EACL,qBAAqB;AACvB;AAEA,IAAM,mBAAmB;AAAA,EACvB,SAAS;AAAA,EACT,KAAK;AAAA,EACL,kBAAkB;AACpB;AAEA,SAAS,gBAAgB,OAAwB;AAC/C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEA,SAAS,SAAS,OAAwB;AACxC,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,UAAU,OAAgB,UAA4B;AAC7D,SAAO,OAAO,UAAU,YAAY,QAAQ;AAC9C;AAEA,SAAS,SAAS,OAAgB,UAA0B;AAC1D,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE;AAEA,SAAS,cAAc,OAA0B;AAC/C,SAAO,MAAM,QAAQ,KAAK,IACtB,MAAM,OAAO,CAAC,SAAyB,OAAO,SAAS,QAAQ,EAAE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EAAE,OAAO,OAAO,IAC1G,CAAC;AACP;AAIA,SAAS,uBAAuB,OAAwB;AACtD,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,OAAO,CAAC,SAAyB,OAAO,SAAS,QAAQ,EAAE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAAA,EAC9H;AACA,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,aAAa,OAAqD;AACzE,SAAO,UAAU,WAAW,UAAU,aAAa,UAAU,aAAa,QAAQ;AACpF;AAEA,SAAS,YAAY,OAAwC;AAC3D,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC;AACnC,SAAO,MACJ;AAAA,IAAO,CAAC,SACP,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI;AAAA,EAClE,EACC,IAAI,CAAC,UAAU;AAAA,IACd,MAAM,SAAS,KAAK,IAAI;AAAA,IACxB,SAAS,UAAU,KAAK,SAAS,IAAI;AAAA,IACrC,WAAW,SAAS,KAAK,SAAS;AAAA,IAClC,aAAa,SAAS,KAAK,WAAW;AAAA,IACtC,QAAQ,SAAS,KAAK,MAAM;AAAA,IAC5B,SAAS,SAAS,KAAK,OAAO;AAAA,EAChC,EAAE;AACN;AAIA,SAAS,kBAAkB,QAA+C;AACxE,QAAM,iBAAiB,oBAAI,IAAY;AACvC,aAAW,SAAS,QAAQ;AAC1B,UAAM,QAAQ,MAAM,QAAQ,MAAM,eAAe,MAAM,aAAa;AACpE,QAAI,CAAC,MAAM,QAAQ;AACjB,aAAO,cAAc,KAAK;AAAA,IAC5B;AACA,QAAI,CAAC,MAAM,aAAa,CAAC,MAAM,aAAa;AAC1C,aAAO,cAAc,KAAK;AAAA,IAC5B;AACA,QAAI,MAAM,WAAW,CAAC,QAAQ,KAAK,MAAM,OAAO,GAAG;AACjD,aAAO,cAAc,KAAK;AAAA,IAC5B;AACA,QAAI,MAAM,WAAW;AACnB,UAAI,eAAe,IAAI,MAAM,SAAS,GAAG;AACvC,eAAO,uCAAuC,MAAM,SAAS;AAAA,MAC/D;AACA,qBAAe,IAAI,MAAM,SAAS;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,0BAA0B,OAAqE;AACtG,SAAO,UAAU,gBAAgB,UAAU,UAAU,QAAQ;AAC/D;AAEA,SAAS,qBAAqB,QAAwD;AACpF,SAAO;AAAA,IACL,eAAe,SAAS,OAAO,aAAa;AAAA,IAC5C,cAAc,UAAU,OAAO,cAAc,uBAAuB,YAAY;AAAA,IAChF,oBAAoB,SAAS,OAAO,oBAAoB,uBAAuB,kBAAkB;AAAA,IACjG,sBAAsB;AAAA,MACpB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,mBAAmB;AAAA,MACjB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,uBAAuB;AAAA,MACrB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,wBAAwB,SAAS,OAAO,sBAAsB;AAAA,IAC9D,sBAAsB;AAAA,MACpB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,sBAAsB;AAAA,MACpB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,gBAAgB,uBAAuB,OAAO,cAAc;AAAA,IAC5D,iBAAiB,SAAS,OAAO,eAAe;AAAA,IAChD,kBAAkB,SAAS,OAAO,gBAAgB;AAAA,IAClD,yBAAyB;AAAA,MACvB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,0BAA0B;AAAA,MACxB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,cAAc,SAAS,OAAO,YAAY;AAAA,IAC1C,eAAe,SAAS,OAAO,aAAa;AAAA,IAC5C,oBAAoB;AAAA,MAClB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,yBAAyB;AAAA,MACvB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,0BAA0B;AAAA,MACxB,OAAO;AAAA,MACP,uBAAuB;AAAA,IACzB;AAAA,IACA,cAAc,SAAS,OAAO,YAAY;AAAA,IAC1C,eAAe,SAAS,OAAO,aAAa;AAAA,IAC5C,YAAY,aAAa,OAAO,UAAU;AAAA,IAC1C,iBAAiB,SAAS,OAAO,eAAe,KAAK,uBAAuB;AAAA,IAC5E,mBAAmB,SAAS,OAAO,iBAAiB,KAAK,uBAAuB;AAAA,IAChF,eAAe,SAAS,OAAO,aAAa,KAAK,uBAAuB;AAAA,IACxE,WAAW,YAAY,OAAO,SAAS;AAAA,EACzC;AACF;AAEA,SAAS,wBAAwB,QAA2D;AAC1F,SAAO;AAAA,IACL,qBAAqB,SAAS,OAAO,mBAAmB;AAAA,IACxD,kBAAkB,SAAS,OAAO,gBAAgB,KAAK,0BAA0B;AAAA,IACjF,oBAAoB,SAAS,OAAO,kBAAkB;AAAA,EACxD;AACF;AAEA,SAAS,mBAAmB,QAAsD;AAChF,SAAO;AAAA,IACL,2BAA2B,SAAS,OAAO,yBAAyB;AAAA,EACtE;AACF;AAEA,SAAS,oBAAoB,QAAuD;AAClF,SAAO;AAAA,IACL,gBAAgB,UAAU,OAAO,gBAAgB,sBAAsB,cAAc;AAAA,IACrF,eAAe,UAAU,OAAO,eAAe,sBAAsB,aAAa;AAAA,IAClF,wBAAwB,cAAc,OAAO,sBAAsB;AAAA,IACnE,wBAAwB,cAAc,OAAO,sBAAsB;AAAA,EACrE;AACF;AAEA,SAAS,mBAAmB,QAAsD;AAChF,SAAO;AAAA,IACL,wBAAwB,SAAS,OAAO,sBAAsB;AAAA,IAC9D,cAAc,SAAS,OAAO,YAAY;AAAA,IAC1C,mBAAmB,cAAc,OAAO,iBAAiB;AAAA,EAC3D;AACF;AAEA,SAAS,wBAAwB,QAA2D;AAC1F,SAAO;AAAA,IACL,kBAAkB,SAAS,OAAO,gBAAgB;AAAA,IAClD,qBAAqB,SAAS,OAAO,qBAAqB,0BAA0B,mBAAmB;AAAA,IACvG,yBAAyB,0BAA0B,OAAO,uBAAuB;AAAA,IACjF,uBAAuB,SAAS,OAAO,qBAAqB,KAAK,0BAA0B;AAAA,EAC7F;AACF;AAEA,SAAS,uBAAuB,QAA0D;AACxF,SAAO;AAAA,IACL,iCAAiC;AAAA,MAC/B,OAAO;AAAA,MACP,yBAAyB;AAAA,IAC3B;AAAA,IACA,4BAA4B;AAAA,MAC1B,OAAO;AAAA,MACP,yBAAyB;AAAA,IAC3B;AAAA,EACF;AACF;AAEA,eAAe,cAAiB,OAAe,OAAoB,CAAC,GAAe;AACjF,QAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,UAAQ,IAAI,UAAU,kBAAkB;AAExC,MAAI,OAAO,KAAK,SAAS,YAAY,CAAC,QAAQ,IAAI,cAAc,GAAG;AACjE,YAAQ,IAAI,gBAAgB,kBAAkB;AAAA,EAChD;AAEA,QAAM,WAAW,MAAM,MAAM,OAAO;AAAA,IAClC,GAAG;AAAA,IACH;AAAA,IACA,aAAa,KAAK,eAAe;AAAA,EACnC,CAAC;AACD,QAAM,UAAU,MAAM,SAAS,KAAK;AACpC,QAAM,iBAAiB,QAAQ,KAAK;AACpC,QAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAE5D,MACE,YAAY,SAAS,WAAW,KAChC,eAAe,WAAW,gBAAgB,KAC1C,eAAe,WAAW,OAAO,GACjC;AACA,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AAEA,MAAI,UAAmB;AACvB,MAAI,gBAAgB;AAClB,QAAI;AACF,gBAAU,KAAK,MAAM,cAAc;AAAA,IACrC,QAAQ;AACN,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,UACJ,OAAO,YAAY,YACnB,YAAY,QACZ,WAAW,WACX,OAAO,QAAQ,UAAU,WACrB,QAAQ,QACR,8BAA8B,SAAS,MAAM;AACnD,UAAM,IAAI,MAAM,OAAO;AAAA,EACzB;AAEA,SAAO;AACT;AAEA,SAAS,uBAAsC;AAC7C,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,UAAU,WAAW,UAAU;AAChF,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,OAAO,SAAS,OAAO,KAAK;AAC3C,MAAI,CAAC,UAAU,WAAW,QAAQ;AAChC,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,mBAAmB,IAAI,IAAI,MAAM;AACvC,QAAI,iBAAiB,aAAa,WAAW,iBAAiB,aAAa,UAAU;AACnF,aAAO;AAAA,IACT;AACA,WAAO,iBAAiB;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,OAA8B;AACvD,QAAM,SAAS,qBAAqB;AACpC,MAAI,CAAC,UAAU,CAAC,MAAM,KAAK,KAAK,MAAM,KAAK,EAAE,WAAW,IAAI,GAAG;AAC7D,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,YAAY,IAAI,IAAI,MAAM,KAAK,GAAG,MAAM;AAC9C,WAAO,UAAU,WAAW,SAAS,UAAU,SAAS,IAAI;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,KAAc,MAA8B;AACrE,MAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,GAAG;AACzC,WAAO,kBAAkB,IAAI,KAAK,CAAC;AAAA,EACrC;AAEA,MAAI,OAAO,SAAS,YAAY,CAAC,KAAK,KAAK,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,SAAO,kBAAkB,KAAK,KAAK,CAAC;AACtC;AAEA,SAAS,sBAAsB,WAAmC;AAChE,MAAI,OAAO,cAAc,YAAY,CAAC,UAAU,KAAK,GAAG;AACtD,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,UAAU,KAAK;AAC/B,MAAI,4BAA4B,KAAK,OAAO,GAAG;AAC7C,WAAO,kBAAkB,OAAO;AAAA,EAClC;AAEA,QAAM,iBAAiB,QAAQ,WAAW,OAAO,IAC7C,UACA,OAAO,QAAQ,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,OAAO;AAEvD,SAAO,kBAAkB,cAAc;AACzC;AAEA,SAAS,wBAAwB,OAAwB;AACvD,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,GAAG;AACtE,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,IAAI,KAAM,KAAK,IAAI,KAAK,KAAK,MAAM,KAAK,CAAC,CAAC;AACxD;AAEA,SAAS,gBAAgB,YAAmC;AAC1D,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,eAAW,WAAW,SAAS,UAAU;AAAA,EAC3C,CAAC;AACH;AAEA,eAAe,4BAA4B,WAAsD;AAC/F,SAAO,cAAwC,4BAA4B;AAAA,IACzE,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU;AAAA,MACnB,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,iBAAiB;AAAA,MACjB,oBAAoB;AAAA,IACtB,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAe,2BAA2B,WAAsD;AAC9F,QAAM,iBAAiB,OAAO,UAAU,UAAU,WAAW,UAAU,MAAM,KAAK,IAAI;AACtF,QAAM,UAAU,sBAAsB,UAAU,WAAW,UAAU,QAAQ;AAC7E,MAAI,CAAC,kBAAkB,CAAC,SAAS;AAC/B,UAAM,IAAI,MAAM,4DAA4D;AAAA,EAC9E;AAEA,QAAM,kBACJ,OAAO,UAAU,cAAc,WAAW,KAAK,MAAM,UAAU,SAAS,IAAI,OAAO;AACrF,QAAM,iBAAiB,wBAAwB,UAAU,uBAAuB;AAEhF,SAAO,MAAM;AACX,UAAM,mBAAmB,IAAI,IAAI,OAAO;AACxC,qBAAiB,aAAa,IAAI,SAAS,cAAc;AACzD,UAAM,aAAa,MAAM;AAAA,MACvB,iBAAiB,SAAS;AAAA,IAC5B;AACA,UAAM,SACJ,OAAO,WAAW,WAAW,WAAW,WAAW,OAAO,KAAK,EAAE,YAAY,IAAI;AAEnF,QAAI,WAAW,YAAY;AACzB,YAAM,gBACJ,OAAO,WAAW,kBAAkB,YAAY,WAAW,cAAc,KAAK,IAC1E,WAAW,cAAc,KAAK,IAC9B,OAAO,UAAU,kBAAkB,YAAY,UAAU,cAAc,KAAK,IAC1E,UAAU,cAAc,KAAK,IAC7B;AACR,UAAI,CAAC,eAAe;AAClB,cAAM,IAAI,MAAM,wEAAwE;AAAA,MAC1F;AAEA,aAAO;AAAA,IACT;AAEA,QAAI,WAAW,aAAa;AAC1B,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AAEA,QAAI,WAAW,WAAW;AACxB,YAAM,IAAI,MAAM,iEAAiE;AAAA,IACnF;AAEA,QAAI,OAAO,SAAS,eAAe,KAAK,KAAK,IAAI,KAAK,iBAAiB;AACrE,YAAM,IAAI,MAAM,iEAAiE;AAAA,IACnF;AAEA,UAAM,gBAAgB,cAAc;AAAA,EACtC;AACF;AAEA,SAAS,iBAAiB,UAAkD;AAC1E,QAAM,aAAa;AAAA,IACjB,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAEA,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,YAAY,UAAU,KAAK,GAAG;AACrD,aAAO,UAAU,KAAK;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AACT;AAEA,eAAe,yBAAyB,eAA+C;AACrF,QAAM,WAAW,MAAM,cAAuC,oBAAoB;AAAA,IAChF,SAAS;AAAA,MACP,eAAe,UAAU,cAAc,KAAK,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO,iBAAiB,QAAQ;AAClC;AAEA,eAAe,oBAAsD;AACnE,QAAM,SAAS,MAAM;AAAA,IACnB,gBAAgB,mBAAmB,kBAAkB,CAAC;AAAA,EACxD;AACA,SAAO,QAAQ,cAAc,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa,CAAC;AAC5F;AAEA,eAAe,iBAAiB,YAAoD;AAClF,QAAM,cAAc,gBAAgB,mBAAmB,kBAAkB,CAAC,WAAW;AAAA,IACnF,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,EAAE,WAAW,CAAC;AAAA,EACrC,CAAC;AACH;AAEA,eAAe,6BACb,WACA,MACA,OACuC;AACvC,QAAM,kBAAkB,MAAM;AAAA,IAC5B,kBAAkB,mBAAmB,SAAS,CAAC;AAAA,EACjD;AACA,QAAM,WAAW,gBAAgB;AAAA,IAC/B,CAAC,WAAW,OAAO,KAAK,KAAK,EAAE,YAAY,MAAM,KAAK,KAAK,EAAE,YAAY;AAAA,EAC3E;AAEA,MAAI,UAAU;AACZ,WAAO;AAAA,MACL,gBAAgB,mBAAmB,SAAS,EAAE,CAAC;AAAA,MAC/C;AAAA,QACE,QAAQ;AAAA,QACR,MAAM,KAAK,UAAU,EAAE,MAAM,CAAC;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,kBAAkB,mBAAmB,SAAS,CAAC;AAAA,IAC/C;AAAA,MACE,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAAA,IACtC;AAAA,EACF;AACF;AAEO,SAAS,qBAAqB,EAAE,QAAQ,GAA+C;AAC5F,QAAM,cAAc,cAAuC,mBAAmB;AAC9E,QAAM,oBAAoB,gBAAgB,qBAAqB;AAE/D,QAAM,gBAAgB,cAAyC,0BAA0B;AACzF,QAAM,sBAAsB,gBAAgB,4BAA4B;AACxE,QAAM,qBAAqB,gBAAgB,2BAA2B;AACtE,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,EAAE;AACrD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AACxD,QAAM,CAAC,sBAAsB,uBAAuB,IAAI,SAAwB,IAAI;AACpF,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,IAAI;AACxD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAgC,sBAAsB;AAChG,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAgC,sBAAsB;AACpG,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,IAAI;AACzD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AACxD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAwB,IAAI;AACxE,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAmC,yBAAyB;AAC5G,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAmC,yBAAyB;AAChH,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,IAAI;AAC/D,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,KAAK;AAC9D,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAwB,IAAI;AAC9E,QAAM,CAAC,aAAa,cAAc,IAAI,SAA8B,oBAAoB;AACxF,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAA8B,oBAAoB;AAC5F,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAS,IAAI;AACjE,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,KAAK;AAChE,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAwB,IAAI;AAChF,QAAM,CAAC,cAAc,eAAe,IAAI,SAA+B,qBAAqB;AAC5F,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAA+B,qBAAqB;AAChG,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,IAAI;AACvD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAwB,IAAI;AACtE,QAAM,CAAC,aAAa,cAAc,IAAI,SAA8B,oBAAoB;AACxF,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAA8B,oBAAoB;AAC5F,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,IAAI;AACrD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AACpD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAwB,IAAI;AACpE,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAmC,yBAAyB;AAC5G,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAmC,yBAAyB;AAChH,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,IAAI;AAC/D,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,KAAK;AAC9D,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAwB,IAAI;AAC9E,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAkC,wBAAwB;AACxG,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAkC,wBAAwB;AAC5G,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,IAAI;AAC7D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,KAAK;AAC5D,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAwB,IAAI;AAC5E,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,eAAe,QAAQ,eAAe,KAAK,KAAK;AACtD,QAAM,aAAa,QAAQ,YAAY,MAAM,UAAU;AACvD,QAAM,WAAW,YAAY,MAAM,UAAU,KAAK,KAAK;AACvD,QAAM,eAAe,KAAK,UAAU,aAAa,MAAM,KAAK,UAAU,eAAe;AACrF,QAAM,kBAAkB,KAAK,UAAU,gBAAgB,MAAM,KAAK,UAAU,kBAAkB;AAC9F,QAAM,mBAAmB,KAAK,UAAU,WAAW,MAAM,KAAK,UAAU,aAAa;AACrF,QAAM,cAAc,KAAK,UAAU,YAAY,MAAM,KAAK,UAAU,cAAc;AAClF,QAAM,aAAa,KAAK,UAAU,WAAW,MAAM,KAAK,UAAU,aAAa;AAC/E,QAAM,kBAAkB,KAAK,UAAU,gBAAgB,MAAM,KAAK,UAAU,kBAAkB;AAC9F,QAAM,iBAAiB,KAAK,UAAU,eAAe,MAAM,KAAK,UAAU,iBAAiB;AAE3F,YAAU,MAAM;AACd,QAAI,YAAY;AAEhB,mBAAe,oBAAmC;AAChD,wBAAkB,IAAI;AACtB,wBAAkB,IAAI;AACtB,UAAI;AACF,cAAM,SAAS,MAAM,kBAAkB;AACvC,YAAI,UAAW;AACf,cAAM,oBAAoB,qBAAqB,MAAM;AACrD,yBAAiB,iBAAiB;AAClC,2BAAmB,iBAAiB;AAAA,MACtC,SAAS,OAAO;AACd,YAAI,CAAC,WAAW;AACd,4BAAkB;AAAA,YAChB,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM,gBAAgB,KAAK;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,UAAE;AACA,YAAI,CAAC,WAAW;AACd,4BAAkB,KAAK;AAAA,QACzB;AAAA,MACF;AAAA,IACF;AAEA,SAAK,kBAAkB;AAEvB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,YAAY;AAEhB,mBAAe,sBAAqC;AAClD,0BAAoB,IAAI;AACxB,0BAAoB,IAAI;AACxB,UAAI;AACF,cAAM,SAAS,MAAM,kBAAkB;AACvC,YAAI,UAAW;AACf,cAAM,sBAAsB,uBAAuB,MAAM;AACzD,2BAAmB,mBAAmB;AACtC,6BAAqB,mBAAmB;AAAA,MAC1C,SAAS,OAAO;AACd,YAAI,CAAC,WAAW;AACd,8BAAoB;AAAA,YAClB,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM,gBAAgB,KAAK;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,UAAE;AACA,YAAI,CAAC,WAAW;AACd,8BAAoB,KAAK;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAEA,SAAK,oBAAoB;AAEzB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,YAAY;AAEhB,mBAAe,uBAAsC;AACnD,2BAAqB,IAAI;AACzB,2BAAqB,IAAI;AACzB,UAAI;AACF,cAAM,SAAS,MAAM,kBAAkB;AACvC,YAAI,UAAW;AACf,cAAM,uBAAuB,wBAAwB,MAAM;AAC3D,4BAAoB,oBAAoB;AACxC,8BAAsB,oBAAoB;AAAA,MAC5C,SAAS,OAAO;AACd,YAAI,CAAC,WAAW;AACd,+BAAqB;AAAA,YACnB,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM,gBAAgB,KAAK;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,UAAE;AACA,YAAI,CAAC,WAAW;AACd,+BAAqB,KAAK;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAEA,SAAK,qBAAqB;AAE1B,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,YAAY;AAEhB,mBAAe,kBAAiC;AAC9C,sBAAgB,IAAI;AACpB,sBAAgB,IAAI;AACpB,UAAI;AACF,cAAM,SAAS,MAAM,kBAAkB;AACvC,YAAI,UAAW;AACf,cAAM,kBAAkB,mBAAmB,MAAM;AACjD,uBAAe,eAAe;AAC9B,yBAAiB,eAAe;AAAA,MAClC,SAAS,OAAO;AACd,YAAI,CAAC,WAAW;AACd,0BAAgB;AAAA,YACd,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM,gBAAgB,KAAK;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,UAAE;AACA,YAAI,CAAC,WAAW;AACd,0BAAgB,KAAK;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAEA,SAAK,gBAAgB;AAErB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,YAAY;AAEhB,mBAAe,mBAAkC;AAC/C,uBAAiB,IAAI;AACrB,uBAAiB,IAAI;AACrB,UAAI;AACF,cAAM,SAAS,MAAM,kBAAkB;AACvC,YAAI,UAAW;AACf,cAAM,mBAAmB,oBAAoB,MAAM;AACnD,wBAAgB,gBAAgB;AAChC,0BAAkB,gBAAgB;AAAA,MACpC,SAAS,OAAO;AACd,YAAI,CAAC,WAAW;AACd,2BAAiB;AAAA,YACf,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM,gBAAgB,KAAK;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,UAAE;AACA,YAAI,CAAC,WAAW;AACd,2BAAiB,KAAK;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AAEA,SAAK,iBAAiB;AAEtB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,YAAY;AAEhB,mBAAe,uBAAsC;AACnD,2BAAqB,IAAI;AACzB,2BAAqB,IAAI;AACzB,UAAI;AACF,cAAM,SAAS,MAAM,kBAAkB;AACvC,YAAI,UAAW;AACf,cAAM,uBAAuB,wBAAwB,MAAM;AAC3D,4BAAoB,oBAAoB;AACxC,8BAAsB,oBAAoB;AAAA,MAC5C,SAAS,OAAO;AACd,YAAI,CAAC,WAAW;AACd,+BAAqB;AAAA,YACnB,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM,gBAAgB,KAAK;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,UAAE;AACA,YAAI,CAAC,WAAW;AACd,+BAAqB,KAAK;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAEA,SAAK,qBAAqB;AAE1B,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,YAAY;AAEhB,mBAAe,kBAAiC;AAC9C,4BAAsB,IAAI;AAC1B,4BAAsB,IAAI;AAC1B,UAAI;AACF,cAAM,SAAS,MAAM,kBAAkB;AACvC,YAAI,UAAW;AACf,cAAM,kBAAkB,mBAAmB,MAAM;AACjD,uBAAe,eAAe;AAC9B,yBAAiB,eAAe;AAAA,MAClC,SAAS,OAAO;AACd,YAAI,CAAC,WAAW;AACd,gCAAsB;AAAA,YACpB,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM,gBAAgB,KAAK;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,UAAE;AACA,YAAI,CAAC,WAAW;AACd,gCAAsB,KAAK;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AAEA,SAAK,gBAAgB;AAErB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,WAAS,mBACP,KACA,OACM;AACN,qBAAiB,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,EAAE;AAC5D,sBAAkB,IAAI;AAAA,EACxB;AAEA,WAAS,cAAoB;AAC3B,qBAAiB,CAAC,aAAa;AAAA,MAC7B,GAAG;AAAA,MACH,WAAW;AAAA,QACT,GAAG,QAAQ;AAAA,QACX,EAAE,MAAM,IAAI,SAAS,MAAM,WAAW,IAAI,aAAa,IAAI,QAAQ,IAAI,SAAS,GAAG;AAAA,MACrF;AAAA,IACF,EAAE;AACF,sBAAkB,IAAI;AAAA,EACxB;AAEA,WAAS,eACP,OACA,KACA,OACM;AACN,qBAAiB,CAAC,aAAa;AAAA,MAC7B,GAAG;AAAA,MACH,WAAW,QAAQ,UAAU;AAAA,QAAI,CAAC,OAAO,MACvC,MAAM,QAAQ,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,MAAM,IAAI;AAAA,MAC7C;AAAA,IACF,EAAE;AACF,sBAAkB,IAAI;AAAA,EACxB;AAEA,WAAS,eAAe,OAAqB;AAC3C,qBAAiB,CAAC,aAAa;AAAA,MAC7B,GAAG;AAAA,MACH,WAAW,QAAQ,UAAU,OAAO,CAAC,GAAG,MAAM,MAAM,KAAK;AAAA,IAC3D,EAAE;AACF,sBAAkB,IAAI;AAAA,EACxB;AAEA,WAAS,iBACP,KACA,OACM;AACN,mBAAe,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,EAAE;AAC1D,0BAAsB,IAAI;AAAA,EAC5B;AAEA,WAAS,kBACP,KACA,OACM;AACN,oBAAgB,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,EAAE;AAC3D,qBAAiB,IAAI;AAAA,EACvB;AAEA,WAAS,sBACP,KACA,OACM;AACN,wBAAoB,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,EAAE;AAC/D,yBAAqB,IAAI;AAAA,EAC3B;AAEA,WAAS,iBACP,KACA,OACM;AACN,mBAAe,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,EAAE;AAC1D,oBAAgB,IAAI;AAAA,EACtB;AAEA,WAAS,sBACP,KACA,OACM;AACN,wBAAoB,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,EAAE;AAC/D,yBAAqB,IAAI;AAAA,EAC3B;AAEA,WAAS,qBACP,KACA,OACM;AACN,uBAAmB,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,EAAE;AAC9D,wBAAoB,IAAI;AAAA,EAC1B;AAEA,iBAAe,wBAAuC;AACpD,yBAAqB,IAAI;AACzB,0BAAsB,IAAI;AAC1B,QAAI;AACF,YAAM,gBAAgB,MAAM,kBAAkB;AAC9C,YAAM,aAAa,EAAE,GAAG,eAAe,GAAG,YAAY;AACtD,YAAM,iBAAiB,UAAU;AACjC,uBAAiB,WAAW;AAC5B,4BAAsB;AAAA,QACpB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,4BAAsB;AAAA,QACpB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,2BAAqB,KAAK;AAAA,IAC5B;AAAA,EACF;AAEA,iBAAe,yBAAwC;AACrD,oBAAgB,IAAI;AACpB,qBAAiB,IAAI;AACrB,QAAI;AACF,YAAM,gBAAgB,MAAM,kBAAkB;AAC9C,YAAM,aAAa,EAAE,GAAG,eAAe,GAAG,aAAa;AACvD,YAAM,iBAAiB,UAAU;AACjC,wBAAkB,YAAY;AAC9B,uBAAiB;AAAA,QACf,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,uBAAiB;AAAA,QACf,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,sBAAgB,KAAK;AAAA,IACvB;AAAA,EACF;AAEA,iBAAe,0BAAyC;AACtD,qBAAiB,IAAI;AACrB,sBAAkB,IAAI;AACtB,QAAI;AAIF,YAAM,gBAAgB,cAAc,UAAU,IAAI,CAAC,WAAW;AAAA,QAC5D,MAAM,MAAM,KAAK,KAAK;AAAA,QACtB,SAAS,MAAM;AAAA,QACf,WAAW,MAAM,UAAU,KAAK;AAAA,QAChC,aAAa,MAAM,YAAY,KAAK;AAAA,QACpC,QAAQ,MAAM,OAAO,KAAK;AAAA,QAC1B,SAAS,MAAM,QAAQ,KAAK;AAAA,MAC9B,EAAE;AACF,YAAM,YAAY,cAAc;AAAA,QAC9B,CAAC,UAAU,MAAM,aAAa,MAAM,eAAe,MAAM,UAAU,MAAM;AAAA,MAC3E;AAEA,YAAM,gBAAgB,kBAAkB,SAAS;AACjD,UAAI,eAAe;AACjB,0BAAkB,EAAE,MAAM,SAAS,OAAO,wBAAwB,MAAM,cAAc,CAAC;AACvF;AAAA,MACF;AAEA,YAAM,mBAAmB,EAAE,GAAG,eAAe,UAAU;AACvD,YAAM,gBAAgB,MAAM,kBAAkB;AAC9C,YAAM,aAAa,EAAE,GAAG,eAAe,GAAG,iBAAiB;AAC3D,YAAM,iBAAiB,UAAU;AACjC,uBAAiB,gBAAgB;AACjC,yBAAmB,gBAAgB;AACnC,wBAAkB;AAAA,QAChB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,wBAAkB;AAAA,QAChB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF;AAOA,iBAAe,mBAAkC;AAC/C,UAAM,QAAQ,cAAc,KAAK;AACjC,QAAI,CAAC,OAAO;AACV,8BAAwB,EAAE,MAAM,SAAS,OAAO,0BAA0B,CAAC;AAC3E;AAAA,IACF;AACA,qBAAiB,IAAI;AACrB,4BAAwB,IAAI;AAC5B,QAAI;AACF,YAAM,SAAU,MAAM,oBAAoB,EAAE,MAAM,CAAC;AAEnD,UAAI;AACF,cAAM,iBAAiB,MAAM,kBAAkB,CAAC;AAAA,MAClD,QAAQ;AAAA,MAGR;AACA,uBAAiB,EAAE;AACnB,YAAM,cAAc,UAAU;AAC9B,YAAM,MAAM,QAAQ,cAAc,IAAI,OAAO,WAAW,KAAK;AAC7D,8BAAwB;AAAA,QACtB,MAAM;AAAA,QACN,OAAO,aAAa,GAAG;AAAA,QACvB,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,8BAAwB;AAAA,QACtB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF;AAEA,iBAAe,sBAAqC;AAClD,qBAAiB,IAAI;AACrB,4BAAwB,IAAI;AAC5B,QAAI;AACF,YAAM,mBAAmB,CAAC,CAAC;AAC3B,YAAM,cAAc,UAAU;AAC9B,8BAAwB;AAAA,QACtB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,8BAAwB;AAAA,QACtB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF;AAEA,iBAAe,6BAA4C;AACzD,wBAAoB,IAAI;AACxB,yBAAqB,IAAI;AACzB,QAAI;AACF,YAAM,gBAAgB,MAAM,kBAAkB;AAC9C,YAAM,aAAa,EAAE,GAAG,eAAe,GAAG,iBAAiB;AAC3D,YAAM,iBAAiB,UAAU;AACjC,4BAAsB,gBAAgB;AACtC,2BAAqB;AAAA,QACnB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,2BAAqB;AAAA,QACnB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,0BAAoB,KAAK;AAAA,IAC3B;AAAA,EACF;AAEA,iBAAe,wBAAuC;AACpD,mBAAe,IAAI;AACnB,oBAAgB,IAAI;AACpB,QAAI;AACF,YAAM,gBAAgB,MAAM,kBAAkB;AAC9C,YAAM,aAAa,EAAE,GAAG,eAAe,GAAG,YAAY;AACtD,YAAM,iBAAiB,UAAU;AACjC,uBAAiB,WAAW;AAC5B,sBAAgB;AAAA,QACd,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,sBAAgB;AAAA,QACd,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,qBAAe,KAAK;AAAA,IACtB;AAAA,EACF;AAEA,iBAAe,6BAA4C;AACzD,wBAAoB,IAAI;AACxB,yBAAqB,IAAI;AACzB,QAAI;AACF,YAAM,gBAAgB,MAAM,kBAAkB;AAC9C,YAAM,aAAa,EAAE,GAAG,eAAe,GAAG,iBAAiB;AAC3D,YAAM,iBAAiB,UAAU;AACjC,4BAAsB,gBAAgB;AACtC,2BAAqB;AAAA,QACnB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,2BAAqB;AAAA,QACnB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,0BAAoB,KAAK;AAAA,IAC3B;AAAA,EACF;AAEA,iBAAe,4BAA2C;AACxD,uBAAmB,IAAI;AACvB,wBAAoB,IAAI;AACxB,QAAI;AACF,YAAM,gBAAgB,MAAM,kBAAkB;AAC9C,YAAM,aAAa,EAAE,GAAG,eAAe,GAAG,gBAAgB;AAC1D,YAAM,iBAAiB,UAAU;AACjC,2BAAqB,eAAe;AACpC,0BAAoB;AAAA,QAClB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,0BAAoB;AAAA,QAClB,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,yBAAmB,KAAK;AAAA,IAC1B;AAAA,EACF;AAEA,iBAAe,2BAA0C;AACvD,QAAI,CAAC,WAAW;AACd,gBAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR,CAAC;AACD;AAAA,IACF;AAEA,kBAAc,IAAI;AAClB,cAAU,IAAI;AACd,QAAI,iBAAgC;AAEpC,QAAI;AACF,UAAI,OAAO,WAAW,aAAa;AACjC,yBAAiB,OAAO,KAAK,eAAe,QAAQ;AAAA,MACtD;AAEA,YAAM,YAAY,MAAM,4BAA4B,SAAS;AAC7D,YAAM,cAAc,kBAAkB,UAAU,aAAa,UAAU,YAAY;AACnF,UAAI,CAAC,aAAa;AAChB,cAAM,IAAI,MAAM,wDAAwD;AAAA,MAC1E;AAEA,UAAI,CAAC,kBAAkB,OAAO,WAAW,aAAa;AACpD,yBAAiB,OAAO,KAAK,aAAa,QAAQ;AAAA,MACpD,OAAO;AACL,wBAAgB,SAAS,QAAQ,WAAW;AAAA,MAC9C;AAEA,UAAI,CAAC,gBAAgB;AACnB,cAAM,IAAI,MAAM,sEAAsE;AAAA,MACxF;AAEA,YAAM,gBAAgB,MAAM,2BAA2B,SAAS;AAChE,YAAM,eAAe,MAAM,yBAAyB,aAAa;AACjE,YAAM,aAAa,sBAAsB,UAAU,QAAQ,gBAAgB,GAAG,EAAE,YAAY,CAAC;AAC7F,YAAM,SAAS,MAAM,6BAA6B,WAAW,YAAY,aAAa;AAEtF,YAAM,kBAAkB;AAAA,QACtB;AAAA,QACA,2BAA2B,OAAO;AAAA,QAClC,UAAU;AAAA,MACZ,CAAC;AACD,YAAM,YAAY,QAAQ;AAE1B,gBAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO,eAAe,gBAAgB,YAAY,KAAK;AAAA,QACvD,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,OAAO;AACd,gBAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM,gBAAgB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,UAAE;AACA,oBAAc,KAAK;AACnB,UAAI;AACF,wBAAgB,MAAM;AAAA,MACxB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,SACE,qBAAC,UAAK,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,SAAS,IAAI,OAAO,UAAU,GACrE;AAAA,yBAAC,aAAQ,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACxC;AAAA,0BAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,QAAQ,QAAQ,EAAE,GAAG,0BAAY;AAAA,MACxE,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,GAAG,UAAU,IAAI,GAAG,+IAE1D;AAAA,OACF;AAAA,IAEC,SACC;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ,aAAa,OAAO,SAAS,YAAY,YAAY,SAAS;AAAA,UACtE,cAAc;AAAA,UACd,YAAY,OAAO,SAAS,YAAY,YAAY;AAAA,UACpD,OAAO,OAAO,SAAS,YAAY,YAAY;AAAA,UAC/C,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,8BAAC,YAAQ,iBAAO,OAAM;AAAA,UACrB,OAAO,OAAO,oBAAC,OAAE,OAAO,EAAE,QAAQ,UAAU,GAAI,iBAAO,MAAK,IAAO;AAAA;AAAA;AAAA,IACtE,IACE;AAAA,IAIJ;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,KAAK;AAAA,UACL,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,+BAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,gCAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,KAAK,YAAY,QAAQ,QAAQ,EAAE,GAAG,4BAAc;AAAA,YAC3F,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG,qQAE3C;AAAA,aACF;AAAA,UAEC,cAAc,UACb,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG,2CAAwB,IACjE,cAAc,MAAM,aACtB;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,gBACL,YAAY;AAAA,gBACZ,QAAQ;AAAA,gBACR,cAAc;AAAA,gBACd,OAAO;AAAA,gBACP,SAAS;AAAA,gBACT,KAAK;AAAA,gBACL,SAAS;AAAA,cACX;AAAA,cAEA;AAAA,oCAAC,YACE,wBAAc,KAAK,WAAW,mBAC3B,YAAY,cAAc,KAAK,cAAc,QAAQ,cAAc,KAAK,WAAW,KAAK,EAAE,qBAC1F,yCACN;AAAA,gBACA,oBAAC,UAAK,OAAO,EAAE,UAAU,GAAG,GACzB,wBAAc,KAAK,WAAW,mBAC3B,mDACA,oHACN;AAAA;AAAA;AAAA,UACF,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,gBACL,YAAY;AAAA,gBACZ,QAAQ;AAAA,gBACR,cAAc;AAAA,gBACd,OAAO;AAAA,gBACP,SAAS;AAAA,cACX;AAAA,cAEA;AAAA,oCAAC,YAAO,+BAAiB;AAAA,gBAAS;AAAA;AAAA;AAAA,UACpC;AAAA,UAGF,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA,YAAC;AAAA;AAAA,cACC,UAAU;AAAA,cACV,OAAM;AAAA,cACN,UAAU,CAAC,UAAU,iBAAiB,KAAK;AAAA,cAC3C,aAAY;AAAA,cACZ,MAAK;AAAA,cACL,OAAO;AAAA,cACR;AAAA;AAAA,UAED,GACF;AAAA,UAEC,uBAAuB,oBAAC,eAAY,QAAQ,sBAAsB,IAAK;AAAA,UAExE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,gBAAgB,WAAW,GAChE;AAAA,0BAAc,MAAM,cAAc,cAAc,KAAK,WAAW,mBAC/D;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU;AAAA,gBACV,SAAS,MAAM,KAAK,oBAAoB;AAAA,gBACxC,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,gBAAgB,gBAAgB;AAAA,kBACxC,YAAY;AAAA,kBACZ,SAAS;AAAA,gBACX;AAAA,gBACD;AAAA;AAAA,YAED,IACE;AAAA,YACJ;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,iBAAiB,CAAC,cAAc,KAAK;AAAA,gBAC/C,SAAS,MAAM,KAAK,iBAAiB;AAAA,gBACrC,OAAO;AAAA,kBACL,YAAY,iBAAiB,CAAC,cAAc,KAAK,IAAI,YAAY;AAAA,kBACjE,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,iBAAiB,CAAC,cAAc,KAAK,IAAI,gBAAgB;AAAA,kBACjE,YAAY;AAAA,kBACZ,SAAS;AAAA,gBACX;AAAA,gBAEC,0BAAgB,qBAAgB;AAAA;AAAA,YACnC;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,KAAK;AAAA,UACL,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,+BAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,gCAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,KAAK,YAAY,QAAQ,QAAQ,EAAE,GAAG,+BAAiB;AAAA,YAC9F,qBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG;AAAA;AAAA,cACwC,oBAAC,YAAO,4BAAc;AAAA,cAAS;AAAA,eAClH;AAAA,aACF;AAAA,UAEA,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB;AAAA,gBAC/B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,sBAAsB,uBAAuB,KAAK;AAAA,gBACvE,aAAY;AAAA,gBACZ,OAAO,iBAAiB;AAAA,gBACzB;AAAA;AAAA,kBAC2D,oBAAC,YAAO,4BAAc;AAAA,kBAAS;AAAA;AAAA;AAAA,YAC3F;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB;AAAA,gBAC/B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,sBAAsB,oBAAoB,KAAK;AAAA,gBACpE,aAAY;AAAA,gBACZ,OAAO,iBAAiB;AAAA,gBACzB;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB;AAAA,gBAC/B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,sBAAsB,sBAAsB,KAAK;AAAA,gBACtE,aAAY;AAAA,gBACZ,OAAO,iBAAiB;AAAA,gBACzB;AAAA;AAAA,YAED;AAAA,aACF;AAAA,UAEC,oBAAoB,oBAAC,eAAY,QAAQ,mBAAmB,IAAK;AAAA,UAElE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,gBAAgB,WAAW,GACjE;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB;AAAA,gBAC/B,SAAS,MAAM;AACb,sCAAoB,kBAAkB;AACtC,uCAAqB,IAAI;AAAA,gBAC3B;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,qBAAqB,mBAAmB,gBAAgB;AAAA,kBAChE,YAAY;AAAA,kBACZ,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBACN;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB,oBAAoB,CAAC;AAAA,gBACpD,SAAS,MAAM;AACb,uBAAK,2BAA2B;AAAA,gBAClC;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY,qBAAqB,oBAAoB,CAAC,kBAAkB,YAAY;AAAA,kBACpF,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,qBAAqB,oBAAoB,CAAC,kBAAkB,gBAAgB;AAAA,kBACpF,YAAY;AAAA,kBACZ,UAAU;AAAA,kBACV,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBAEJ,6BAAmB,cAAc;AAAA;AAAA,YACpC;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,KAAK;AAAA,UACL,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,+BAAC,SAAI,OAAO,EAAE,YAAY,SAAS,SAAS,QAAQ,KAAK,IAAI,gBAAgB,gBAAgB,GAC3F;AAAA,iCAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,kCAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,KAAK,YAAY,QAAQ,QAAQ,EAAE,GAAG,qCAAuB;AAAA,cACpG,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG,mHAE3C;AAAA,eACF;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO;AAAA,kBACL,YAAY,aAAa,YAAY;AAAA,kBACrC,cAAc;AAAA,kBACd,OAAO,aAAa,YAAY;AAAA,kBAChC,UAAU;AAAA,kBACV,YAAY;AAAA,kBACZ,SAAS;AAAA,kBACT,YAAY;AAAA,gBACd;AAAA,gBAEC,uBAAa,eAAe,aAAa,cAAc;AAAA;AAAA,YAC1D;AAAA,aACF;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,gBACL,YAAY;AAAA,gBACZ,YAAY;AAAA,gBACZ,QAAQ;AAAA,gBACR,cAAc;AAAA,gBACd,SAAS;AAAA,gBACT,KAAK;AAAA,gBACL,gBAAgB;AAAA,gBAChB,SAAS;AAAA,cACX;AAAA,cAEA;AAAA,qCAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,sCAAC,YACE,WAAC,YACE,oCACA,aACE,WACE,gBAAgB,QAAQ,KACxB,iBAAiB,YAAY,KAC/B,4BAA4B,YAAY,IAChD;AAAA,kBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,UAAU,GAC7B,uBACG,iGACA,mGACN;AAAA,mBACF;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,UAAU,CAAC,aAAa,cAAc,YAAY;AAAA,oBAClD,SAAS,MAAM;AACb,2BAAK,yBAAyB;AAAA,oBAChC;AAAA,oBACA,OAAO;AAAA,sBACL,YAAY,CAAC,aAAa,cAAc,YAAY,UAAU,YAAY;AAAA,sBAC1E,QAAQ;AAAA,sBACR,cAAc;AAAA,sBACd,OAAO;AAAA,sBACP,QAAQ,CAAC,aAAa,cAAc,YAAY,UAAU,gBAAgB;AAAA,sBAC1E,YAAY;AAAA,sBACZ,UAAU;AAAA,sBACV,SAAS;AAAA,oBACX;AAAA,oBACA,MAAK;AAAA,oBAEJ,uBAAa,4BAA4B,aAAa,2BAA2B;AAAA;AAAA,gBACpF;AAAA;AAAA;AAAA,UACF;AAAA,UAEA,qBAAC,SAAI,OAAO,EAAE,WAAW,qBAAqB,SAAS,QAAQ,KAAK,IAAI,YAAY,GAAG,GACrF;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,sBAAsB;AAAA,gBAChC,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,iBAAiB,6BAA6B,KAAK;AAAA,gBACxE,aAAY;AAAA,gBACZ,OAAO,YAAY;AAAA,gBACpB;AAAA;AAAA,YAED;AAAA,YAEC,qBAAqB,oBAAC,eAAY,QAAQ,oBAAoB,IAAK;AAAA,YAEpE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,gBAAgB,WAAW,GACjE;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,UAAU,sBAAsB;AAAA,kBAChC,SAAS,MAAM;AACb,mCAAe,aAAa;AAC5B,0CAAsB,IAAI;AAAA,kBAC5B;AAAA,kBACA,OAAO;AAAA,oBACL,YAAY;AAAA,oBACZ,QAAQ;AAAA,oBACR,cAAc;AAAA,oBACd,OAAO;AAAA,oBACP,QAAQ,sBAAsB,oBAAoB,gBAAgB;AAAA,oBAClE,YAAY;AAAA,oBACZ,SAAS;AAAA,kBACX;AAAA,kBACA,MAAK;AAAA,kBACN;AAAA;AAAA,cAED;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,UAAU,sBAAsB,qBAAqB,CAAC;AAAA,kBACtD,SAAS,MAAM;AACb,yBAAK,sBAAsB;AAAA,kBAC7B;AAAA,kBACA,OAAO;AAAA,oBACL,YAAY,sBAAsB,qBAAqB,CAAC,mBAAmB,YAAY;AAAA,oBACvF,QAAQ;AAAA,oBACR,cAAc;AAAA,oBACd,OAAO;AAAA,oBACP,QAAQ,sBAAsB,qBAAqB,CAAC,mBAAmB,gBAAgB;AAAA,oBACvF,YAAY;AAAA,oBACZ,UAAU;AAAA,oBACV,SAAS;AAAA,kBACX;AAAA,kBACA,MAAK;AAAA,kBAEJ,8BAAoB,cAAc;AAAA;AAAA,cACrC;AAAA,eACF;AAAA,aACF;AAAA,UAEC,YAAY,QACX,qBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG;AAAA;AAAA,YACL,YAAY,MAAM;AAAA,aACxD,IACE;AAAA;AAAA;AAAA,IACN;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,KAAK;AAAA,UACL,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,+BAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,gCAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,KAAK,YAAY,QAAQ,QAAQ,EAAE,GAAG,8CAAgC;AAAA,YAC7G,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG,sJAE3C;AAAA,aACF;AAAA,UAEA,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,SAAS,aAAa;AAAA,gBACtB,UAAU,iBAAiB;AAAA,gBAC3B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,kBAAkB,kBAAkB,KAAK;AAAA,gBAC/D;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,SAAS,aAAa;AAAA,gBACtB,UAAU,iBAAiB;AAAA,gBAC3B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,kBAAkB,iBAAiB,KAAK;AAAA,gBAC9D;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,iBAAiB;AAAA,gBAC3B,iBAAgB;AAAA,gBAChB,OAAM;AAAA,gBACN,cAAa;AAAA,gBACb,UAAU,CAAC,UAAU,kBAAkB,0BAA0B,KAAK;AAAA,gBACtE,aAAY;AAAA,gBACZ,OAAO,aAAa;AAAA,gBACrB;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,iBAAiB;AAAA,gBAC3B,iBAAgB;AAAA,gBAChB,OAAM;AAAA,gBACN,cAAa;AAAA,gBACb,UAAU,CAAC,UAAU,kBAAkB,0BAA0B,KAAK;AAAA,gBACtE,aAAY;AAAA,gBACZ,OAAO,aAAa;AAAA,gBACrB;AAAA;AAAA,YAED;AAAA,aACF;AAAA,UAEC,gBAAgB,oBAAC,eAAY,QAAQ,eAAe,IAAK;AAAA,UAE1D,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,gBAAgB,WAAW,GACjE;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,iBAAiB;AAAA,gBAC3B,SAAS,MAAM;AACb,kCAAgB,cAAc;AAC9B,mCAAiB,IAAI;AAAA,gBACvB;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,iBAAiB,eAAe,gBAAgB;AAAA,kBACxD,YAAY;AAAA,kBACZ,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBACN;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,iBAAiB,gBAAgB,CAAC;AAAA,gBAC5C,SAAS,MAAM;AACb,uBAAK,uBAAuB;AAAA,gBAC9B;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY,iBAAiB,gBAAgB,CAAC,cAAc,YAAY;AAAA,kBACxE,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,iBAAiB,gBAAgB,CAAC,cAAc,gBAAgB;AAAA,kBACxE,YAAY;AAAA,kBACZ,UAAU;AAAA,kBACV,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBAEJ,yBAAe,cAAc;AAAA;AAAA,YAChC;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,KAAK;AAAA,UACL,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,+BAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,gCAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,KAAK,YAAY,QAAQ,QAAQ,EAAE,GAAG,iDAAmC;AAAA,YAChH,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG,8KAE3C;AAAA,aACF;AAAA,UAEA,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO;AAAA,kBACL,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,SAAS;AAAA,kBACT,KAAK;AAAA,kBACL,SAAS;AAAA,gBACX;AAAA,gBAEA;AAAA,sCAAC,YAAO,2BAAa;AAAA,kBACrB,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,wCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,8BAAgB;AAAA,oBAClF;AAAA,sBAAC;AAAA;AAAA,wBACC,UAAU,kBAAkB;AAAA,wBAC5B,UAAU,CAAC,UAAU,mBAAmB,iBAAiB,MAAM,cAAc,KAAK;AAAA,wBAClF,aAAY;AAAA,wBACZ,OAAO;AAAA,0BACL,QAAQ;AAAA,0BACR,cAAc;AAAA,0BACd,UAAU;AAAA,0BACV,UAAU;AAAA,0BACV,SAAS;AAAA,wBACX;AAAA,wBACA,MAAK;AAAA,wBACL,OAAO,cAAc;AAAA;AAAA,oBACvB;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAG,+GAEjD;AAAA,qBACF;AAAA,kBACA,qBAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,yCAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,SAAS,cAAc;AAAA,0BACvB,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,gBAAgB,MAAM,cAAc,OAAO;AAAA,0BACnF,MAAK;AAAA;AAAA,sBACP;AAAA,sBAAE;AAAA,uBAEJ;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,qGAEjE;AAAA,qBACF;AAAA,kBACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,wCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,wCAA0B;AAAA,oBAC5F;AAAA,sBAAC;AAAA;AAAA,wBACC,UAAU,kBAAkB;AAAA,wBAC5B,KAAK;AAAA,wBACL,UAAU,CAAC,UAAU,mBAAmB,sBAAsB,OAAO,MAAM,cAAc,KAAK,CAAC;AAAA,wBAC/F,aAAY;AAAA,wBACZ,OAAO;AAAA,0BACL,QAAQ;AAAA,0BACR,cAAc;AAAA,0BACd,UAAU;AAAA,0BACV,UAAU;AAAA,0BACV,UAAU;AAAA,0BACV,SAAS;AAAA,wBACX;AAAA,wBACA,MAAK;AAAA,wBACL,OAAO,cAAc;AAAA;AAAA,oBACvB;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAG,wJAEjD;AAAA,qBACF;AAAA;AAAA;AAAA,YACF;AAAA,YAEA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO;AAAA,kBACL,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,SAAS;AAAA,kBACT,KAAK;AAAA,kBACL,SAAS;AAAA,gBACX;AAAA,gBAEA;AAAA,sCAAC,YAAO,oBAAM;AAAA,kBACd,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA,yCAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,2CAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,0BAAC;AAAA;AAAA,4BACC,SAAS,cAAc;AAAA,4BACvB,UAAU,kBAAkB;AAAA,4BAC5B,UAAU,CAAC,UAAU,mBAAmB,wBAAwB,MAAM,cAAc,OAAO;AAAA,4BAC3F,MAAK;AAAA;AAAA,wBACP;AAAA,wBAAE;AAAA,yBAEJ;AAAA,sBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,uEAEjE;AAAA,uBACF;AAAA,oBACA,qBAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,2CAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,0BAAC;AAAA;AAAA,4BACC,SAAS,cAAc;AAAA,4BACvB,UAAU,kBAAkB;AAAA,4BAC5B,UAAU,CAAC,UAAU,mBAAmB,qBAAqB,MAAM,cAAc,OAAO;AAAA,4BACxF,MAAK;AAAA;AAAA,wBACP;AAAA,wBAAE;AAAA,yBAEJ;AAAA,sBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,sEAEjE;AAAA,uBACF;AAAA,oBACA,qBAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,2CAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,0BAAC;AAAA;AAAA,4BACC,SAAS,cAAc;AAAA,4BACvB,UAAU,kBAAkB;AAAA,4BAC5B,UAAU,CAAC,UAAU,mBAAmB,yBAAyB,MAAM,cAAc,OAAO;AAAA,4BAC5F,MAAK;AAAA;AAAA,wBACP;AAAA,wBAAE;AAAA,yBAEJ;AAAA,sBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,0EAEjE;AAAA,uBACF;AAAA,oBACA,qBAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,2CAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,0BAAC;AAAA;AAAA,4BACC,SAAS,cAAc;AAAA,4BACvB,UAAU,kBAAkB;AAAA,4BAC5B,UAAU,CAAC,UAAU,mBAAmB,wBAAwB,MAAM,cAAc,OAAO;AAAA,4BAC3F,MAAK;AAAA;AAAA,wBACP;AAAA,wBAAE;AAAA,yBAEJ;AAAA,sBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,qIAEjE;AAAA,uBACF;AAAA,qBACF;AAAA,kBACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,wCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,2CAA6B;AAAA,oBAC/F;AAAA,sBAAC;AAAA;AAAA,wBACC,UAAU,kBAAkB;AAAA,wBAC5B,UAAU,CAAC,UAAU,mBAAmB,0BAA0B,MAAM,cAAc,KAAK;AAAA,wBAC3F,aAAY;AAAA,wBACZ,OAAO;AAAA,0BACL,QAAQ;AAAA,0BACR,cAAc;AAAA,0BACd,UAAU;AAAA,0BACV,UAAU;AAAA,0BACV,SAAS;AAAA,wBACX;AAAA,wBACA,MAAK;AAAA,wBACL,OAAO,cAAc;AAAA;AAAA,oBACvB;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAG,4GAEjD;AAAA,qBACF;AAAA,kBACA,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA,yCAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,2CAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,0BAAC;AAAA;AAAA,4BACC,SAAS,cAAc;AAAA,4BACvB,UAAU,kBAAkB;AAAA,4BAC5B,UAAU,CAAC,UAAU,mBAAmB,wBAAwB,MAAM,cAAc,OAAO;AAAA,4BAC3F,MAAK;AAAA;AAAA,wBACP;AAAA,wBAAE;AAAA,yBAEJ;AAAA,sBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,6IAEjE;AAAA,uBACF;AAAA,oBACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,0CAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,6BAAe;AAAA,sBACjF;AAAA,wBAAC;AAAA;AAAA,0BACC,UAAU,kBAAkB,iBAAiB,CAAC,cAAc;AAAA,0BAC5D,UAAU,CAAC,UAAU,mBAAmB,kBAAkB,MAAM,cAAc,KAAK;AAAA,0BACnF,aAAY;AAAA,0BACZ,OAAO;AAAA,4BACL,QAAQ;AAAA,4BACR,cAAc;AAAA,4BACd,UAAU;AAAA,4BACV,UAAU;AAAA,4BACV,SAAS;AAAA,0BACX;AAAA,0BACA,MAAK;AAAA,0BACL,OAAO,cAAc;AAAA;AAAA,sBACvB;AAAA,sBACA,qBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAG;AAAA;AAAA,wBAC6B,oBAAC,UAAK,uBAAe;AAAA,wBAAO;AAAA,yBAC1G;AAAA,uBACF;AAAA,qBACF;AAAA;AAAA;AAAA,YACF;AAAA,YAEA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAM;AAAA,gBACN,QAAQ,cAAc;AAAA,gBACtB,SAAS,cAAc;AAAA,gBACvB,iBAAgB;AAAA,gBAChB,kBAAiB;AAAA,gBACjB,UAAU,kBAAkB;AAAA,gBAC5B,gBAAgB,CAAC,UAAU,mBAAmB,mBAAmB,KAAK;AAAA,gBACtE,iBAAiB,CAAC,UAAU,mBAAmB,oBAAoB,KAAK;AAAA,gBACxE,UAAS;AAAA,gBACT,QACE,iCACE;AAAA,uCAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,yCAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,SAAS,cAAc;AAAA,0BACvB,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,2BAA2B,MAAM,cAAc,OAAO;AAAA,0BAC9F,MAAK;AAAA;AAAA,sBACP;AAAA,sBAAE;AAAA,uBAEJ;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,6EAEjE;AAAA,qBACF;AAAA,kBACA,qBAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,yCAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,SAAS,cAAc;AAAA,0BACvB,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,4BAA4B,MAAM,cAAc,OAAO;AAAA,0BAC/F,MAAK;AAAA;AAAA,sBACP;AAAA,sBAAE;AAAA,uBAEJ;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,8FAEjE;AAAA,qBACF;AAAA,mBACF;AAAA;AAAA,YAEJ;AAAA,YAEA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAM;AAAA,gBACN,QAAQ,cAAc;AAAA,gBACtB,SAAS,cAAc;AAAA,gBACvB,iBAAgB;AAAA,gBAChB,kBAAiB;AAAA,gBACjB,UAAU,kBAAkB;AAAA,gBAC5B,gBAAgB,CAAC,UAAU,mBAAmB,gBAAgB,KAAK;AAAA,gBACnE,iBAAiB,CAAC,UAAU,mBAAmB,iBAAiB,KAAK;AAAA,gBACrE,UAAS;AAAA,gBACT,QACE,iCACE;AAAA,uCAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,yCAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,SAAS,cAAc;AAAA,0BACvB,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,sBAAsB,MAAM,cAAc,OAAO;AAAA,0BACzF,MAAK;AAAA;AAAA,sBACP;AAAA,sBAAE;AAAA,uBAEJ;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,6EAEjE;AAAA,qBACF;AAAA,kBACA,qBAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,yCAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,SAAS,cAAc;AAAA,0BACvB,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,2BAA2B,MAAM,cAAc,OAAO;AAAA,0BAC9F,MAAK;AAAA;AAAA,sBACP;AAAA,sBAAE;AAAA,uBAEJ;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,mKAEjE;AAAA,qBACF;AAAA,kBACA,qBAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,yCAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,SAAS,cAAc;AAAA,0BACvB,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,4BAA4B,MAAM,cAAc,OAAO;AAAA,0BAC/F,MAAK;AAAA;AAAA,sBACP;AAAA,sBAAE;AAAA,uBAEJ;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAG,wKAEjE;AAAA,qBACF;AAAA,mBACF;AAAA;AAAA,YAEJ;AAAA,YAEA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO;AAAA,kBACL,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,SAAS;AAAA,kBACT,KAAK;AAAA,kBACL,SAAS;AAAA,gBACX;AAAA,gBAEA;AAAA,uCAAC,SAAI,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,gBAAgB,gBAAgB,GACnF;AAAA,wCAAC,YAAO,wBAAU;AAAA,oBAClB;AAAA,sBAAC;AAAA;AAAA,wBACC,UAAU,kBAAkB;AAAA,wBAC5B,SAAS,MAAM,YAAY;AAAA,wBAC3B,OAAO;AAAA,0BACL,YAAY;AAAA,0BACZ,QAAQ;AAAA,0BACR,cAAc;AAAA,0BACd,OAAO;AAAA,0BACP,QAAQ,kBAAkB,gBAAgB,gBAAgB;AAAA,0BAC1D,UAAU;AAAA,0BACV,YAAY;AAAA,0BACZ,SAAS;AAAA,wBACX;AAAA,wBACA,MAAK;AAAA,wBACN;AAAA;AAAA,oBAED;AAAA,qBACF;AAAA,kBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAG,6SAKjD;AAAA,kBACC,cAAc,UAAU,WAAW,IAClC,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,WAAW,SAAS,GAAG,uCAEtE,IAEA,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACpC,wBAAc,UAAU,IAAI,CAAC,OAAO,UACnC;AAAA,oBAAC;AAAA;AAAA,sBAEC,OAAO;AAAA,wBACL,QAAQ;AAAA,wBACR,cAAc;AAAA,wBACd,SAAS;AAAA,wBACT,KAAK;AAAA,wBACL,SAAS;AAAA,sBACX;AAAA,sBAEA;AAAA,6CAAC,SAAI,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,IAAI,gBAAgB,gBAAgB,GAC5F;AAAA,+CAAC,WAAM,OAAO,EAAE,YAAY,UAAU,OAAO,WAAW,SAAS,QAAQ,UAAU,IAAI,KAAK,EAAE,GAC5F;AAAA;AAAA,8BAAC;AAAA;AAAA,gCACC,SAAS,MAAM;AAAA,gCACf,UAAU,kBAAkB;AAAA,gCAC5B,UAAU,CAAC,UAAU,eAAe,OAAO,WAAW,MAAM,cAAc,OAAO;AAAA,gCACjF,MAAK;AAAA;AAAA,4BACP;AAAA,4BAAE;AAAA,6BAEJ;AAAA,0BACA;AAAA,4BAAC;AAAA;AAAA,8BACC,UAAU,kBAAkB;AAAA,8BAC5B,SAAS,MAAM,eAAe,KAAK;AAAA,8BACnC,OAAO;AAAA,gCACL,YAAY;AAAA,gCACZ,QAAQ;AAAA,gCACR,cAAc;AAAA,gCACd,OAAO;AAAA,gCACP,QAAQ,kBAAkB,gBAAgB,gBAAgB;AAAA,gCAC1D,UAAU;AAAA,gCACV,YAAY;AAAA,gCACZ,SAAS;AAAA,8BACX;AAAA,8BACA,MAAK;AAAA,8BACN;AAAA;AAAA,0BAED;AAAA,2BACF;AAAA,wBACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,8CAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,6BAAe;AAAA,0BACjF;AAAA,4BAAC;AAAA;AAAA,8BACC,UAAU,kBAAkB;AAAA,8BAC5B,UAAU,CAAC,UAAU,eAAe,OAAO,QAAQ,MAAM,cAAc,KAAK;AAAA,8BAC5E,aAAY;AAAA,8BACZ,OAAO,EAAE,QAAQ,qBAAqB,cAAc,GAAG,UAAU,IAAI,UAAU,GAAG,SAAS,WAAW;AAAA,8BACtG,MAAK;AAAA,8BACL,OAAO,MAAM;AAAA;AAAA,0BACf;AAAA,2BACF;AAAA,wBACA,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,qBAAqB,4BAA4B,GACtF;AAAA,+CAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,gDAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,wBAAU;AAAA,4BAC5E;AAAA,8BAAC;AAAA;AAAA,gCACC,UAAU,kBAAkB;AAAA,gCAC5B,UAAU,CAAC,UAAU,eAAe,OAAO,aAAa,MAAM,cAAc,KAAK;AAAA,gCACjF,aAAY;AAAA,gCACZ,OAAO,EAAE,QAAQ,qBAAqB,cAAc,GAAG,UAAU,IAAI,UAAU,GAAG,SAAS,WAAW;AAAA,gCACtG,MAAK;AAAA,gCACL,OAAO,MAAM;AAAA;AAAA,4BACf;AAAA,6BACF;AAAA,0BACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,gDAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,0BAAY;AAAA,4BAC9E;AAAA,8BAAC;AAAA;AAAA,gCACC,UAAU,kBAAkB;AAAA,gCAC5B,UAAU,CAAC,UAAU,eAAe,OAAO,eAAe,MAAM,cAAc,KAAK;AAAA,gCACnF,aAAY;AAAA,gCACZ,OAAO,EAAE,QAAQ,qBAAqB,cAAc,GAAG,UAAU,IAAI,UAAU,GAAG,SAAS,WAAW;AAAA,gCACtG,MAAK;AAAA,gCACL,OAAO,MAAM;AAAA;AAAA,4BACf;AAAA,6BACF;AAAA,0BACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,gDAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,qBAAO;AAAA,4BACzE;AAAA,8BAAC;AAAA;AAAA,gCACC,UAAU,kBAAkB;AAAA,gCAC5B,UAAU,CAAC,UAAU,eAAe,OAAO,UAAU,MAAM,cAAc,KAAK;AAAA,gCAC9E,aAAY;AAAA,gCACZ,OAAO,EAAE,QAAQ,qBAAqB,cAAc,GAAG,UAAU,IAAI,UAAU,GAAG,SAAS,WAAW;AAAA,gCACtG,MAAK;AAAA,gCACL,OAAO,MAAM;AAAA;AAAA,4BACf;AAAA,6BACF;AAAA,0BACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,gDAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,iCAAmB;AAAA,4BACrF;AAAA,8BAAC;AAAA;AAAA,gCACC,UAAU,kBAAkB;AAAA,gCAC5B,UAAU,CAAC,UAAU,eAAe,OAAO,WAAW,MAAM,cAAc,KAAK;AAAA,gCAC/E,aAAY;AAAA,gCACZ,OAAO,EAAE,QAAQ,qBAAqB,cAAc,GAAG,UAAU,IAAI,UAAU,GAAG,SAAS,WAAW;AAAA,gCACtG,MAAK;AAAA,gCACL,OAAO,MAAM;AAAA;AAAA,4BACf;AAAA,6BACF;AAAA,2BACF;AAAA;AAAA;AAAA,oBA7FK;AAAA,kBA8FP,CACD,GACH;AAAA;AAAA;AAAA,YAEJ;AAAA,YAEA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAM;AAAA,gBACN,QAAQ,cAAc;AAAA,gBACtB,SAAS,cAAc;AAAA,gBACvB,iBAAgB;AAAA,gBAChB,kBAAiB;AAAA,gBACjB,UAAU,kBAAkB;AAAA,gBAC5B,gBAAgB,CAAC,UAAU,mBAAmB,gBAAgB,KAAK;AAAA,gBACnE,iBAAiB,CAAC,UAAU,mBAAmB,iBAAiB,KAAK;AAAA,gBACrE,UAAS;AAAA,gBACT,QACE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA,uCAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,wCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,kBAAI;AAAA,oBACtE;AAAA,sBAAC;AAAA;AAAA,wBACC,UAAU,kBAAkB;AAAA,wBAC5B,UAAU,CAAC,UAAU,mBAAmB,cAAc,MAAM,cAAc,KAA4C;AAAA,wBACtH,OAAO;AAAA,0BACL,QAAQ;AAAA,0BACR,cAAc;AAAA,0BACd,UAAU;AAAA,0BACV,UAAU;AAAA,0BACV,SAAS;AAAA,wBACX;AAAA,wBACA,OAAO,cAAc;AAAA,wBAErB;AAAA,8CAAC,YAAO,OAAM,OAAM,iBAAG;AAAA,0BACvB,oBAAC,YAAO,OAAM,SAAQ,mBAAK;AAAA,0BAC3B,oBAAC,YAAO,OAAM,WAAU,qBAAO;AAAA,0BAC/B,oBAAC,YAAO,OAAM,YAAW,sBAAQ;AAAA;AAAA;AAAA,oBACnC;AAAA,oBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAG,+DAEjD;AAAA,qBACF;AAAA,kBACA,qBAAC,SAAI,OAAO,EAAE,YAAY,WAAW,SAAS,QAAQ,KAAK,IAAI,qBAAqB,4BAA4B,GAC9G;AAAA,yCAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,kBAAkB,+BAA+B,GACxF;AAAA,0CAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,wBAAU;AAAA,sBAC5E;AAAA,wBAAC;AAAA;AAAA,0BACC,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,mBAAmB,MAAM,cAAc,KAAK;AAAA,0BACpF,aAAY;AAAA,0BACZ,OAAO;AAAA,4BACL,QAAQ;AAAA,4BACR,cAAc;AAAA,4BACd,UAAU;AAAA,4BACV,UAAU;AAAA,4BACV,SAAS;AAAA,0BACX;AAAA,0BACA,MAAK;AAAA,0BACL,OAAO,cAAc;AAAA;AAAA,sBACvB;AAAA,sBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,OAAO,GAAG,gEAErE;AAAA,uBACF;AAAA,oBACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,kBAAkB,+BAA+B,GACxF;AAAA,0CAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,iCAAmB;AAAA,sBACrF;AAAA,wBAAC;AAAA;AAAA,0BACC,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,qBAAqB,MAAM,cAAc,KAAK;AAAA,0BACtF,aAAY;AAAA,0BACZ,OAAO;AAAA,4BACL,QAAQ;AAAA,4BACR,cAAc;AAAA,4BACd,UAAU;AAAA,4BACV,UAAU;AAAA,4BACV,SAAS;AAAA,0BACX;AAAA,0BACA,MAAK;AAAA,0BACL,OAAO,cAAc;AAAA;AAAA,sBACvB;AAAA,sBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,OAAO,GAAG,6DAErE;AAAA,uBACF;AAAA,oBACA,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,kBAAkB,+BAA+B,GACxF;AAAA,0CAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,4BAAc;AAAA,sBAChF;AAAA,wBAAC;AAAA;AAAA,0BACC,UAAU,kBAAkB;AAAA,0BAC5B,UAAU,CAAC,UAAU,mBAAmB,iBAAiB,MAAM,cAAc,KAAK;AAAA,0BAClF,aAAY;AAAA,0BACZ,OAAO;AAAA,4BACL,QAAQ;AAAA,4BACR,cAAc;AAAA,4BACd,UAAU;AAAA,4BACV,UAAU;AAAA,4BACV,SAAS;AAAA,0BACX;AAAA,0BACA,MAAK;AAAA,0BACL,OAAO,cAAc;AAAA;AAAA,sBACvB;AAAA,sBACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,OAAO,GAAG,gEAErE;AAAA,uBACF;AAAA,qBACF;AAAA,mBACF;AAAA;AAAA,YAEJ;AAAA,aACF;AAAA,UAEC,iBAAiB,oBAAC,eAAY,QAAQ,gBAAgB,IAAK;AAAA,UAE5D,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,gBAAgB,WAAW,GACjE;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,kBAAkB;AAAA,gBAC5B,SAAS,MAAM;AACb,mCAAiB,eAAe;AAChC,oCAAkB,IAAI;AAAA,gBACxB;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,kBAAkB,gBAAgB,gBAAgB;AAAA,kBAC1D,YAAY;AAAA,kBACZ,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBACN;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,kBAAkB,iBAAiB,CAAC;AAAA,gBAC9C,SAAS,MAAM;AACb,uBAAK,wBAAwB;AAAA,gBAC/B;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY,kBAAkB,iBAAiB,CAAC,eAAe,YAAY;AAAA,kBAC3E,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,kBAAkB,iBAAiB,CAAC,eAAe,gBAAgB;AAAA,kBAC3E,YAAY;AAAA,kBACZ,UAAU;AAAA,kBACV,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBAEJ,0BAAgB,cAAc;AAAA;AAAA,YACjC;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,KAAK;AAAA,UACL,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,+BAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,gCAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,KAAK,YAAY,QAAQ,QAAQ,EAAE,GAAG,wCAA0B;AAAA,YACvG,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG,sJAE3C;AAAA,aACF;AAAA,UAEA,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,gBAAgB;AAAA,gBAC1B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,iBAAiB,0BAA0B,KAAK;AAAA,gBACrE,aAAY;AAAA,gBACZ,OAAO,YAAY;AAAA,gBACpB;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,gBAAgB;AAAA,gBAC1B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,iBAAiB,gBAAgB,KAAK;AAAA,gBAC3D,aAAY;AAAA,gBACZ,OAAO,YAAY;AAAA,gBACpB;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,gBAAgB;AAAA,gBAC1B,iBAAgB;AAAA,gBAChB,OAAM;AAAA,gBACN,cAAa;AAAA,gBACb,UAAU,CAAC,UAAU,iBAAiB,qBAAqB,KAAK;AAAA,gBAChE,aAAY;AAAA,gBACZ,OAAO,YAAY;AAAA,gBACpB;AAAA;AAAA,YAED;AAAA,aACF;AAAA,UAEC,eAAe,oBAAC,eAAY,QAAQ,cAAc,IAAK;AAAA,UAExD,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,gBAAgB,WAAW,GACjE;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,gBAAgB;AAAA,gBAC1B,SAAS,MAAM;AACb,iCAAe,aAAa;AAC5B,kCAAgB,IAAI;AAAA,gBACtB;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,gBAAgB,cAAc,gBAAgB;AAAA,kBACtD,YAAY;AAAA,kBACZ,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBACN;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,gBAAgB,eAAe,CAAC;AAAA,gBAC1C,SAAS,MAAM;AACb,uBAAK,sBAAsB;AAAA,gBAC7B;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY,gBAAgB,eAAe,CAAC,aAAa,YAAY;AAAA,kBACrE,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,gBAAgB,eAAe,CAAC,aAAa,gBAAgB;AAAA,kBACrE,YAAY;AAAA,kBACZ,UAAU;AAAA,kBACV,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBAEJ,wBAAc,cAAc;AAAA;AAAA,YAC/B;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,KAAK;AAAA,UACL,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,+BAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,gCAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,KAAK,YAAY,QAAQ,QAAQ,EAAE,GAAG,8BAAgB;AAAA,YAC7F,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG,uHAE3C;AAAA,aACF;AAAA,UAEA,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB;AAAA,gBAC/B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,sBAAsB,oBAAoB,KAAK;AAAA,gBACpE,aAAY;AAAA,gBACZ,OAAO,iBAAiB;AAAA,gBACzB;AAAA;AAAA,YAED;AAAA,YACA,qBAAC,SAAI,OAAO,oBACV;AAAA,mCAAC,WAAM,OAAO,kBACZ;AAAA,oCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,qCAAuB;AAAA,gBACzF;AAAA,kBAAC;AAAA;AAAA,oBACC,UAAU,qBAAqB;AAAA,oBAC/B,KAAK;AAAA,oBACL,UAAU,CAAC,UAAU,sBAAsB,uBAAuB,OAAO,MAAM,cAAc,KAAK,CAAC;AAAA,oBACnG,aAAY;AAAA,oBACZ,OAAO;AAAA,oBACP,MAAK;AAAA,oBACL,OAAO,iBAAiB;AAAA;AAAA,gBAC1B;AAAA,gBACA,oBAAC,UAAK,OAAO,iBAAiB,yFAE9B;AAAA,iBACF;AAAA,cACA,qBAAC,WAAM,OAAO,kBACZ;AAAA,oCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,uCAAyB;AAAA,gBAC3F;AAAA,kBAAC;AAAA;AAAA,oBACC,UAAU,qBAAqB;AAAA,oBAC/B,UAAU,CAAC,UAAU,sBAAsB,2BAA2B,MAAM,cAAc,KAA4D;AAAA,oBACtJ,OAAO;AAAA,oBACP,OAAO,iBAAiB;AAAA,oBAExB;AAAA,0CAAC,YAAO,OAAM,SAAQ,mBAAK;AAAA,sBAC3B,oBAAC,YAAO,OAAM,cAAa,wBAAU;AAAA,sBACrC,oBAAC,YAAO,OAAM,SAAQ,mBAAK;AAAA;AAAA;AAAA,gBAC7B;AAAA,gBACA,oBAAC,UAAK,OAAO,iBAAiB,2GAE9B;AAAA,iBACF;AAAA,eACF;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB;AAAA,gBAC/B,OAAM;AAAA,gBACN,UAAU,CAAC,UAAU,sBAAsB,yBAAyB,KAAK;AAAA,gBACzE,aAAY;AAAA,gBACZ,MAAM;AAAA,gBACN,OAAO,iBAAiB;AAAA,gBACzB;AAAA;AAAA,YAED;AAAA,aACF;AAAA,UAEC,oBAAoB,oBAAC,eAAY,QAAQ,mBAAmB,IAAK;AAAA,UAElE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,gBAAgB,WAAW,GACjE;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB;AAAA,gBAC/B,SAAS,MAAM;AACb,sCAAoB,kBAAkB;AACtC,uCAAqB,IAAI;AAAA,gBAC3B;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,qBAAqB,mBAAmB,gBAAgB;AAAA,kBAChE,YAAY;AAAA,kBACZ,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBACN;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,qBAAqB,oBAAoB,CAAC;AAAA,gBACpD,SAAS,MAAM;AACb,uBAAK,2BAA2B;AAAA,gBAClC;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY,qBAAqB,oBAAoB,CAAC,kBAAkB,YAAY;AAAA,kBACpF,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,qBAAqB,oBAAoB,CAAC,kBAAkB,gBAAgB;AAAA,kBACpF,YAAY;AAAA,kBACZ,UAAU;AAAA,kBACV,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBAEJ,6BAAmB,cAAc;AAAA;AAAA,YACpC;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,SAAS;AAAA,UACT,KAAK;AAAA,UACL,SAAS;AAAA,QACX;AAAA,QAEA;AAAA,+BAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,gCAAC,QAAG,OAAO,EAAE,UAAU,IAAI,YAAY,KAAK,YAAY,QAAQ,QAAQ,EAAE,GAAG,mCAAqB;AAAA,YAClG,oBAAC,OAAE,OAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,GAAG,mIAE3C;AAAA,aACF;AAAA,UAEA,qBAAC,SAAI,OAAO,oBACV;AAAA,iCAAC,WAAM,OAAO,kBACZ;AAAA,kCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,mCAAqB;AAAA,cACvF;AAAA,gBAAC;AAAA;AAAA,kBACC,UAAU,oBAAoB;AAAA,kBAC9B,KAAK;AAAA,kBACL,UAAU,CAAC,UAAU,qBAAqB,mCAAmC,OAAO,MAAM,cAAc,KAAK,CAAC;AAAA,kBAC9G,aAAY;AAAA,kBACZ,OAAO;AAAA,kBACP,MAAK;AAAA,kBACL,OAAO,gBAAgB;AAAA;AAAA,cACzB;AAAA,cACA,oBAAC,UAAK,OAAO,iBAAiB,uIAE9B;AAAA,eACF;AAAA,YACA,qBAAC,WAAM,OAAO,kBACZ;AAAA,kCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,6CAA+B;AAAA,cACjG;AAAA,gBAAC;AAAA;AAAA,kBACC,UAAU,oBAAoB;AAAA,kBAC9B,KAAK;AAAA,kBACL,UAAU,CAAC,UAAU,qBAAqB,8BAA8B,OAAO,MAAM,cAAc,KAAK,CAAC;AAAA,kBACzG,aAAY;AAAA,kBACZ,OAAO;AAAA,kBACP,MAAK;AAAA,kBACL,OAAO,gBAAgB;AAAA;AAAA,cACzB;AAAA,cACA,oBAAC,UAAK,OAAO,iBAAiB,mIAE9B;AAAA,eACF;AAAA,aACF;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,gBACL,YAAY;AAAA,gBACZ,QAAQ;AAAA,gBACR,cAAc;AAAA,gBACd,OAAO;AAAA,gBACP,SAAS;AAAA,gBACT,UAAU;AAAA,gBACV,KAAK;AAAA,gBACL,SAAS;AAAA,cACX;AAAA,cAEA;AAAA,oCAAC,YAAO,OAAO,EAAE,OAAO,UAAU,GAAG,4BAAc;AAAA,gBACnD,oBAAC,UAAK,sOAEN;AAAA;AAAA;AAAA,UACF;AAAA,UAEC,mBAAmB,oBAAC,eAAY,QAAQ,kBAAkB,IAAK;AAAA,UAEhE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,IAAI,gBAAgB,WAAW,GACjE;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,oBAAoB;AAAA,gBAC9B,SAAS,MAAM;AACb,qCAAmB,iBAAiB;AACpC,sCAAoB,IAAI;AAAA,gBAC1B;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,oBAAoB,kBAAkB,gBAAgB;AAAA,kBAC9D,YAAY;AAAA,kBACZ,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBACN;AAAA;AAAA,YAED;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,UAAU,oBAAoB,mBAAmB,CAAC;AAAA,gBAClD,SAAS,MAAM;AACb,uBAAK,0BAA0B;AAAA,gBACjC;AAAA,gBACA,OAAO;AAAA,kBACL,YAAY,oBAAoB,mBAAmB,CAAC,iBAAiB,YAAY;AAAA,kBACjF,QAAQ;AAAA,kBACR,cAAc;AAAA,kBACd,OAAO;AAAA,kBACP,QAAQ,oBAAoB,mBAAmB,CAAC,iBAAiB,gBAAgB;AAAA,kBACjF,YAAY;AAAA,kBACZ,UAAU;AAAA,kBACV,SAAS;AAAA,gBACX;AAAA,gBACA,MAAK;AAAA,gBAEJ,4BAAkB,cAAc;AAAA;AAAA,YACnC;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAEA,SAAS,YAAY,EAAE,OAAO,GAA0C;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,QAAQ,aAAa,OAAO,SAAS,YAAY,YAAY,SAAS;AAAA,QACtE,cAAc;AAAA,QACd,YAAY,OAAO,SAAS,YAAY,YAAY;AAAA,QACpD,OAAO,OAAO,SAAS,YAAY,YAAY;AAAA,QAC/C,SAAS;AAAA,MACX;AAAA,MAEA;AAAA,4BAAC,YAAQ,iBAAO,OAAM;AAAA,QACrB,OAAO,OAAO,oBAAC,OAAE,OAAO,EAAE,QAAQ,UAAU,GAAI,iBAAO,MAAK,IAAO;AAAA;AAAA;AAAA,EACtE;AAEJ;AAEA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AACT,GAQsB;AACpB,SACE,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,wBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAI,iBAAM;AAAA,IACzE;AAAA,MAAC;AAAA;AAAA,QACC,cAAc,SAAS,aAAa,QAAQ;AAAA,QAC5C;AAAA,QACA,UAAU,CAAC,UAAU,SAAS,MAAM,cAAc,KAAK;AAAA,QACvD;AAAA,QACA,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,UAAU;AAAA,UACV,UAAU;AAAA,UACV,SAAS;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAI,UAAS;AAAA,KAC7D;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AACF,GAQsB;AACpB,SACE,qBAAC,WAAM,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACtC;AAAA,wBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAI,iBAAM;AAAA,IACzE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,UAAU,CAAC,UAAU,SAAS,MAAM,cAAc,KAAK;AAAA,QACvD;AAAA,QACA;AAAA,QACA,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,UAAU;AAAA,UACV,UAAU;AAAA,UACV,SAAS;AAAA,UACT,QAAQ;AAAA,QACV;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAI,UAAS;AAAA,KAC7D;AAEJ;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASsB;AACpB,WAAS,WAAW,OAAe,WAAyB;AAC1D,UAAM,OAAO,CAAC,GAAG,KAAK;AACtB,SAAK,KAAK,IAAI;AACd,aAAS,IAAI;AAAA,EACf;AAEA,WAAS,WAAW,OAAqB;AACvC,aAAS,MAAM,OAAO,CAAC,GAAG,cAAc,cAAc,KAAK,CAAC;AAAA,EAC9D;AAEA,WAAS,UAAgB;AACvB,aAAS,CAAC,GAAG,OAAO,EAAE,CAAC;AAAA,EACzB;AAEA,SACE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA,wBAAC,SAAI,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAI,iBAAM;AAAA,IACxE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACnC;AAAA,YAAM,WAAW,IAChB;AAAA,QAAC;AAAA;AAAA,UACC,OAAO;AAAA,YACL,QAAQ;AAAA,YACR,cAAc;AAAA,YACd,OAAO;AAAA,YACP,UAAU;AAAA,YACV,SAAS;AAAA,UACX;AAAA,UAEC;AAAA;AAAA,MACH,IACE;AAAA,MACH,MAAM,IAAI,CAAC,MAAM,UAChB,qBAAC,SAAgB,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,GAAG,qBAAqB,sBAAsB,GAClH;AAAA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,QAAQ,MAAM;AACZ,oBAAM,UAAU,MAAM,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAAE,OAAO,OAAO;AACjE,kBAAI,KAAK,UAAU,OAAO,MAAM,KAAK,UAAU,KAAK,GAAG;AACrD,yBAAS,OAAO;AAAA,cAClB;AAAA,YACF;AAAA,YACA,UAAU,CAAC,UAAU,WAAW,OAAO,MAAM,cAAc,KAAK;AAAA,YAChE;AAAA,YACA,OAAO;AAAA,cACL,QAAQ;AAAA,cACR,cAAc;AAAA,cACd,UAAU;AAAA,cACV,UAAU;AAAA,cACV,SAAS;AAAA,YACX;AAAA,YACA,MAAK;AAAA,YACL,OAAO;AAAA;AAAA,QACT;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,SAAS,MAAM,WAAW,KAAK;AAAA,YAC/B,OAAO;AAAA,cACL,YAAY;AAAA,cACZ,QAAQ;AAAA,cACR,cAAc;AAAA,cACd,OAAO;AAAA,cACP,QAAQ,WAAW,gBAAgB;AAAA,cACnC,YAAY;AAAA,cACZ,SAAS;AAAA,YACX;AAAA,YACA,MAAK;AAAA,YACN;AAAA;AAAA,QAED;AAAA,WApCQ,KAqCV,CACD;AAAA,OACH;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,SAAS;AAAA,QACT,OAAO;AAAA,UACL,YAAY;AAAA,UACZ,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,OAAO;AAAA,UACP,QAAQ,WAAW,gBAAgB;AAAA,UACnC,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,SAAS;AAAA,QACX;AAAA,QACA,MAAK;AAAA,QAEJ;AAAA;AAAA,IACH;AAAA,IACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,GAAG,GAAI,UAAS;AAAA,KAC7D;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMsB;AACpB,SACE,qBAAC,WAAM,OAAO,EAAE,OAAO,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,GACtE;AAAA,yBAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,EAAE,GAC3D;AAAA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,UAAU,CAAC,UAAU,SAAS,MAAM,cAAc,OAAO;AAAA,UACzD,MAAK;AAAA;AAAA,MACP;AAAA,MACC;AAAA,OACH;AAAA,IACA,oBAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,GAAG,GAAI,UAAS;AAAA,KAC7E;AAEJ;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAYsB;AACpB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,cAAc;AAAA,QACd,SAAS;AAAA,QACT,KAAK;AAAA,QACL,SAAS;AAAA,MACX;AAAA,MAEA;AAAA,6BAAC,SAAI,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,KAAK,IAAI,gBAAgB,gBAAgB,GAC5F;AAAA,8BAAC,YAAQ,iBAAM;AAAA,UACd,WAAW,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,UAAU,QAAQ,KAAK,GAAG,GAAI,UAAS,IAAS;AAAA,WAC7F;AAAA,QACA,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GACrC,+BAAC,SAAI,OAAO,oBACV;AAAA,+BAAC,WAAM,OAAO,kBACZ;AAAA,gCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,qBAAO;AAAA,YACzE;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA,UAAU,CAAC,UAAU,eAAe,MAAM,cAAc,KAAK;AAAA,gBAC7D,aAAa;AAAA,gBACb,OAAO;AAAA,gBACP,MAAK;AAAA,gBACL,OAAO;AAAA;AAAA,YACT;AAAA,YACA,oBAAC,UAAK,OAAO,iBAAkB,oBAAS;AAAA,aAC1C;AAAA,UACA,qBAAC,WAAM,OAAO,kBACZ;AAAA,gCAAC,UAAK,OAAO,EAAE,OAAO,WAAW,UAAU,IAAI,YAAY,IAAI,GAAG,sBAAQ;AAAA,YAC1E;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA,UAAU,CAAC,UAAU,gBAAgB,MAAM,cAAc,KAAK;AAAA,gBAC9D,aAAa;AAAA,gBACb,OAAO;AAAA,gBACP,MAAK;AAAA,gBACL,OAAO;AAAA;AAAA,YACT;AAAA,YACA,oBAAC,UAAK,OAAO,iBAAiB,oFAE9B;AAAA,aACF;AAAA,WACF,GACF;AAAA,QACC,SAAS,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,GAAI,kBAAO,IAAS;AAAA;AAAA;AAAA,EACvE;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|