thepopebot 1.1.2 → 1.2.3
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 +1 -1
- package/api/index.js +72 -165
- package/bin/cli.js +36 -6
- package/bin/local.sh +31 -0
- package/bin/postinstall.js +6 -2
- package/config/index.js +2 -11
- package/config/instrumentation.js +17 -5
- package/lib/actions.js +7 -6
- package/lib/ai/agent.js +36 -0
- package/lib/ai/index.js +274 -0
- package/lib/ai/model.js +67 -0
- package/lib/ai/tools.js +49 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +45 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +30 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +146 -0
- package/lib/chat/actions.js +239 -0
- package/lib/chat/api.js +103 -0
- package/lib/chat/components/app-sidebar.js +161 -0
- package/lib/chat/components/app-sidebar.jsx +214 -0
- package/lib/chat/components/chat-header.js +9 -0
- package/lib/chat/components/chat-header.jsx +14 -0
- package/lib/chat/components/chat-input.js +230 -0
- package/lib/chat/components/chat-input.jsx +232 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +70 -0
- package/lib/chat/components/chat-page.jsx +89 -0
- package/lib/chat/components/chat.js +78 -0
- package/lib/chat/components/chat.jsx +91 -0
- package/lib/chat/components/chats-page.js +170 -0
- package/lib/chat/components/chats-page.jsx +203 -0
- package/lib/chat/components/crons-page.js +144 -0
- package/lib/chat/components/crons-page.jsx +204 -0
- package/lib/chat/components/greeting.js +11 -0
- package/lib/chat/components/greeting.jsx +14 -0
- package/lib/chat/components/icons.js +518 -0
- package/lib/chat/components/icons.jsx +482 -0
- package/lib/chat/components/index.js +19 -0
- package/lib/chat/components/message.js +66 -0
- package/lib/chat/components/message.jsx +92 -0
- package/lib/chat/components/messages.js +63 -0
- package/lib/chat/components/messages.jsx +72 -0
- package/lib/chat/components/notifications-page.js +54 -0
- package/lib/chat/components/notifications-page.jsx +83 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/settings-layout.js +37 -0
- package/lib/chat/components/settings-layout.jsx +51 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +54 -0
- package/lib/chat/components/sidebar-history-item.jsx +50 -0
- package/lib/chat/components/sidebar-history.js +92 -0
- package/lib/chat/components/sidebar-history.jsx +132 -0
- package/lib/chat/components/sidebar-user-nav.js +59 -0
- package/lib/chat/components/sidebar-user-nav.jsx +69 -0
- package/lib/chat/components/swarm-page.js +250 -0
- package/lib/chat/components/swarm-page.jsx +356 -0
- package/lib/chat/components/triggers-page.js +121 -0
- package/lib/chat/components/triggers-page.jsx +177 -0
- package/lib/chat/components/ui/dropdown-menu.js +98 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +227 -0
- package/lib/chat/components/ui/sidebar.jsx +245 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/utils.js +11 -0
- package/lib/cron.js +7 -8
- package/lib/db/api-keys.js +160 -0
- package/lib/db/chats.js +129 -0
- package/lib/db/index.js +106 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +51 -0
- package/lib/db/users.js +89 -0
- package/lib/paths.js +23 -17
- package/lib/tools/create-job.js +3 -3
- package/lib/tools/github.js +145 -1
- package/lib/tools/openai.js +1 -1
- package/lib/tools/telegram.js +4 -3
- package/lib/triggers.js +6 -7
- package/lib/utils/render-md.js +6 -6
- package/package.json +43 -6
- package/setup/lib/auth.mjs +22 -9
- package/setup/lib/prerequisites.mjs +10 -3
- package/setup/lib/telegram-verify.mjs +3 -16
- package/setup/setup-telegram.mjs +31 -62
- package/setup/setup.mjs +58 -98
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +18 -2
- package/templates/.github/workflows/auto-merge.yml +1 -1
- package/templates/.github/workflows/build-image.yml +6 -4
- package/templates/.github/workflows/notify-job-failed.yml +2 -2
- package/templates/.github/workflows/notify-pr-complete.yml +2 -2
- package/templates/.github/workflows/run-job.yml +24 -10
- package/templates/CLAUDE.md +5 -3
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/api/chat/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +8 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/components/ascii-logo.jsx +10 -0
- package/templates/app/components/login-form.jsx +81 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +7 -0
- package/templates/app/globals.css +66 -0
- package/templates/app/layout.js +9 -2
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +6 -30
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/triggers/page.js +7 -0
- package/templates/config/CRONS.json +2 -2
- package/templates/config/TRIGGERS.json +2 -2
- package/templates/docker/event_handler/Dockerfile +19 -0
- package/templates/docker/{entrypoint.sh → job/entrypoint.sh} +4 -4
- package/templates/docker/runner/Dockerfile +38 -0
- package/templates/docker/runner/entrypoint.sh +41 -0
- package/templates/docker-compose.yml +52 -0
- package/templates/instrumentation.js +6 -1
- package/templates/middleware.js +1 -0
- package/templates/postcss.config.mjs +5 -0
- package/lib/claude/conversation.js +0 -76
- package/lib/claude/index.js +0 -142
- package/lib/claude/tools.js +0 -54
- /package/templates/docker/{Dockerfile → job/Dockerfile} +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useEffect, useState } from "react";
|
|
4
|
+
import { PreviewMessage, ThinkingMessage } from "./message.js";
|
|
5
|
+
import { Greeting } from "./greeting.js";
|
|
6
|
+
import { ArrowDown } from "lucide-react";
|
|
7
|
+
function Messages({ messages, status }) {
|
|
8
|
+
const containerRef = useRef(null);
|
|
9
|
+
const endRef = useRef(null);
|
|
10
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (isAtBottom && endRef.current) {
|
|
13
|
+
endRef.current.scrollIntoView({ behavior: "smooth" });
|
|
14
|
+
}
|
|
15
|
+
}, [messages, status, isAtBottom]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const container = containerRef.current;
|
|
18
|
+
if (!container) return;
|
|
19
|
+
const handleScroll = () => {
|
|
20
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
21
|
+
setIsAtBottom(scrollHeight - scrollTop - clientHeight < 40);
|
|
22
|
+
};
|
|
23
|
+
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
24
|
+
return () => container.removeEventListener("scroll", handleScroll);
|
|
25
|
+
}, []);
|
|
26
|
+
const scrollToBottom = () => {
|
|
27
|
+
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
28
|
+
};
|
|
29
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
"div",
|
|
32
|
+
{
|
|
33
|
+
className: "absolute inset-0 touch-pan-y overflow-y-auto",
|
|
34
|
+
ref: containerRef,
|
|
35
|
+
children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex min-w-0 max-w-4xl flex-col gap-4 px-2 py-4 md:gap-6 md:px-4", children: [
|
|
36
|
+
messages.length === 0 && /* @__PURE__ */ jsx(Greeting, {}),
|
|
37
|
+
messages.map((message, index) => /* @__PURE__ */ jsx(
|
|
38
|
+
PreviewMessage,
|
|
39
|
+
{
|
|
40
|
+
message,
|
|
41
|
+
isLoading: status === "streaming" && index === messages.length - 1
|
|
42
|
+
},
|
|
43
|
+
message.id
|
|
44
|
+
)),
|
|
45
|
+
status === "submitted" && /* @__PURE__ */ jsx(ThinkingMessage, {}),
|
|
46
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-[24px] shrink-0", ref: endRef })
|
|
47
|
+
] })
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
!isAtBottom && /* @__PURE__ */ jsx(
|
|
51
|
+
"button",
|
|
52
|
+
{
|
|
53
|
+
className: "absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full border border-border bg-background p-2 shadow-lg hover:bg-muted",
|
|
54
|
+
onClick: scrollToBottom,
|
|
55
|
+
"aria-label": "Scroll to bottom",
|
|
56
|
+
children: /* @__PURE__ */ jsx(ArrowDown, { className: "size-4" })
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
] });
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
Messages
|
|
63
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect, useState } from 'react';
|
|
4
|
+
import { PreviewMessage, ThinkingMessage } from './message.js';
|
|
5
|
+
import { Greeting } from './greeting.js';
|
|
6
|
+
import { ArrowDown } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export function Messages({ messages, status }) {
|
|
9
|
+
const containerRef = useRef(null);
|
|
10
|
+
const endRef = useRef(null);
|
|
11
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
12
|
+
|
|
13
|
+
// Auto-scroll to bottom when new messages arrive
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (isAtBottom && endRef.current) {
|
|
16
|
+
endRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
17
|
+
}
|
|
18
|
+
}, [messages, status, isAtBottom]);
|
|
19
|
+
|
|
20
|
+
// Track scroll position
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const container = containerRef.current;
|
|
23
|
+
if (!container) return;
|
|
24
|
+
|
|
25
|
+
const handleScroll = () => {
|
|
26
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
27
|
+
setIsAtBottom(scrollHeight - scrollTop - clientHeight < 40);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
container.addEventListener('scroll', handleScroll, { passive: true });
|
|
31
|
+
return () => container.removeEventListener('scroll', handleScroll);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const scrollToBottom = () => {
|
|
35
|
+
endRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="relative flex-1">
|
|
40
|
+
<div
|
|
41
|
+
className="absolute inset-0 touch-pan-y overflow-y-auto"
|
|
42
|
+
ref={containerRef}
|
|
43
|
+
>
|
|
44
|
+
<div className="mx-auto flex min-w-0 max-w-4xl flex-col gap-4 px-2 py-4 md:gap-6 md:px-4">
|
|
45
|
+
{messages.length === 0 && <Greeting />}
|
|
46
|
+
|
|
47
|
+
{messages.map((message, index) => (
|
|
48
|
+
<PreviewMessage
|
|
49
|
+
key={message.id}
|
|
50
|
+
message={message}
|
|
51
|
+
isLoading={status === 'streaming' && index === messages.length - 1}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
|
|
55
|
+
{status === 'submitted' && <ThinkingMessage />}
|
|
56
|
+
|
|
57
|
+
<div className="min-h-[24px] shrink-0" ref={endRef} />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{!isAtBottom && (
|
|
62
|
+
<button
|
|
63
|
+
className="absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full border border-border bg-background p-2 shadow-lg hover:bg-muted"
|
|
64
|
+
onClick={scrollToBottom}
|
|
65
|
+
aria-label="Scroll to bottom"
|
|
66
|
+
>
|
|
67
|
+
<ArrowDown className="size-4" />
|
|
68
|
+
</button>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { PageLayout } from "./page-layout.js";
|
|
5
|
+
import { BellIcon } from "./icons.js";
|
|
6
|
+
import { getNotifications, markNotificationsRead } from "../actions.js";
|
|
7
|
+
function timeAgo(ts) {
|
|
8
|
+
const seconds = Math.floor((Date.now() - ts) / 1e3);
|
|
9
|
+
if (seconds < 60) return "just now";
|
|
10
|
+
const minutes = Math.floor(seconds / 60);
|
|
11
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
12
|
+
const hours = Math.floor(minutes / 60);
|
|
13
|
+
if (hours < 24) return `${hours}h ago`;
|
|
14
|
+
const days = Math.floor(hours / 24);
|
|
15
|
+
if (days < 30) return `${days}d ago`;
|
|
16
|
+
const months = Math.floor(days / 30);
|
|
17
|
+
return `${months}mo ago`;
|
|
18
|
+
}
|
|
19
|
+
function NotificationsPage({ session }) {
|
|
20
|
+
const [notifications, setNotifications] = useState([]);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
async function load() {
|
|
24
|
+
try {
|
|
25
|
+
const result = await getNotifications();
|
|
26
|
+
setNotifications(result);
|
|
27
|
+
await markNotificationsRead();
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error("Failed to load notifications:", err);
|
|
30
|
+
} finally {
|
|
31
|
+
setLoading(false);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
load();
|
|
35
|
+
}, []);
|
|
36
|
+
return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
|
|
37
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-between mb-6", children: /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Notifications" }) }),
|
|
38
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mb-4", children: [
|
|
39
|
+
notifications.length,
|
|
40
|
+
" ",
|
|
41
|
+
notifications.length === 1 ? "notification" : "notifications"
|
|
42
|
+
] }),
|
|
43
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i)) }) : notifications.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-8 text-center", children: "No notifications yet." }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y divide-border", children: notifications.map((n) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 px-3 py-3", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("div", { className: "mt-0.5 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(BellIcon, { size: 16 }) }),
|
|
45
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
46
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm whitespace-pre-wrap", children: n.notification }),
|
|
47
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: timeAgo(n.createdAt) })
|
|
48
|
+
] })
|
|
49
|
+
] }, n.id)) })
|
|
50
|
+
] });
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
NotificationsPage
|
|
54
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { PageLayout } from './page-layout.js';
|
|
5
|
+
import { BellIcon } from './icons.js';
|
|
6
|
+
import { getNotifications, markNotificationsRead } from '../actions.js';
|
|
7
|
+
|
|
8
|
+
function timeAgo(ts) {
|
|
9
|
+
const seconds = Math.floor((Date.now() - ts) / 1000);
|
|
10
|
+
if (seconds < 60) return 'just now';
|
|
11
|
+
const minutes = Math.floor(seconds / 60);
|
|
12
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
13
|
+
const hours = Math.floor(minutes / 60);
|
|
14
|
+
if (hours < 24) return `${hours}h ago`;
|
|
15
|
+
const days = Math.floor(hours / 24);
|
|
16
|
+
if (days < 30) return `${days}d ago`;
|
|
17
|
+
const months = Math.floor(days / 30);
|
|
18
|
+
return `${months}mo ago`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function NotificationsPage({ session }) {
|
|
22
|
+
const [notifications, setNotifications] = useState([]);
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
async function load() {
|
|
27
|
+
try {
|
|
28
|
+
const result = await getNotifications();
|
|
29
|
+
setNotifications(result);
|
|
30
|
+
// Mark all as read on view
|
|
31
|
+
await markNotificationsRead();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error('Failed to load notifications:', err);
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
load();
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<PageLayout session={session}>
|
|
43
|
+
{/* Header */}
|
|
44
|
+
<div className="flex items-center justify-between mb-6">
|
|
45
|
+
<h1 className="text-2xl font-semibold">Notifications</h1>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Count */}
|
|
49
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
50
|
+
{notifications.length} {notifications.length === 1 ? 'notification' : 'notifications'}
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
{/* Notification list */}
|
|
54
|
+
{loading ? (
|
|
55
|
+
<div className="flex flex-col gap-3">
|
|
56
|
+
{[...Array(5)].map((_, i) => (
|
|
57
|
+
<div key={i} className="h-14 animate-pulse rounded-md bg-border/50" />
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
) : notifications.length === 0 ? (
|
|
61
|
+
<p className="text-sm text-muted-foreground py-8 text-center">
|
|
62
|
+
No notifications yet.
|
|
63
|
+
</p>
|
|
64
|
+
) : (
|
|
65
|
+
<div className="flex flex-col divide-y divide-border">
|
|
66
|
+
{notifications.map((n) => (
|
|
67
|
+
<div key={n.id} className="flex items-start gap-3 px-3 py-3">
|
|
68
|
+
<div className="mt-0.5 shrink-0 text-muted-foreground">
|
|
69
|
+
<BellIcon size={16} />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="flex-1 min-w-0">
|
|
72
|
+
<p className="text-sm whitespace-pre-wrap">{n.notification}</p>
|
|
73
|
+
<span className="text-xs text-muted-foreground">
|
|
74
|
+
{timeAgo(n.createdAt)}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</PageLayout>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AppSidebar } from "./app-sidebar.js";
|
|
4
|
+
import { SidebarProvider, SidebarInset } from "./ui/sidebar.js";
|
|
5
|
+
import { ChatNavProvider } from "./chat-nav-context.js";
|
|
6
|
+
function defaultNavigateToChat(id) {
|
|
7
|
+
if (id) {
|
|
8
|
+
window.location.href = `/chat/${id}`;
|
|
9
|
+
} else {
|
|
10
|
+
window.location.href = "/";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function PageLayout({ session, children }) {
|
|
14
|
+
return /* @__PURE__ */ jsx(ChatNavProvider, { value: { activeChatId: null, navigateToChat: defaultNavigateToChat }, children: /* @__PURE__ */ jsxs(SidebarProvider, { children: [
|
|
15
|
+
/* @__PURE__ */ jsx(AppSidebar, { user: session.user }),
|
|
16
|
+
/* @__PURE__ */ jsx(SidebarInset, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-col h-full max-w-4xl mx-auto w-full px-4 py-6", children }) })
|
|
17
|
+
] }) });
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
PageLayout
|
|
21
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AppSidebar } from './app-sidebar.js';
|
|
4
|
+
import { SidebarProvider, SidebarInset } from './ui/sidebar.js';
|
|
5
|
+
import { ChatNavProvider } from './chat-nav-context.js';
|
|
6
|
+
|
|
7
|
+
function defaultNavigateToChat(id) {
|
|
8
|
+
if (id) {
|
|
9
|
+
window.location.href = `/chat/${id}`;
|
|
10
|
+
} else {
|
|
11
|
+
window.location.href = '/';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PageLayout({ session, children }) {
|
|
16
|
+
return (
|
|
17
|
+
<ChatNavProvider value={{ activeChatId: null, navigateToChat: defaultNavigateToChat }}>
|
|
18
|
+
<SidebarProvider>
|
|
19
|
+
<AppSidebar user={session.user} />
|
|
20
|
+
<SidebarInset>
|
|
21
|
+
<div className="flex flex-col h-full max-w-4xl mx-auto w-full px-4 py-6">
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
</SidebarInset>
|
|
25
|
+
</SidebarProvider>
|
|
26
|
+
</ChatNavProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { PageLayout } from "./page-layout.js";
|
|
5
|
+
import { KeyIcon } from "./icons.js";
|
|
6
|
+
const TABS = [
|
|
7
|
+
{ id: "secrets", label: "Secrets", href: "/settings/secrets", icon: KeyIcon }
|
|
8
|
+
];
|
|
9
|
+
function SettingsLayout({ session, children }) {
|
|
10
|
+
const [activePath, setActivePath] = useState("");
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setActivePath(window.location.pathname);
|
|
13
|
+
}, []);
|
|
14
|
+
return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
|
|
15
|
+
/* @__PURE__ */ jsx("div", { className: "mb-6", children: /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Settings" }) }),
|
|
16
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b border-border mb-6", children: TABS.map((tab) => {
|
|
17
|
+
const isActive = activePath === tab.href || activePath.startsWith(tab.href + "/");
|
|
18
|
+
const Icon = tab.icon;
|
|
19
|
+
return /* @__PURE__ */ jsxs(
|
|
20
|
+
"a",
|
|
21
|
+
{
|
|
22
|
+
href: tab.href,
|
|
23
|
+
className: `inline-flex items-center gap-2 px-3 py-2 text-sm font-medium border-b-2 transition-colors ${isActive ? "border-foreground text-foreground" : "border-transparent text-muted-foreground hover:text-foreground hover:border-border"}`,
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ jsx(Icon, { size: 14 }),
|
|
26
|
+
tab.label
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
tab.id
|
|
30
|
+
);
|
|
31
|
+
}) }),
|
|
32
|
+
children
|
|
33
|
+
] });
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
SettingsLayout
|
|
37
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { PageLayout } from './page-layout.js';
|
|
5
|
+
import { KeyIcon } from './icons.js';
|
|
6
|
+
|
|
7
|
+
const TABS = [
|
|
8
|
+
{ id: 'secrets', label: 'Secrets', href: '/settings/secrets', icon: KeyIcon },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function SettingsLayout({ session, children }) {
|
|
12
|
+
const [activePath, setActivePath] = useState('');
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
setActivePath(window.location.pathname);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<PageLayout session={session}>
|
|
20
|
+
{/* Header */}
|
|
21
|
+
<div className="mb-6">
|
|
22
|
+
<h1 className="text-2xl font-semibold">Settings</h1>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
{/* Tab navigation */}
|
|
26
|
+
<div className="flex gap-1 border-b border-border mb-6">
|
|
27
|
+
{TABS.map((tab) => {
|
|
28
|
+
const isActive = activePath === tab.href || activePath.startsWith(tab.href + '/');
|
|
29
|
+
const Icon = tab.icon;
|
|
30
|
+
return (
|
|
31
|
+
<a
|
|
32
|
+
key={tab.id}
|
|
33
|
+
href={tab.href}
|
|
34
|
+
className={`inline-flex items-center gap-2 px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
35
|
+
isActive
|
|
36
|
+
? 'border-foreground text-foreground'
|
|
37
|
+
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
|
38
|
+
}`}
|
|
39
|
+
>
|
|
40
|
+
<Icon size={14} />
|
|
41
|
+
{tab.label}
|
|
42
|
+
</a>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Tab content */}
|
|
48
|
+
{children}
|
|
49
|
+
</PageLayout>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { KeyIcon, CopyIcon, CheckIcon, TrashIcon, RefreshIcon } from "./icons.js";
|
|
5
|
+
import { createNewApiKey, getApiKeys, deleteApiKey } from "../actions.js";
|
|
6
|
+
function timeAgo(ts) {
|
|
7
|
+
if (!ts) return "Never";
|
|
8
|
+
const seconds = Math.floor((Date.now() - ts) / 1e3);
|
|
9
|
+
if (seconds < 60) return "just now";
|
|
10
|
+
const minutes = Math.floor(seconds / 60);
|
|
11
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
12
|
+
const hours = Math.floor(minutes / 60);
|
|
13
|
+
if (hours < 24) return `${hours}h ago`;
|
|
14
|
+
const days = Math.floor(hours / 24);
|
|
15
|
+
if (days < 30) return `${days}d ago`;
|
|
16
|
+
const months = Math.floor(days / 30);
|
|
17
|
+
return `${months}mo ago`;
|
|
18
|
+
}
|
|
19
|
+
function formatDate(ts) {
|
|
20
|
+
if (!ts) return "\u2014";
|
|
21
|
+
return new Date(ts).toLocaleDateString(void 0, {
|
|
22
|
+
year: "numeric",
|
|
23
|
+
month: "short",
|
|
24
|
+
day: "numeric"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function CopyButton({ text }) {
|
|
28
|
+
const [copied, setCopied] = useState(false);
|
|
29
|
+
const handleCopy = async () => {
|
|
30
|
+
try {
|
|
31
|
+
await navigator.clipboard.writeText(text);
|
|
32
|
+
setCopied(true);
|
|
33
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
34
|
+
} catch {
|
|
35
|
+
const textarea = document.createElement("textarea");
|
|
36
|
+
textarea.value = text;
|
|
37
|
+
document.body.appendChild(textarea);
|
|
38
|
+
textarea.select();
|
|
39
|
+
document.execCommand("copy");
|
|
40
|
+
document.body.removeChild(textarea);
|
|
41
|
+
setCopied(true);
|
|
42
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return /* @__PURE__ */ jsxs(
|
|
46
|
+
"button",
|
|
47
|
+
{
|
|
48
|
+
onClick: handleCopy,
|
|
49
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
50
|
+
children: [
|
|
51
|
+
copied ? /* @__PURE__ */ jsx(CheckIcon, { size: 14 }) : /* @__PURE__ */ jsx(CopyIcon, { size: 14 }),
|
|
52
|
+
copied ? "Copied" : "Copy"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
function Section({ title, description, children }) {
|
|
58
|
+
return /* @__PURE__ */ jsxs("div", { className: "pb-8 mb-8 border-b border-border last:border-b-0 last:pb-0 last:mb-0", children: [
|
|
59
|
+
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium mb-1", children: title }),
|
|
60
|
+
description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: description }),
|
|
61
|
+
children
|
|
62
|
+
] });
|
|
63
|
+
}
|
|
64
|
+
function ApiKeySection() {
|
|
65
|
+
const [currentKey, setCurrentKey] = useState(null);
|
|
66
|
+
const [loading, setLoading] = useState(true);
|
|
67
|
+
const [creating, setCreating] = useState(false);
|
|
68
|
+
const [newKey, setNewKey] = useState(null);
|
|
69
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
70
|
+
const [confirmRegenerate, setConfirmRegenerate] = useState(false);
|
|
71
|
+
const [error, setError] = useState(null);
|
|
72
|
+
const loadKey = async () => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await getApiKeys();
|
|
75
|
+
setCurrentKey(result);
|
|
76
|
+
} catch {
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
loadKey();
|
|
83
|
+
}, []);
|
|
84
|
+
const handleCreate = async () => {
|
|
85
|
+
if (creating) return;
|
|
86
|
+
setCreating(true);
|
|
87
|
+
setError(null);
|
|
88
|
+
setConfirmRegenerate(false);
|
|
89
|
+
try {
|
|
90
|
+
const result = await createNewApiKey();
|
|
91
|
+
if (result.error) {
|
|
92
|
+
setError(result.error);
|
|
93
|
+
} else {
|
|
94
|
+
setNewKey(result.key);
|
|
95
|
+
await loadKey();
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
setError("Failed to create API key");
|
|
99
|
+
} finally {
|
|
100
|
+
setCreating(false);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const handleDelete = async () => {
|
|
104
|
+
if (!confirmDelete) {
|
|
105
|
+
setConfirmDelete(true);
|
|
106
|
+
setTimeout(() => setConfirmDelete(false), 3e3);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
await deleteApiKey();
|
|
111
|
+
setCurrentKey(null);
|
|
112
|
+
setNewKey(null);
|
|
113
|
+
setConfirmDelete(false);
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const handleRegenerate = () => {
|
|
118
|
+
if (!confirmRegenerate) {
|
|
119
|
+
setConfirmRegenerate(true);
|
|
120
|
+
setTimeout(() => setConfirmRegenerate(false), 3e3);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
handleCreate();
|
|
124
|
+
};
|
|
125
|
+
if (loading) {
|
|
126
|
+
return /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" });
|
|
127
|
+
}
|
|
128
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
129
|
+
error && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive mb-4", children: error }),
|
|
130
|
+
newKey && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-green-500/30 bg-green-500/5 p-4 mb-4", children: [
|
|
131
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 mb-2", children: [
|
|
132
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-green-600 dark:text-green-400", children: "API key created \u2014 copy it now. You won't be able to see it again." }),
|
|
133
|
+
/* @__PURE__ */ jsx(
|
|
134
|
+
"button",
|
|
135
|
+
{
|
|
136
|
+
onClick: () => setNewKey(null),
|
|
137
|
+
className: "text-xs text-muted-foreground hover:text-foreground shrink-0",
|
|
138
|
+
children: "Dismiss"
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
] }),
|
|
142
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
143
|
+
/* @__PURE__ */ jsx("code", { className: "flex-1 rounded-md bg-muted px-3 py-2 text-xs font-mono break-all select-all", children: newKey }),
|
|
144
|
+
/* @__PURE__ */ jsx(CopyButton, { text: newKey })
|
|
145
|
+
] })
|
|
146
|
+
] }),
|
|
147
|
+
currentKey ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
148
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
149
|
+
/* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-muted p-2", children: /* @__PURE__ */ jsx(KeyIcon, { size: 16 }) }),
|
|
150
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
151
|
+
/* @__PURE__ */ jsxs("code", { className: "text-sm font-mono", children: [
|
|
152
|
+
currentKey.keyPrefix,
|
|
153
|
+
"..."
|
|
154
|
+
] }),
|
|
155
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-0.5", children: [
|
|
156
|
+
"Created ",
|
|
157
|
+
formatDate(currentKey.createdAt),
|
|
158
|
+
currentKey.lastUsedAt && /* @__PURE__ */ jsxs("span", { className: "ml-2", children: [
|
|
159
|
+
"\xB7 Last used ",
|
|
160
|
+
timeAgo(currentKey.lastUsedAt)
|
|
161
|
+
] })
|
|
162
|
+
] })
|
|
163
|
+
] })
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
166
|
+
/* @__PURE__ */ jsxs(
|
|
167
|
+
"button",
|
|
168
|
+
{
|
|
169
|
+
onClick: handleRegenerate,
|
|
170
|
+
disabled: creating,
|
|
171
|
+
className: `inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border ${confirmRegenerate ? "border-yellow-500 text-yellow-600 hover:bg-yellow-500/10" : "border-border text-muted-foreground hover:bg-accent hover:text-foreground"} disabled:opacity-50`,
|
|
172
|
+
children: [
|
|
173
|
+
/* @__PURE__ */ jsx(RefreshIcon, { size: 12 }),
|
|
174
|
+
creating ? "Generating..." : confirmRegenerate ? "Confirm regenerate" : "Regenerate"
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
/* @__PURE__ */ jsxs(
|
|
179
|
+
"button",
|
|
180
|
+
{
|
|
181
|
+
onClick: handleDelete,
|
|
182
|
+
className: `inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border ${confirmDelete ? "border-destructive text-destructive hover:bg-destructive/10" : "border-border text-muted-foreground hover:text-destructive hover:border-destructive/50"}`,
|
|
183
|
+
children: [
|
|
184
|
+
/* @__PURE__ */ jsx(TrashIcon, { size: 12 }),
|
|
185
|
+
confirmDelete ? "Confirm delete" : "Delete"
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
] })
|
|
190
|
+
] }) }) : /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed bg-card p-6 flex flex-col items-center text-center", children: [
|
|
191
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-3", children: "No API key configured" }),
|
|
192
|
+
/* @__PURE__ */ jsx(
|
|
193
|
+
"button",
|
|
194
|
+
{
|
|
195
|
+
onClick: handleCreate,
|
|
196
|
+
disabled: creating,
|
|
197
|
+
className: "inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 disabled:pointer-events-none",
|
|
198
|
+
children: creating ? "Creating..." : "Create API key"
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
] })
|
|
202
|
+
] });
|
|
203
|
+
}
|
|
204
|
+
function SettingsSecretsPage() {
|
|
205
|
+
return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
|
|
206
|
+
Section,
|
|
207
|
+
{
|
|
208
|
+
title: "API Key",
|
|
209
|
+
description: "Authenticates external requests to /api endpoints. Pass via the x-api-key header.",
|
|
210
|
+
children: /* @__PURE__ */ jsx(ApiKeySection, {})
|
|
211
|
+
}
|
|
212
|
+
) });
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
SettingsSecretsPage
|
|
216
|
+
};
|