thepopebot 1.2.76-beta.2 → 1.2.76-beta.21
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/README.md +3 -3
- package/api/CLAUDE.md +11 -4
- package/api/index.js +56 -18
- package/bin/CLAUDE.md +7 -4
- package/bin/cli.js +25 -45
- package/config/CLAUDE.md +23 -4
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/0022_organic_apocalypse.sql +16 -0
- package/drizzle/0023_needy_ender_wiggin.sql +1 -0
- package/drizzle/meta/0021_snapshot.json +639 -0
- package/drizzle/meta/0022_snapshot.json +743 -0
- package/drizzle/meta/0023_snapshot.json +750 -0
- package/drizzle/meta/_journal.json +21 -0
- package/lib/CLAUDE.md +2 -2
- package/lib/actions.js +9 -1
- package/lib/ai/CLAUDE.md +72 -57
- package/lib/ai/helper-llm.js +108 -0
- package/lib/ai/index.js +308 -438
- package/lib/ai/line-mappers.js +42 -24
- package/lib/ai/scope.js +26 -0
- package/lib/ai/sdk-adapters/CLAUDE.md +114 -0
- package/lib/ai/sdk-adapters/claude-code.js +120 -8
- package/lib/ai/system-prompt.js +34 -0
- package/lib/ai/workspace-setup.js +19 -35
- package/lib/channels/CLAUDE.md +14 -4
- package/lib/channels/base.js +6 -2
- package/lib/channels/commands/index.js +42 -0
- package/lib/channels/commands/session.js +53 -0
- package/lib/channels/commands/verify.js +18 -0
- package/lib/channels/telegram.js +79 -28
- package/lib/chat/CLAUDE.md +4 -4
- package/lib/chat/actions.js +270 -49
- package/lib/chat/api.js +185 -31
- package/lib/chat/components/CLAUDE.md +6 -2
- package/lib/chat/components/chat-input.js +77 -47
- package/lib/chat/components/chat-input.jsx +77 -40
- package/lib/chat/components/chat-page.js +2 -0
- package/lib/chat/components/chat-page.jsx +3 -0
- package/lib/chat/components/chat.js +62 -14
- package/lib/chat/components/chat.jsx +68 -10
- package/lib/chat/components/code-mode-toggle.js +141 -22
- package/lib/chat/components/code-mode-toggle.jsx +129 -20
- package/lib/chat/components/containers-page.js +58 -40
- package/lib/chat/components/containers-page.jsx +64 -25
- package/lib/chat/components/crons-page.js +17 -3
- package/lib/chat/components/crons-page.jsx +34 -6
- package/lib/chat/components/index.js +2 -2
- package/lib/chat/components/message.js +18 -3
- package/lib/chat/components/message.jsx +18 -3
- package/lib/chat/components/profile-page.js +182 -4
- package/lib/chat/components/profile-page.jsx +196 -1
- package/lib/chat/components/scope-picker.js +21 -0
- package/lib/chat/components/scope-picker.jsx +27 -0
- package/lib/chat/components/settings-chat-page.js +11 -11
- package/lib/chat/components/settings-chat-page.jsx +14 -18
- package/lib/chat/components/settings-coding-agents-page.js +110 -16
- package/lib/chat/components/settings-coding-agents-page.jsx +87 -3
- package/lib/chat/components/settings-github-page.js +5 -0
- package/lib/chat/components/settings-github-page.jsx +5 -0
- package/lib/chat/components/settings-layout.js +3 -3
- package/lib/chat/components/settings-layout.jsx +3 -3
- package/lib/chat/components/settings-secrets-layout.js +1 -2
- package/lib/chat/components/settings-secrets-layout.jsx +1 -2
- package/lib/chat/components/settings-secrets-page.js +180 -75
- package/lib/chat/components/settings-secrets-page.jsx +212 -66
- package/lib/chat/components/triggers-page.js +17 -3
- package/lib/chat/components/triggers-page.jsx +34 -6
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/chat/components/ui/dropdown-menu.js +23 -2
- package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
- package/lib/chat/telegram-profile.js +33 -0
- package/lib/cluster/CLAUDE.md +9 -3
- package/lib/code/CLAUDE.md +11 -3
- package/lib/code/actions.js +47 -8
- package/lib/code/terminal-view.js +31 -21
- package/lib/code/terminal-view.jsx +32 -23
- package/lib/config.js +15 -4
- package/lib/containers/CLAUDE.md +16 -6
- package/lib/db/CLAUDE.md +5 -2
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +8 -3
- package/lib/db/config.js +0 -1
- package/lib/db/index.js +12 -0
- package/lib/db/schema.js +24 -1
- package/lib/db/user-channels.js +129 -0
- package/lib/llm-providers.js +8 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/CLAUDE.md +12 -3
- package/lib/tools/assemblyai.js +17 -0
- package/lib/tools/create-agent-job.js +12 -8
- package/lib/tools/docker.js +34 -10
- package/lib/tools/github.js +34 -0
- package/lib/tools/telegram.js +106 -0
- package/lib/utils/render-md.js +44 -18
- package/package.json +8 -8
- package/setup/CLAUDE.md +11 -5
- package/setup/lib/providers.mjs +2 -1
- package/setup/lib/targets.mjs +13 -16
- package/setup/lib/telegram.mjs +8 -69
- package/templates/.env.example +0 -7
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
- package/templates/.gitignore.template +1 -3
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.template +29 -7
- package/templates/agent-job/CLAUDE.md.template +5 -3
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/agent-job/SYSTEM.md +16 -11
- package/templates/agents/CLAUDE.md.template +17 -17
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/data/CLAUDE.md.template +1 -1
- package/templates/docker-compose.custom.yml +1 -0
- package/templates/docker-compose.yml +1 -0
- package/templates/event-handler/CLAUDE.md.template +79 -0
- package/templates/event-handler/TRIGGERS.json +18 -2
- package/templates/skills/CLAUDE.md.template +20 -22
- package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/SKILL.md +2 -2
- package/lib/ai/agent.js +0 -65
- package/lib/ai/async-channel.js +0 -51
- package/lib/ai/model.js +0 -130
- package/lib/ai/tools.js +0 -164
- package/lib/tools/openai.js +0 -37
- package/setup/lib/telegram-verify.mjs +0 -63
- package/setup/setup-telegram.mjs +0 -260
- package/templates/agent-job/SOUL.md +0 -17
- /package/templates/{skills/active/.gitkeep → coding-workspace/SYSTEM.md} +0 -0
- /package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/agent-job-secrets.js +0 -0
- /package/templates/skills/{library/playwright-cli → playwright-cli}/SKILL.md +0 -0
|
@@ -133,12 +133,32 @@ function CronCard({ cron }) {
|
|
|
133
133
|
<pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48">
|
|
134
134
|
{cron.job}
|
|
135
135
|
</pre>
|
|
136
|
-
{(cron.
|
|
137
|
-
<div className="flex items-center gap-2 mt-2">
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
{(cron.agent_backend || cron.llm_model || cron.scope) && (
|
|
137
|
+
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
|
138
|
+
{cron.agent_backend && (
|
|
139
|
+
<>
|
|
140
|
+
<span className="text-xs font-medium text-muted-foreground">Agent:</span>
|
|
141
|
+
<span className="inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium">
|
|
142
|
+
{cron.agent_backend}
|
|
143
|
+
</span>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
{cron.llm_model && (
|
|
147
|
+
<>
|
|
148
|
+
<span className="text-xs font-medium text-muted-foreground">Model:</span>
|
|
149
|
+
<span className="inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium">
|
|
150
|
+
{cron.llm_model}
|
|
151
|
+
</span>
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
{cron.scope && (
|
|
155
|
+
<>
|
|
156
|
+
<span className="text-xs font-medium text-muted-foreground">Scope:</span>
|
|
157
|
+
<span className="inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-mono">
|
|
158
|
+
{cron.scope}
|
|
159
|
+
</span>
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
142
162
|
</div>
|
|
143
163
|
)}
|
|
144
164
|
</div>
|
|
@@ -167,6 +187,14 @@ function CronCard({ cron }) {
|
|
|
167
187
|
</pre>
|
|
168
188
|
</div>
|
|
169
189
|
)}
|
|
190
|
+
{cron.headers && Object.keys(cron.headers).length > 0 && (
|
|
191
|
+
<div>
|
|
192
|
+
<p className="text-xs font-medium text-muted-foreground mb-1.5">Headers</p>
|
|
193
|
+
<pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48">
|
|
194
|
+
{JSON.stringify(cron.headers, null, 2)}
|
|
195
|
+
</pre>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
170
198
|
</div>
|
|
171
199
|
)}
|
|
172
200
|
</div>
|
|
@@ -10,13 +10,13 @@ export { SettingsLayout } from './settings-layout.js';
|
|
|
10
10
|
export { SubTabLayout, ApiKeysLayout, EventHandlerLayout, ChatSettingsLayout, GitHubSettingsLayout, SecretsLayout } from './settings-secrets-layout.js';
|
|
11
11
|
export { ApiKeysListPage, ApiKeysVoicePage, ApiKeysTelegramPage, SettingsSecretsPage } from './settings-secrets-page.js';
|
|
12
12
|
export { SettingsUsersPage } from './settings-users-page.js';
|
|
13
|
-
export { ChatConfigPage, ChatProvidersPage, SettingsChatPage } from './settings-chat-page.js';
|
|
13
|
+
export { HelperLlmPage, ChatConfigPage, ChatProvidersPage, SettingsChatPage } from './settings-chat-page.js';
|
|
14
14
|
export { ChatProvidersPage as LlmsPage } from './settings-chat-page.js';
|
|
15
15
|
export { CodingAgentsPage } from './settings-coding-agents-page.js';
|
|
16
16
|
export { JobsPage } from './settings-jobs-page.js';
|
|
17
17
|
export { GitHubTokensPage, GitHubSecretsPage, GitHubVariablesPage, SettingsGitHubPage } from './settings-github-page.js';
|
|
18
18
|
export { SettingsGeneralPage } from './settings-general-page.js';
|
|
19
|
-
export { ProfileLayout, ProfileLoginPage } from './profile-page.js';
|
|
19
|
+
export { ProfileLayout, ProfileLoginPage, ProfileTelegramPage } from './profile-page.js';
|
|
20
20
|
export { AppSidebar } from './app-sidebar.js';
|
|
21
21
|
export { SidebarHistory } from './sidebar-history.js';
|
|
22
22
|
export { SidebarHistoryItem } from './sidebar-history-item.js';
|
|
@@ -363,6 +363,21 @@ function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
|
363
363
|
const afterTool = prevPart?.type?.startsWith("tool-");
|
|
364
364
|
return /* @__PURE__ */ jsx(Streamdown, { className: afterTool ? "mt-3" : void 0, mode: isLoading ? "streaming" : "static", linkSafety, children: part.text }, i);
|
|
365
365
|
}
|
|
366
|
+
if (part.type === "data-error") {
|
|
367
|
+
const prevPart = message.parts[i - 1];
|
|
368
|
+
const afterContent = prevPart?.type === "text" || prevPart?.type?.startsWith("tool-");
|
|
369
|
+
return /* @__PURE__ */ jsx(
|
|
370
|
+
"div",
|
|
371
|
+
{
|
|
372
|
+
className: cn(
|
|
373
|
+
"rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive whitespace-pre-wrap break-words",
|
|
374
|
+
afterContent && "mt-3"
|
|
375
|
+
),
|
|
376
|
+
children: part.data?.message || "An error occurred"
|
|
377
|
+
},
|
|
378
|
+
i
|
|
379
|
+
);
|
|
380
|
+
}
|
|
366
381
|
if (part.type === "file") {
|
|
367
382
|
if (part.mediaType?.startsWith("image/")) {
|
|
368
383
|
return /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsx("img", { src: part.url, alt: "attachment", className: "max-h-64 max-w-full rounded-lg object-contain" }) }, i);
|
|
@@ -381,11 +396,11 @@ function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
|
381
396
|
}),
|
|
382
397
|
showWorking && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
|
|
383
398
|
/* @__PURE__ */ jsx(SpinnerIcon, { size: 14 }),
|
|
384
|
-
/* @__PURE__ */ jsx("span", { children: "Working..." })
|
|
399
|
+
/* @__PURE__ */ jsx("span", { className: "thinking-shimmer", children: "Working..." })
|
|
385
400
|
] })
|
|
386
401
|
] }) : text ? /* @__PURE__ */ jsx(Streamdown, { mode: isLoading ? "streaming" : "static", linkSafety, children: text }) : isLoading && !hasToolParts ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
|
|
387
402
|
/* @__PURE__ */ jsx(SpinnerIcon, { size: 14 }),
|
|
388
|
-
/* @__PURE__ */ jsx("span", { children: "Working..." })
|
|
403
|
+
/* @__PURE__ */ jsx("span", { className: "thinking-shimmer", children: "Working..." })
|
|
389
404
|
] }) : null })
|
|
390
405
|
}
|
|
391
406
|
),
|
|
@@ -434,7 +449,7 @@ function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
|
434
449
|
function ThinkingMessage() {
|
|
435
450
|
return /* @__PURE__ */ jsx("div", { className: "flex gap-4 w-full justify-start", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground", children: [
|
|
436
451
|
/* @__PURE__ */ jsx(SpinnerIcon, { size: 14 }),
|
|
437
|
-
/* @__PURE__ */ jsx("span", { children: "
|
|
452
|
+
/* @__PURE__ */ jsx("span", { className: "thinking-shimmer", children: "Waiting..." })
|
|
438
453
|
] }) });
|
|
439
454
|
}
|
|
440
455
|
export {
|
|
@@ -435,6 +435,21 @@ export function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
|
435
435
|
const afterTool = prevPart?.type?.startsWith('tool-');
|
|
436
436
|
return <Streamdown key={i} className={afterTool ? 'mt-3' : undefined} mode={isLoading ? 'streaming' : 'static'} linkSafety={linkSafety}>{part.text}</Streamdown>;
|
|
437
437
|
}
|
|
438
|
+
if (part.type === 'data-error') {
|
|
439
|
+
const prevPart = message.parts[i - 1];
|
|
440
|
+
const afterContent = prevPart?.type === 'text' || prevPart?.type?.startsWith('tool-');
|
|
441
|
+
return (
|
|
442
|
+
<div
|
|
443
|
+
key={i}
|
|
444
|
+
className={cn(
|
|
445
|
+
'rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive whitespace-pre-wrap break-words',
|
|
446
|
+
afterContent && 'mt-3'
|
|
447
|
+
)}
|
|
448
|
+
>
|
|
449
|
+
{part.data?.message || 'An error occurred'}
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
438
453
|
if (part.type === 'file') {
|
|
439
454
|
if (part.mediaType?.startsWith('image/')) {
|
|
440
455
|
return (
|
|
@@ -460,7 +475,7 @@ export function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
|
460
475
|
{showWorking && (
|
|
461
476
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
462
477
|
<SpinnerIcon size={14} />
|
|
463
|
-
<span>Working...</span>
|
|
478
|
+
<span className="thinking-shimmer">Working...</span>
|
|
464
479
|
</div>
|
|
465
480
|
)}
|
|
466
481
|
</>
|
|
@@ -469,7 +484,7 @@ export function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
|
469
484
|
) : isLoading && !hasToolParts ? (
|
|
470
485
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
471
486
|
<SpinnerIcon size={14} />
|
|
472
|
-
<span>Working...</span>
|
|
487
|
+
<span className="thinking-shimmer">Working...</span>
|
|
473
488
|
</div>
|
|
474
489
|
) : null}
|
|
475
490
|
</>
|
|
@@ -523,7 +538,7 @@ export function ThinkingMessage() {
|
|
|
523
538
|
<div className="flex gap-4 w-full justify-start">
|
|
524
539
|
<div className="flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground">
|
|
525
540
|
<SpinnerIcon size={14} />
|
|
526
|
-
<span>
|
|
541
|
+
<span className="thinking-shimmer">Waiting...</span>
|
|
527
542
|
</div>
|
|
528
543
|
</div>
|
|
529
544
|
);
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
4
|
import { PageLayout } from "./page-layout.js";
|
|
5
|
-
import { KeyIcon } from "./icons.js";
|
|
5
|
+
import { KeyIcon, SendIcon, CopyIcon, CheckIcon } from "./icons.js";
|
|
6
6
|
import { updateProfile } from "../../auth/actions.js";
|
|
7
|
+
import {
|
|
8
|
+
issueTelegramCode,
|
|
9
|
+
unlinkTelegramChannel
|
|
10
|
+
} from "../actions.js";
|
|
7
11
|
const TABS = [
|
|
8
|
-
{ id: "login", label: "Login", href: "/profile/login", icon: KeyIcon }
|
|
12
|
+
{ id: "login", label: "Login", href: "/profile/login", icon: KeyIcon },
|
|
13
|
+
{ id: "telegram", label: "Telegram", href: "/profile/telegram", icon: SendIcon }
|
|
9
14
|
];
|
|
10
15
|
function ProfileLayout({ session, children }) {
|
|
11
16
|
const [activePath, setActivePath] = useState("");
|
|
@@ -135,7 +140,180 @@ function ProfileLoginPage({ session }) {
|
|
|
135
140
|
)
|
|
136
141
|
] });
|
|
137
142
|
}
|
|
143
|
+
function formatCountdown(ms) {
|
|
144
|
+
if (ms <= 0) return "expired";
|
|
145
|
+
const total = Math.ceil(ms / 1e3);
|
|
146
|
+
const m = Math.floor(total / 60);
|
|
147
|
+
const s = total % 60;
|
|
148
|
+
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
149
|
+
}
|
|
150
|
+
function ProfileTelegramPage({ initial }) {
|
|
151
|
+
const [state, setState] = useState(initial);
|
|
152
|
+
const [busy, setBusy] = useState(false);
|
|
153
|
+
const [error, setError] = useState(null);
|
|
154
|
+
const [copied, setCopied] = useState(false);
|
|
155
|
+
const [now, setNow] = useState(() => Date.now());
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (state.status !== "pending") return;
|
|
158
|
+
const id = setInterval(() => setNow(Date.now()), 1e3);
|
|
159
|
+
return () => clearInterval(id);
|
|
160
|
+
}, [state.status]);
|
|
161
|
+
const handleIssue = async () => {
|
|
162
|
+
setBusy(true);
|
|
163
|
+
setError(null);
|
|
164
|
+
try {
|
|
165
|
+
const result = await issueTelegramCode();
|
|
166
|
+
if (result.error) {
|
|
167
|
+
setError(result.error);
|
|
168
|
+
} else {
|
|
169
|
+
setState({
|
|
170
|
+
status: "pending",
|
|
171
|
+
code: result.code,
|
|
172
|
+
expiresAt: result.expiresAt,
|
|
173
|
+
botUsername: result.botUsername ?? state.botUsername
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
setBusy(false);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const handleUnlink = async () => {
|
|
181
|
+
setBusy(true);
|
|
182
|
+
setError(null);
|
|
183
|
+
try {
|
|
184
|
+
await unlinkTelegramChannel();
|
|
185
|
+
setState({ status: "unlinked", botUsername: state.botUsername });
|
|
186
|
+
} finally {
|
|
187
|
+
setBusy(false);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const handleCopy = async () => {
|
|
191
|
+
if (!state.code) return;
|
|
192
|
+
try {
|
|
193
|
+
await navigator.clipboard.writeText(`/verify ${state.code}`);
|
|
194
|
+
setCopied(true);
|
|
195
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const botLink = state.botUsername ? `https://t.me/${state.botUsername}` : null;
|
|
200
|
+
return /* @__PURE__ */ jsxs("div", { className: "max-w-md space-y-6", children: [
|
|
201
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
202
|
+
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Telegram" }),
|
|
203
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Link a Telegram chat to your account to talk to the bot from your phone." })
|
|
204
|
+
] }),
|
|
205
|
+
!state.botUsername && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-yellow-500/30 bg-yellow-500/5 p-3 text-sm text-yellow-500", children: [
|
|
206
|
+
"Telegram bot token is not configured. An admin needs to set",
|
|
207
|
+
/* @__PURE__ */ jsx("code", { className: "mx-1 px-1 rounded bg-muted text-foreground", children: "TELEGRAM_BOT_TOKEN" }),
|
|
208
|
+
"before users can link their chat."
|
|
209
|
+
] }),
|
|
210
|
+
error && /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive", children: error }),
|
|
211
|
+
state.status === "unlinked" && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border p-4 space-y-3", children: [
|
|
212
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
|
|
213
|
+
/* @__PURE__ */ jsx("span", { className: "h-2 w-2 rounded-full bg-muted-foreground" }),
|
|
214
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Not linked" })
|
|
215
|
+
] }),
|
|
216
|
+
/* @__PURE__ */ jsx(
|
|
217
|
+
"button",
|
|
218
|
+
{
|
|
219
|
+
type: "button",
|
|
220
|
+
onClick: handleIssue,
|
|
221
|
+
disabled: busy || !state.botUsername,
|
|
222
|
+
className: "rounded-md px-3 py-1.5 text-sm bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 transition-colors",
|
|
223
|
+
children: busy ? "Generating..." : "Generate code"
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
] }),
|
|
227
|
+
state.status === "pending" && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border p-4 space-y-4", children: [
|
|
228
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
|
|
229
|
+
/* @__PURE__ */ jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500" }),
|
|
230
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
231
|
+
"Waiting for verification \u2014 expires in ",
|
|
232
|
+
formatCountdown(state.expiresAt - now)
|
|
233
|
+
] })
|
|
234
|
+
] }),
|
|
235
|
+
/* @__PURE__ */ jsxs("ol", { className: "text-sm space-y-2 text-muted-foreground list-decimal list-inside", children: [
|
|
236
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
237
|
+
"Open",
|
|
238
|
+
" ",
|
|
239
|
+
botLink ? /* @__PURE__ */ jsxs("a", { className: "text-foreground underline", href: botLink, target: "_blank", rel: "noreferrer", children: [
|
|
240
|
+
"@",
|
|
241
|
+
state.botUsername
|
|
242
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "text-foreground", children: "the bot" }),
|
|
243
|
+
" ",
|
|
244
|
+
"on Telegram."
|
|
245
|
+
] }),
|
|
246
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
247
|
+
"Send this message:",
|
|
248
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-2", children: [
|
|
249
|
+
/* @__PURE__ */ jsxs("code", { className: "flex-1 rounded-md bg-muted px-3 py-2 text-xs text-foreground font-mono", children: [
|
|
250
|
+
"/verify ",
|
|
251
|
+
state.code
|
|
252
|
+
] }),
|
|
253
|
+
/* @__PURE__ */ jsx(
|
|
254
|
+
"button",
|
|
255
|
+
{
|
|
256
|
+
type: "button",
|
|
257
|
+
onClick: handleCopy,
|
|
258
|
+
className: "inline-flex items-center gap-1 rounded-md border border-border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
259
|
+
children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
260
|
+
/* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
|
|
261
|
+
" Copied"
|
|
262
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
263
|
+
/* @__PURE__ */ jsx(CopyIcon, { size: 12 }),
|
|
264
|
+
" Copy"
|
|
265
|
+
] })
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
] })
|
|
269
|
+
] })
|
|
270
|
+
] }),
|
|
271
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
272
|
+
/* @__PURE__ */ jsx(
|
|
273
|
+
"button",
|
|
274
|
+
{
|
|
275
|
+
type: "button",
|
|
276
|
+
onClick: handleIssue,
|
|
277
|
+
disabled: busy,
|
|
278
|
+
className: "rounded-md border border-border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 transition-colors",
|
|
279
|
+
children: busy ? "Regenerating..." : "Regenerate"
|
|
280
|
+
}
|
|
281
|
+
),
|
|
282
|
+
/* @__PURE__ */ jsx(
|
|
283
|
+
"button",
|
|
284
|
+
{
|
|
285
|
+
type: "button",
|
|
286
|
+
onClick: handleUnlink,
|
|
287
|
+
disabled: busy,
|
|
288
|
+
className: "rounded-md border border-border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 transition-colors",
|
|
289
|
+
children: "Cancel"
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
] })
|
|
293
|
+
] }),
|
|
294
|
+
state.status === "verified" && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border p-4 space-y-3", children: [
|
|
295
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
|
|
296
|
+
/* @__PURE__ */ jsx("span", { className: "h-2 w-2 rounded-full bg-green-500" }),
|
|
297
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
298
|
+
"Linked to Telegram chat ",
|
|
299
|
+
/* @__PURE__ */ jsx("code", { className: "text-foreground", children: state.channelChatId })
|
|
300
|
+
] })
|
|
301
|
+
] }),
|
|
302
|
+
/* @__PURE__ */ jsx(
|
|
303
|
+
"button",
|
|
304
|
+
{
|
|
305
|
+
type: "button",
|
|
306
|
+
onClick: handleUnlink,
|
|
307
|
+
disabled: busy,
|
|
308
|
+
className: "rounded-md border border-destructive px-2.5 py-1.5 text-xs text-destructive hover:bg-destructive/10 disabled:opacity-50 transition-colors",
|
|
309
|
+
children: busy ? "Unlinking..." : "Unlink"
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
] })
|
|
313
|
+
] });
|
|
314
|
+
}
|
|
138
315
|
export {
|
|
139
316
|
ProfileLayout,
|
|
140
|
-
ProfileLoginPage
|
|
317
|
+
ProfileLoginPage,
|
|
318
|
+
ProfileTelegramPage
|
|
141
319
|
};
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { PageLayout } from './page-layout.js';
|
|
5
|
-
import { KeyIcon } from './icons.js';
|
|
5
|
+
import { KeyIcon, SendIcon, CopyIcon, CheckIcon } from './icons.js';
|
|
6
6
|
import { updateProfile } from '../../auth/actions.js';
|
|
7
|
+
import {
|
|
8
|
+
issueTelegramCode,
|
|
9
|
+
unlinkTelegramChannel,
|
|
10
|
+
} from '../actions.js';
|
|
7
11
|
|
|
8
12
|
const TABS = [
|
|
9
13
|
{ id: 'login', label: 'Login', href: '/profile/login', icon: KeyIcon },
|
|
14
|
+
{ id: 'telegram', label: 'Telegram', href: '/profile/telegram', icon: SendIcon },
|
|
10
15
|
];
|
|
11
16
|
|
|
12
17
|
export function ProfileLayout({ session, children }) {
|
|
@@ -166,3 +171,193 @@ export function ProfileLoginPage({ session }) {
|
|
|
166
171
|
</form>
|
|
167
172
|
);
|
|
168
173
|
}
|
|
174
|
+
|
|
175
|
+
function formatCountdown(ms) {
|
|
176
|
+
if (ms <= 0) return 'expired';
|
|
177
|
+
const total = Math.ceil(ms / 1000);
|
|
178
|
+
const m = Math.floor(total / 60);
|
|
179
|
+
const s = total % 60;
|
|
180
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Telegram linking UI. Initial state is server-rendered (passed via `initial`);
|
|
185
|
+
* mutations use server actions, which return the new state.
|
|
186
|
+
*/
|
|
187
|
+
export function ProfileTelegramPage({ initial }) {
|
|
188
|
+
const [state, setState] = useState(initial);
|
|
189
|
+
const [busy, setBusy] = useState(false);
|
|
190
|
+
const [error, setError] = useState(null);
|
|
191
|
+
const [copied, setCopied] = useState(false);
|
|
192
|
+
const [now, setNow] = useState(() => Date.now());
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (state.status !== 'pending') return;
|
|
196
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
197
|
+
return () => clearInterval(id);
|
|
198
|
+
}, [state.status]);
|
|
199
|
+
|
|
200
|
+
const handleIssue = async () => {
|
|
201
|
+
setBusy(true);
|
|
202
|
+
setError(null);
|
|
203
|
+
try {
|
|
204
|
+
const result = await issueTelegramCode();
|
|
205
|
+
if (result.error) {
|
|
206
|
+
setError(result.error);
|
|
207
|
+
} else {
|
|
208
|
+
setState({
|
|
209
|
+
status: 'pending',
|
|
210
|
+
code: result.code,
|
|
211
|
+
expiresAt: result.expiresAt,
|
|
212
|
+
botUsername: result.botUsername ?? state.botUsername,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
setBusy(false);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const handleUnlink = async () => {
|
|
221
|
+
setBusy(true);
|
|
222
|
+
setError(null);
|
|
223
|
+
try {
|
|
224
|
+
await unlinkTelegramChannel();
|
|
225
|
+
setState({ status: 'unlinked', botUsername: state.botUsername });
|
|
226
|
+
} finally {
|
|
227
|
+
setBusy(false);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const handleCopy = async () => {
|
|
232
|
+
if (!state.code) return;
|
|
233
|
+
try {
|
|
234
|
+
await navigator.clipboard.writeText(`/verify ${state.code}`);
|
|
235
|
+
setCopied(true);
|
|
236
|
+
setTimeout(() => setCopied(false), 2000);
|
|
237
|
+
} catch {}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const botLink = state.botUsername
|
|
241
|
+
? `https://t.me/${state.botUsername}`
|
|
242
|
+
: null;
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div className="max-w-md space-y-6">
|
|
246
|
+
<div>
|
|
247
|
+
<h2 className="text-base font-medium">Telegram</h2>
|
|
248
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
249
|
+
Link a Telegram chat to your account to talk to the bot from your phone.
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{!state.botUsername && (
|
|
254
|
+
<div className="rounded-lg border border-yellow-500/30 bg-yellow-500/5 p-3 text-sm text-yellow-500">
|
|
255
|
+
Telegram bot token is not configured. An admin needs to set
|
|
256
|
+
<code className="mx-1 px-1 rounded bg-muted text-foreground">TELEGRAM_BOT_TOKEN</code>
|
|
257
|
+
before users can link their chat.
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{error && (
|
|
262
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
|
|
263
|
+
{error}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{state.status === 'unlinked' && (
|
|
268
|
+
<div className="rounded-lg border border-border p-4 space-y-3">
|
|
269
|
+
<div className="flex items-center gap-2 text-sm">
|
|
270
|
+
<span className="h-2 w-2 rounded-full bg-muted-foreground" />
|
|
271
|
+
<span className="text-muted-foreground">Not linked</span>
|
|
272
|
+
</div>
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={handleIssue}
|
|
276
|
+
disabled={busy || !state.botUsername}
|
|
277
|
+
className="rounded-md px-3 py-1.5 text-sm bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 transition-colors"
|
|
278
|
+
>
|
|
279
|
+
{busy ? 'Generating...' : 'Generate code'}
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{state.status === 'pending' && (
|
|
285
|
+
<div className="rounded-lg border border-border p-4 space-y-4">
|
|
286
|
+
<div className="flex items-center gap-2 text-sm">
|
|
287
|
+
<span className="h-2 w-2 rounded-full bg-yellow-500" />
|
|
288
|
+
<span className="text-muted-foreground">
|
|
289
|
+
Waiting for verification — expires in {formatCountdown(state.expiresAt - now)}
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<ol className="text-sm space-y-2 text-muted-foreground list-decimal list-inside">
|
|
294
|
+
<li>
|
|
295
|
+
Open{' '}
|
|
296
|
+
{botLink ? (
|
|
297
|
+
<a className="text-foreground underline" href={botLink} target="_blank" rel="noreferrer">
|
|
298
|
+
@{state.botUsername}
|
|
299
|
+
</a>
|
|
300
|
+
) : (
|
|
301
|
+
<span className="text-foreground">the bot</span>
|
|
302
|
+
)}{' '}
|
|
303
|
+
on Telegram.
|
|
304
|
+
</li>
|
|
305
|
+
<li>
|
|
306
|
+
Send this message:
|
|
307
|
+
<div className="mt-2 flex items-center gap-2">
|
|
308
|
+
<code className="flex-1 rounded-md bg-muted px-3 py-2 text-xs text-foreground font-mono">
|
|
309
|
+
/verify {state.code}
|
|
310
|
+
</code>
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
onClick={handleCopy}
|
|
314
|
+
className="inline-flex items-center gap-1 rounded-md border border-border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
315
|
+
>
|
|
316
|
+
{copied ? <><CheckIcon size={12} /> Copied</> : <><CopyIcon size={12} /> Copy</>}
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
</li>
|
|
320
|
+
</ol>
|
|
321
|
+
|
|
322
|
+
<div className="flex gap-2">
|
|
323
|
+
<button
|
|
324
|
+
type="button"
|
|
325
|
+
onClick={handleIssue}
|
|
326
|
+
disabled={busy}
|
|
327
|
+
className="rounded-md border border-border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 transition-colors"
|
|
328
|
+
>
|
|
329
|
+
{busy ? 'Regenerating...' : 'Regenerate'}
|
|
330
|
+
</button>
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
onClick={handleUnlink}
|
|
334
|
+
disabled={busy}
|
|
335
|
+
className="rounded-md border border-border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 transition-colors"
|
|
336
|
+
>
|
|
337
|
+
Cancel
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{state.status === 'verified' && (
|
|
344
|
+
<div className="rounded-lg border border-border p-4 space-y-3">
|
|
345
|
+
<div className="flex items-center gap-2 text-sm">
|
|
346
|
+
<span className="h-2 w-2 rounded-full bg-green-500" />
|
|
347
|
+
<span className="text-muted-foreground">
|
|
348
|
+
Linked to Telegram chat <code className="text-foreground">{state.channelChatId}</code>
|
|
349
|
+
</span>
|
|
350
|
+
</div>
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
onClick={handleUnlink}
|
|
354
|
+
disabled={busy}
|
|
355
|
+
className="rounded-md border border-destructive px-2.5 py-1.5 text-xs text-destructive hover:bg-destructive/10 disabled:opacity-50 transition-colors"
|
|
356
|
+
>
|
|
357
|
+
{busy ? 'Unlinking...' : 'Unlink'}
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Combobox } from "./ui/combobox.js";
|
|
4
|
+
function ScopePicker({ scope, onScopeChange, scopes }) {
|
|
5
|
+
const options = [
|
|
6
|
+
{ value: "/", label: "/ (root)" },
|
|
7
|
+
...(scopes || []).map((s) => ({ value: s.path, label: `/${s.path}` }))
|
|
8
|
+
];
|
|
9
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-center gap-3", children: /* @__PURE__ */ jsx("div", { className: "w-full sm:w-auto sm:min-w-[240px] sm:max-w-[240px]", children: /* @__PURE__ */ jsx(
|
|
10
|
+
Combobox,
|
|
11
|
+
{
|
|
12
|
+
options,
|
|
13
|
+
value: scope || "/",
|
|
14
|
+
onChange: (val) => onScopeChange(val === "/" ? null : val),
|
|
15
|
+
placeholder: "Select scope..."
|
|
16
|
+
}
|
|
17
|
+
) }) });
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
ScopePicker
|
|
21
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Combobox } from './ui/combobox.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scope picker for agent mode — selects which subdirectory the agent runs in.
|
|
7
|
+
* Defaults to root when no scope is selected.
|
|
8
|
+
*/
|
|
9
|
+
export function ScopePicker({ scope, onScopeChange, scopes }) {
|
|
10
|
+
const options = [
|
|
11
|
+
{ value: '/', label: '/ (root)' },
|
|
12
|
+
...(scopes || []).map((s) => ({ value: s.path, label: `/${s.path}` })),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
17
|
+
<div className="w-full sm:w-auto sm:min-w-[240px] sm:max-w-[240px]">
|
|
18
|
+
<Combobox
|
|
19
|
+
options={options}
|
|
20
|
+
value={scope || '/'}
|
|
21
|
+
onChange={(val) => onScopeChange(val === '/' ? null : val)}
|
|
22
|
+
placeholder="Select scope..."
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|