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,264 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
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
|
+
|
|
7
|
+
function timeAgo(ts) {
|
|
8
|
+
if (!ts) return 'Never';
|
|
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
|
+
function formatDate(ts) {
|
|
22
|
+
if (!ts) return '—';
|
|
23
|
+
return new Date(ts).toLocaleDateString(undefined, {
|
|
24
|
+
year: 'numeric',
|
|
25
|
+
month: 'short',
|
|
26
|
+
day: 'numeric',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function CopyButton({ text }) {
|
|
31
|
+
const [copied, setCopied] = useState(false);
|
|
32
|
+
|
|
33
|
+
const handleCopy = async () => {
|
|
34
|
+
try {
|
|
35
|
+
await navigator.clipboard.writeText(text);
|
|
36
|
+
setCopied(true);
|
|
37
|
+
setTimeout(() => setCopied(false), 2000);
|
|
38
|
+
} catch {
|
|
39
|
+
const textarea = document.createElement('textarea');
|
|
40
|
+
textarea.value = text;
|
|
41
|
+
document.body.appendChild(textarea);
|
|
42
|
+
textarea.select();
|
|
43
|
+
document.execCommand('copy');
|
|
44
|
+
document.body.removeChild(textarea);
|
|
45
|
+
setCopied(true);
|
|
46
|
+
setTimeout(() => setCopied(false), 2000);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<button
|
|
52
|
+
onClick={handleCopy}
|
|
53
|
+
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"
|
|
54
|
+
>
|
|
55
|
+
{copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
|
|
56
|
+
{copied ? 'Copied' : 'Copy'}
|
|
57
|
+
</button>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// Section wrapper — reusable for each secrets section
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function Section({ title, description, children }) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="pb-8 mb-8 border-b border-border last:border-b-0 last:pb-0 last:mb-0">
|
|
68
|
+
<h2 className="text-base font-medium mb-1">{title}</h2>
|
|
69
|
+
{description && (
|
|
70
|
+
<p className="text-sm text-muted-foreground mb-4">{description}</p>
|
|
71
|
+
)}
|
|
72
|
+
{children}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
// API Key section
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function ApiKeySection() {
|
|
82
|
+
const [currentKey, setCurrentKey] = useState(null);
|
|
83
|
+
const [loading, setLoading] = useState(true);
|
|
84
|
+
const [creating, setCreating] = useState(false);
|
|
85
|
+
const [newKey, setNewKey] = useState(null);
|
|
86
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
87
|
+
const [confirmRegenerate, setConfirmRegenerate] = useState(false);
|
|
88
|
+
const [error, setError] = useState(null);
|
|
89
|
+
|
|
90
|
+
const loadKey = async () => {
|
|
91
|
+
try {
|
|
92
|
+
const result = await getApiKeys();
|
|
93
|
+
setCurrentKey(result);
|
|
94
|
+
} catch {
|
|
95
|
+
// ignore
|
|
96
|
+
} finally {
|
|
97
|
+
setLoading(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
loadKey();
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const handleCreate = async () => {
|
|
106
|
+
if (creating) return;
|
|
107
|
+
setCreating(true);
|
|
108
|
+
setError(null);
|
|
109
|
+
setConfirmRegenerate(false);
|
|
110
|
+
try {
|
|
111
|
+
const result = await createNewApiKey();
|
|
112
|
+
if (result.error) {
|
|
113
|
+
setError(result.error);
|
|
114
|
+
} else {
|
|
115
|
+
setNewKey(result.key);
|
|
116
|
+
await loadKey();
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
setError('Failed to create API key');
|
|
120
|
+
} finally {
|
|
121
|
+
setCreating(false);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleDelete = async () => {
|
|
126
|
+
if (!confirmDelete) {
|
|
127
|
+
setConfirmDelete(true);
|
|
128
|
+
setTimeout(() => setConfirmDelete(false), 3000);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
await deleteApiKey();
|
|
133
|
+
setCurrentKey(null);
|
|
134
|
+
setNewKey(null);
|
|
135
|
+
setConfirmDelete(false);
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleRegenerate = () => {
|
|
142
|
+
if (!confirmRegenerate) {
|
|
143
|
+
setConfirmRegenerate(true);
|
|
144
|
+
setTimeout(() => setConfirmRegenerate(false), 3000);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
handleCreate();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (loading) {
|
|
151
|
+
return <div className="h-14 animate-pulse rounded-md bg-border/50" />;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div>
|
|
156
|
+
{error && (
|
|
157
|
+
<p className="text-sm text-destructive mb-4">{error}</p>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* New key banner */}
|
|
161
|
+
{newKey && (
|
|
162
|
+
<div className="rounded-lg border border-green-500/30 bg-green-500/5 p-4 mb-4">
|
|
163
|
+
<div className="flex items-start justify-between gap-3 mb-2">
|
|
164
|
+
<p className="text-sm font-medium text-green-600 dark:text-green-400">
|
|
165
|
+
API key created — copy it now. You won't be able to see it again.
|
|
166
|
+
</p>
|
|
167
|
+
<button
|
|
168
|
+
onClick={() => setNewKey(null)}
|
|
169
|
+
className="text-xs text-muted-foreground hover:text-foreground shrink-0"
|
|
170
|
+
>
|
|
171
|
+
Dismiss
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex items-center gap-2">
|
|
175
|
+
<code className="flex-1 rounded-md bg-muted px-3 py-2 text-xs font-mono break-all select-all">
|
|
176
|
+
{newKey}
|
|
177
|
+
</code>
|
|
178
|
+
<CopyButton text={newKey} />
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{currentKey ? (
|
|
184
|
+
<div className="rounded-lg border bg-card p-4">
|
|
185
|
+
<div className="flex items-center justify-between">
|
|
186
|
+
<div className="flex items-center gap-3">
|
|
187
|
+
<div className="shrink-0 rounded-md bg-muted p-2">
|
|
188
|
+
<KeyIcon size={16} />
|
|
189
|
+
</div>
|
|
190
|
+
<div>
|
|
191
|
+
<code className="text-sm font-mono">{currentKey.keyPrefix}...</code>
|
|
192
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
193
|
+
Created {formatDate(currentKey.createdAt)}
|
|
194
|
+
{currentKey.lastUsedAt && (
|
|
195
|
+
<span className="ml-2">· Last used {timeAgo(currentKey.lastUsedAt)}</span>
|
|
196
|
+
)}
|
|
197
|
+
</p>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<button
|
|
202
|
+
onClick={handleRegenerate}
|
|
203
|
+
disabled={creating}
|
|
204
|
+
className={`inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border ${
|
|
205
|
+
confirmRegenerate
|
|
206
|
+
? 'border-yellow-500 text-yellow-600 hover:bg-yellow-500/10'
|
|
207
|
+
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
208
|
+
} disabled:opacity-50`}
|
|
209
|
+
>
|
|
210
|
+
<RefreshIcon size={12} />
|
|
211
|
+
{creating ? 'Generating...' : confirmRegenerate ? 'Confirm regenerate' : 'Regenerate'}
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
onClick={handleDelete}
|
|
215
|
+
className={`inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border ${
|
|
216
|
+
confirmDelete
|
|
217
|
+
? 'border-destructive text-destructive hover:bg-destructive/10'
|
|
218
|
+
: 'border-border text-muted-foreground hover:text-destructive hover:border-destructive/50'
|
|
219
|
+
}`}
|
|
220
|
+
>
|
|
221
|
+
<TrashIcon size={12} />
|
|
222
|
+
{confirmDelete ? 'Confirm delete' : 'Delete'}
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
) : (
|
|
228
|
+
<div className="rounded-lg border border-dashed bg-card p-6 flex flex-col items-center text-center">
|
|
229
|
+
<p className="text-sm text-muted-foreground mb-3">No API key configured</p>
|
|
230
|
+
<button
|
|
231
|
+
onClick={handleCreate}
|
|
232
|
+
disabled={creating}
|
|
233
|
+
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"
|
|
234
|
+
>
|
|
235
|
+
{creating ? 'Creating...' : 'Create API key'}
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
// Main page
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
export function SettingsSecretsPage() {
|
|
248
|
+
return (
|
|
249
|
+
<div>
|
|
250
|
+
<Section
|
|
251
|
+
title="API Key"
|
|
252
|
+
description="Authenticates external requests to /api endpoints. Pass via the x-api-key header."
|
|
253
|
+
>
|
|
254
|
+
<ApiKeySection />
|
|
255
|
+
</Section>
|
|
256
|
+
|
|
257
|
+
{/* Future sections go here, e.g.:
|
|
258
|
+
<Section title="GitHub Token" description="...">
|
|
259
|
+
<GitHubTokenSection />
|
|
260
|
+
</Section>
|
|
261
|
+
*/}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { MessageIcon, TrashIcon } from "./icons.js";
|
|
5
|
+
import { SidebarMenuButton, SidebarMenuItem, useSidebar } from "./ui/sidebar.js";
|
|
6
|
+
import { useChatNav } from "./chat-nav-context.js";
|
|
7
|
+
import { cn } from "../utils.js";
|
|
8
|
+
function SidebarHistoryItem({ chat, isActive, onDelete }) {
|
|
9
|
+
const { navigateToChat } = useChatNav();
|
|
10
|
+
const { setOpenMobile } = useSidebar();
|
|
11
|
+
const [showDelete, setShowDelete] = useState(false);
|
|
12
|
+
return /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(
|
|
13
|
+
"div",
|
|
14
|
+
{
|
|
15
|
+
className: "relative group",
|
|
16
|
+
onMouseEnter: () => setShowDelete(true),
|
|
17
|
+
onMouseLeave: () => setShowDelete(false),
|
|
18
|
+
children: [
|
|
19
|
+
/* @__PURE__ */ jsxs(
|
|
20
|
+
SidebarMenuButton,
|
|
21
|
+
{
|
|
22
|
+
isActive,
|
|
23
|
+
onClick: () => {
|
|
24
|
+
navigateToChat(chat.id);
|
|
25
|
+
setOpenMobile(false);
|
|
26
|
+
},
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ jsx(MessageIcon, { size: 14 }),
|
|
29
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: chat.title })
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
),
|
|
33
|
+
showDelete && /* @__PURE__ */ jsx(
|
|
34
|
+
"button",
|
|
35
|
+
{
|
|
36
|
+
className: cn(
|
|
37
|
+
"absolute right-1 top-1/2 -translate-y-1/2 rounded-md p-1",
|
|
38
|
+
"text-muted-foreground hover:text-destructive hover:bg-muted"
|
|
39
|
+
),
|
|
40
|
+
onClick: (e) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
onDelete(chat.id);
|
|
43
|
+
},
|
|
44
|
+
"aria-label": "Delete chat",
|
|
45
|
+
children: /* @__PURE__ */ jsx(TrashIcon, { size: 14 })
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
) });
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
SidebarHistoryItem
|
|
54
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { MessageIcon, TrashIcon } from './icons.js';
|
|
5
|
+
import { SidebarMenuButton, SidebarMenuItem, useSidebar } from './ui/sidebar.js';
|
|
6
|
+
import { useChatNav } from './chat-nav-context.js';
|
|
7
|
+
import { cn } from '../utils.js';
|
|
8
|
+
|
|
9
|
+
export function SidebarHistoryItem({ chat, isActive, onDelete }) {
|
|
10
|
+
const { navigateToChat } = useChatNav();
|
|
11
|
+
const { setOpenMobile } = useSidebar();
|
|
12
|
+
const [showDelete, setShowDelete] = useState(false);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<SidebarMenuItem>
|
|
16
|
+
<div
|
|
17
|
+
className="relative group"
|
|
18
|
+
onMouseEnter={() => setShowDelete(true)}
|
|
19
|
+
onMouseLeave={() => setShowDelete(false)}
|
|
20
|
+
>
|
|
21
|
+
<SidebarMenuButton
|
|
22
|
+
isActive={isActive}
|
|
23
|
+
onClick={() => {
|
|
24
|
+
navigateToChat(chat.id);
|
|
25
|
+
setOpenMobile(false);
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<MessageIcon size={14} />
|
|
29
|
+
<span className="truncate flex-1">{chat.title}</span>
|
|
30
|
+
</SidebarMenuButton>
|
|
31
|
+
|
|
32
|
+
{showDelete && (
|
|
33
|
+
<button
|
|
34
|
+
className={cn(
|
|
35
|
+
'absolute right-1 top-1/2 -translate-y-1/2 rounded-md p-1',
|
|
36
|
+
'text-muted-foreground hover:text-destructive hover:bg-muted'
|
|
37
|
+
)}
|
|
38
|
+
onClick={(e) => {
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
onDelete(chat.id);
|
|
41
|
+
}}
|
|
42
|
+
aria-label="Delete chat"
|
|
43
|
+
>
|
|
44
|
+
<TrashIcon size={14} />
|
|
45
|
+
</button>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</SidebarMenuItem>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { SidebarHistoryItem } from "./sidebar-history-item.js";
|
|
5
|
+
import { SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu } from "./ui/sidebar.js";
|
|
6
|
+
import { useChatNav } from "./chat-nav-context.js";
|
|
7
|
+
import { getChats, deleteChat } from "../actions.js";
|
|
8
|
+
function groupChatsByDate(chats) {
|
|
9
|
+
const now = /* @__PURE__ */ new Date();
|
|
10
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
11
|
+
const yesterday = new Date(today.getTime() - 864e5);
|
|
12
|
+
const last7Days = new Date(today.getTime() - 7 * 864e5);
|
|
13
|
+
const last30Days = new Date(today.getTime() - 30 * 864e5);
|
|
14
|
+
const groups = {
|
|
15
|
+
Today: [],
|
|
16
|
+
Yesterday: [],
|
|
17
|
+
"Last 7 Days": [],
|
|
18
|
+
"Last 30 Days": [],
|
|
19
|
+
Older: []
|
|
20
|
+
};
|
|
21
|
+
for (const chat of chats) {
|
|
22
|
+
const date = new Date(chat.updatedAt);
|
|
23
|
+
if (date >= today) {
|
|
24
|
+
groups.Today.push(chat);
|
|
25
|
+
} else if (date >= yesterday) {
|
|
26
|
+
groups.Yesterday.push(chat);
|
|
27
|
+
} else if (date >= last7Days) {
|
|
28
|
+
groups["Last 7 Days"].push(chat);
|
|
29
|
+
} else if (date >= last30Days) {
|
|
30
|
+
groups["Last 30 Days"].push(chat);
|
|
31
|
+
} else {
|
|
32
|
+
groups.Older.push(chat);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return groups;
|
|
36
|
+
}
|
|
37
|
+
function SidebarHistory() {
|
|
38
|
+
const [chats, setChats] = useState([]);
|
|
39
|
+
const [loading, setLoading] = useState(true);
|
|
40
|
+
const { activeChatId, navigateToChat } = useChatNav();
|
|
41
|
+
const loadChats = async () => {
|
|
42
|
+
try {
|
|
43
|
+
const result = await getChats();
|
|
44
|
+
setChats(result);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error("Failed to load chats:", err);
|
|
47
|
+
} finally {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
loadChats();
|
|
53
|
+
}, [activeChatId]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const handler = () => loadChats();
|
|
56
|
+
window.addEventListener("chatsupdated", handler);
|
|
57
|
+
return () => window.removeEventListener("chatsupdated", handler);
|
|
58
|
+
}, []);
|
|
59
|
+
const handleDelete = async (chatId) => {
|
|
60
|
+
const { success } = await deleteChat(chatId);
|
|
61
|
+
if (success) {
|
|
62
|
+
setChats((prev) => prev.filter((c) => c.id !== chatId));
|
|
63
|
+
if (chatId === activeChatId) {
|
|
64
|
+
navigateToChat(null);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
if (loading && chats.length === 0) {
|
|
69
|
+
return /* @__PURE__ */ jsx(SidebarGroup, { children: /* @__PURE__ */ jsx(SidebarGroupContent, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 px-2", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-8 animate-pulse rounded-md bg-border/50" }, i)) }) }) });
|
|
70
|
+
}
|
|
71
|
+
if (chats.length === 0) {
|
|
72
|
+
return /* @__PURE__ */ jsx(SidebarGroup, { children: /* @__PURE__ */ jsx(SidebarGroupContent, { children: /* @__PURE__ */ jsx("p", { className: "px-4 py-2 text-sm text-muted-foreground", children: "No chats yet. Start a conversation!" }) }) });
|
|
73
|
+
}
|
|
74
|
+
const grouped = groupChatsByDate(chats);
|
|
75
|
+
return /* @__PURE__ */ jsx(Fragment, { children: Object.entries(grouped).map(
|
|
76
|
+
([label, groupChats]) => groupChats.length > 0 && /* @__PURE__ */ jsxs(SidebarGroup, { children: [
|
|
77
|
+
/* @__PURE__ */ jsx(SidebarGroupLabel, { children: label }),
|
|
78
|
+
/* @__PURE__ */ jsx(SidebarGroupContent, { children: /* @__PURE__ */ jsx(SidebarMenu, { children: groupChats.map((chat) => /* @__PURE__ */ jsx(
|
|
79
|
+
SidebarHistoryItem,
|
|
80
|
+
{
|
|
81
|
+
chat,
|
|
82
|
+
isActive: chat.id === activeChatId,
|
|
83
|
+
onDelete: handleDelete
|
|
84
|
+
},
|
|
85
|
+
chat.id
|
|
86
|
+
)) }) })
|
|
87
|
+
] }, label)
|
|
88
|
+
) });
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
SidebarHistory
|
|
92
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { SidebarHistoryItem } from './sidebar-history-item.js';
|
|
5
|
+
import { SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu } from './ui/sidebar.js';
|
|
6
|
+
import { useChatNav } from './chat-nav-context.js';
|
|
7
|
+
import { getChats, deleteChat } from '../actions.js';
|
|
8
|
+
|
|
9
|
+
function groupChatsByDate(chats) {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
12
|
+
const yesterday = new Date(today.getTime() - 86400000);
|
|
13
|
+
const last7Days = new Date(today.getTime() - 7 * 86400000);
|
|
14
|
+
const last30Days = new Date(today.getTime() - 30 * 86400000);
|
|
15
|
+
|
|
16
|
+
const groups = {
|
|
17
|
+
Today: [],
|
|
18
|
+
Yesterday: [],
|
|
19
|
+
'Last 7 Days': [],
|
|
20
|
+
'Last 30 Days': [],
|
|
21
|
+
Older: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (const chat of chats) {
|
|
25
|
+
const date = new Date(chat.updatedAt);
|
|
26
|
+
if (date >= today) {
|
|
27
|
+
groups.Today.push(chat);
|
|
28
|
+
} else if (date >= yesterday) {
|
|
29
|
+
groups.Yesterday.push(chat);
|
|
30
|
+
} else if (date >= last7Days) {
|
|
31
|
+
groups['Last 7 Days'].push(chat);
|
|
32
|
+
} else if (date >= last30Days) {
|
|
33
|
+
groups['Last 30 Days'].push(chat);
|
|
34
|
+
} else {
|
|
35
|
+
groups.Older.push(chat);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return groups;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SidebarHistory() {
|
|
43
|
+
const [chats, setChats] = useState([]);
|
|
44
|
+
const [loading, setLoading] = useState(true);
|
|
45
|
+
const { activeChatId, navigateToChat } = useChatNav();
|
|
46
|
+
|
|
47
|
+
const loadChats = async () => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await getChats();
|
|
50
|
+
setChats(result);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error('Failed to load chats:', err);
|
|
53
|
+
} finally {
|
|
54
|
+
setLoading(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Load chats on mount and refresh when navigating between pages
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
loadChats();
|
|
61
|
+
}, [activeChatId]);
|
|
62
|
+
|
|
63
|
+
// Reload when chats change (new chat created or title updated)
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const handler = () => loadChats();
|
|
66
|
+
window.addEventListener('chatsupdated', handler);
|
|
67
|
+
return () => window.removeEventListener('chatsupdated', handler);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const handleDelete = async (chatId) => {
|
|
71
|
+
const { success } = await deleteChat(chatId);
|
|
72
|
+
if (success) {
|
|
73
|
+
setChats((prev) => prev.filter((c) => c.id !== chatId));
|
|
74
|
+
if (chatId === activeChatId) {
|
|
75
|
+
navigateToChat(null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (loading && chats.length === 0) {
|
|
81
|
+
return (
|
|
82
|
+
<SidebarGroup>
|
|
83
|
+
<SidebarGroupContent>
|
|
84
|
+
<div className="flex flex-col gap-2 px-2">
|
|
85
|
+
{[...Array(5)].map((_, i) => (
|
|
86
|
+
<div key={i} className="h-8 animate-pulse rounded-md bg-border/50" />
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</SidebarGroupContent>
|
|
90
|
+
</SidebarGroup>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (chats.length === 0) {
|
|
95
|
+
return (
|
|
96
|
+
<SidebarGroup>
|
|
97
|
+
<SidebarGroupContent>
|
|
98
|
+
<p className="px-4 py-2 text-sm text-muted-foreground">
|
|
99
|
+
No chats yet. Start a conversation!
|
|
100
|
+
</p>
|
|
101
|
+
</SidebarGroupContent>
|
|
102
|
+
</SidebarGroup>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const grouped = groupChatsByDate(chats);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
{Object.entries(grouped).map(
|
|
111
|
+
([label, groupChats]) =>
|
|
112
|
+
groupChats.length > 0 && (
|
|
113
|
+
<SidebarGroup key={label}>
|
|
114
|
+
<SidebarGroupLabel>{label}</SidebarGroupLabel>
|
|
115
|
+
<SidebarGroupContent>
|
|
116
|
+
<SidebarMenu>
|
|
117
|
+
{groupChats.map((chat) => (
|
|
118
|
+
<SidebarHistoryItem
|
|
119
|
+
key={chat.id}
|
|
120
|
+
chat={chat}
|
|
121
|
+
isActive={chat.id === activeChatId}
|
|
122
|
+
onDelete={handleDelete}
|
|
123
|
+
/>
|
|
124
|
+
))}
|
|
125
|
+
</SidebarMenu>
|
|
126
|
+
</SidebarGroupContent>
|
|
127
|
+
</SidebarGroup>
|
|
128
|
+
)
|
|
129
|
+
)}
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { signOut } from "next-auth/react";
|
|
4
|
+
import { useTheme } from "next-themes";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger
|
|
12
|
+
} from "./ui/dropdown-menu.js";
|
|
13
|
+
import { SidebarMenu, SidebarMenuItem, SidebarMenuButton } from "./ui/sidebar.js";
|
|
14
|
+
import { SettingsIcon, SunIcon, MoonIcon, LogOutIcon } from "./icons.js";
|
|
15
|
+
import { cn } from "../utils.js";
|
|
16
|
+
function SidebarUserNav({ user, collapsed }) {
|
|
17
|
+
const { theme, setTheme } = useTheme();
|
|
18
|
+
const [mounted, setMounted] = useState(false);
|
|
19
|
+
useEffect(() => setMounted(true), []);
|
|
20
|
+
return /* @__PURE__ */ jsx(SidebarMenu, { children: /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
21
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(SidebarMenuButton, { className: cn(collapsed ? "justify-center" : "justify-between"), children: [
|
|
22
|
+
/* @__PURE__ */ jsxs("div", { className: cn("flex items-center overflow-hidden", collapsed ? "" : "gap-2"), children: [
|
|
23
|
+
/* @__PURE__ */ jsx("div", { className: "flex size-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium", children: (user?.email?.[0] || "U").toUpperCase() }),
|
|
24
|
+
!collapsed && /* @__PURE__ */ jsx("span", { className: "truncate text-sm", children: user?.email || "User" })
|
|
25
|
+
] }),
|
|
26
|
+
!collapsed && /* @__PURE__ */ jsxs("svg", { className: "size-4 text-muted-foreground", width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: [
|
|
27
|
+
/* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5" }),
|
|
28
|
+
/* @__PURE__ */ jsx("path", { d: "m7 9 5-5 5 5" })
|
|
29
|
+
] })
|
|
30
|
+
] }) }),
|
|
31
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", side: "top", className: "w-56", children: [
|
|
32
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => {
|
|
33
|
+
window.location.href = "/settings";
|
|
34
|
+
}, children: [
|
|
35
|
+
/* @__PURE__ */ jsx(SettingsIcon, { size: 14 }),
|
|
36
|
+
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Settings" })
|
|
37
|
+
] }),
|
|
38
|
+
mounted && /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setTheme(theme === "dark" ? "light" : "dark"), children: [
|
|
39
|
+
theme === "dark" ? /* @__PURE__ */ jsx(SunIcon, { size: 14 }) : /* @__PURE__ */ jsx(MoonIcon, { size: 14 }),
|
|
40
|
+
/* @__PURE__ */ jsx("span", { className: "ml-2", children: theme === "dark" ? "Light Mode" : "Dark Mode" })
|
|
41
|
+
] }),
|
|
42
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
43
|
+
/* @__PURE__ */ jsxs(
|
|
44
|
+
DropdownMenuItem,
|
|
45
|
+
{
|
|
46
|
+
onClick: () => signOut({ callbackUrl: "/" }),
|
|
47
|
+
className: "text-destructive",
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsx(LogOutIcon, { size: 14 }),
|
|
50
|
+
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Sign Out" })
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
] })
|
|
55
|
+
] }) }) });
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
SidebarUserNav
|
|
59
|
+
};
|