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.
Files changed (143) hide show
  1. package/README.md +1 -1
  2. package/api/index.js +72 -165
  3. package/bin/cli.js +36 -6
  4. package/bin/local.sh +31 -0
  5. package/bin/postinstall.js +6 -2
  6. package/config/index.js +2 -11
  7. package/config/instrumentation.js +17 -5
  8. package/lib/actions.js +7 -6
  9. package/lib/ai/agent.js +36 -0
  10. package/lib/ai/index.js +274 -0
  11. package/lib/ai/model.js +67 -0
  12. package/lib/ai/tools.js +49 -0
  13. package/lib/auth/actions.js +28 -0
  14. package/lib/auth/config.js +45 -0
  15. package/lib/auth/index.js +27 -0
  16. package/lib/auth/middleware.js +30 -0
  17. package/lib/channels/base.js +56 -0
  18. package/lib/channels/index.js +15 -0
  19. package/lib/channels/telegram.js +146 -0
  20. package/lib/chat/actions.js +239 -0
  21. package/lib/chat/api.js +103 -0
  22. package/lib/chat/components/app-sidebar.js +161 -0
  23. package/lib/chat/components/app-sidebar.jsx +214 -0
  24. package/lib/chat/components/chat-header.js +9 -0
  25. package/lib/chat/components/chat-header.jsx +14 -0
  26. package/lib/chat/components/chat-input.js +230 -0
  27. package/lib/chat/components/chat-input.jsx +232 -0
  28. package/lib/chat/components/chat-nav-context.js +11 -0
  29. package/lib/chat/components/chat-nav-context.jsx +11 -0
  30. package/lib/chat/components/chat-page.js +70 -0
  31. package/lib/chat/components/chat-page.jsx +89 -0
  32. package/lib/chat/components/chat.js +78 -0
  33. package/lib/chat/components/chat.jsx +91 -0
  34. package/lib/chat/components/chats-page.js +170 -0
  35. package/lib/chat/components/chats-page.jsx +203 -0
  36. package/lib/chat/components/crons-page.js +144 -0
  37. package/lib/chat/components/crons-page.jsx +204 -0
  38. package/lib/chat/components/greeting.js +11 -0
  39. package/lib/chat/components/greeting.jsx +14 -0
  40. package/lib/chat/components/icons.js +518 -0
  41. package/lib/chat/components/icons.jsx +482 -0
  42. package/lib/chat/components/index.js +19 -0
  43. package/lib/chat/components/message.js +66 -0
  44. package/lib/chat/components/message.jsx +92 -0
  45. package/lib/chat/components/messages.js +63 -0
  46. package/lib/chat/components/messages.jsx +72 -0
  47. package/lib/chat/components/notifications-page.js +54 -0
  48. package/lib/chat/components/notifications-page.jsx +83 -0
  49. package/lib/chat/components/page-layout.js +21 -0
  50. package/lib/chat/components/page-layout.jsx +28 -0
  51. package/lib/chat/components/settings-layout.js +37 -0
  52. package/lib/chat/components/settings-layout.jsx +51 -0
  53. package/lib/chat/components/settings-secrets-page.js +216 -0
  54. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  55. package/lib/chat/components/sidebar-history-item.js +54 -0
  56. package/lib/chat/components/sidebar-history-item.jsx +50 -0
  57. package/lib/chat/components/sidebar-history.js +92 -0
  58. package/lib/chat/components/sidebar-history.jsx +132 -0
  59. package/lib/chat/components/sidebar-user-nav.js +59 -0
  60. package/lib/chat/components/sidebar-user-nav.jsx +69 -0
  61. package/lib/chat/components/swarm-page.js +250 -0
  62. package/lib/chat/components/swarm-page.jsx +356 -0
  63. package/lib/chat/components/triggers-page.js +121 -0
  64. package/lib/chat/components/triggers-page.jsx +177 -0
  65. package/lib/chat/components/ui/dropdown-menu.js +98 -0
  66. package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
  67. package/lib/chat/components/ui/scroll-area.js +13 -0
  68. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  69. package/lib/chat/components/ui/separator.js +21 -0
  70. package/lib/chat/components/ui/separator.jsx +18 -0
  71. package/lib/chat/components/ui/sheet.js +75 -0
  72. package/lib/chat/components/ui/sheet.jsx +95 -0
  73. package/lib/chat/components/ui/sidebar.js +227 -0
  74. package/lib/chat/components/ui/sidebar.jsx +245 -0
  75. package/lib/chat/components/ui/tooltip.js +56 -0
  76. package/lib/chat/components/ui/tooltip.jsx +66 -0
  77. package/lib/chat/utils.js +11 -0
  78. package/lib/cron.js +7 -8
  79. package/lib/db/api-keys.js +160 -0
  80. package/lib/db/chats.js +129 -0
  81. package/lib/db/index.js +106 -0
  82. package/lib/db/notifications.js +99 -0
  83. package/lib/db/schema.js +51 -0
  84. package/lib/db/users.js +89 -0
  85. package/lib/paths.js +23 -17
  86. package/lib/tools/create-job.js +3 -3
  87. package/lib/tools/github.js +145 -1
  88. package/lib/tools/openai.js +1 -1
  89. package/lib/tools/telegram.js +4 -3
  90. package/lib/triggers.js +6 -7
  91. package/lib/utils/render-md.js +6 -6
  92. package/package.json +43 -6
  93. package/setup/lib/auth.mjs +22 -9
  94. package/setup/lib/prerequisites.mjs +10 -3
  95. package/setup/lib/telegram-verify.mjs +3 -16
  96. package/setup/setup-telegram.mjs +31 -62
  97. package/setup/setup.mjs +58 -98
  98. package/templates/.dockerignore +5 -0
  99. package/templates/.env.example +18 -2
  100. package/templates/.github/workflows/auto-merge.yml +1 -1
  101. package/templates/.github/workflows/build-image.yml +6 -4
  102. package/templates/.github/workflows/notify-job-failed.yml +2 -2
  103. package/templates/.github/workflows/notify-pr-complete.yml +2 -2
  104. package/templates/.github/workflows/run-job.yml +24 -10
  105. package/templates/CLAUDE.md +5 -3
  106. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  107. package/templates/app/api/chat/route.js +1 -0
  108. package/templates/app/chat/[chatId]/page.js +8 -0
  109. package/templates/app/chats/page.js +7 -0
  110. package/templates/app/components/ascii-logo.jsx +10 -0
  111. package/templates/app/components/login-form.jsx +81 -0
  112. package/templates/app/components/setup-form.jsx +82 -0
  113. package/templates/app/components/theme-provider.jsx +11 -0
  114. package/templates/app/components/theme-toggle.jsx +38 -0
  115. package/templates/app/components/ui/button.jsx +21 -0
  116. package/templates/app/components/ui/card.jsx +23 -0
  117. package/templates/app/components/ui/input.jsx +10 -0
  118. package/templates/app/components/ui/label.jsx +10 -0
  119. package/templates/app/crons/page.js +7 -0
  120. package/templates/app/globals.css +66 -0
  121. package/templates/app/layout.js +9 -2
  122. package/templates/app/login/page.js +15 -0
  123. package/templates/app/notifications/page.js +7 -0
  124. package/templates/app/page.js +6 -30
  125. package/templates/app/settings/layout.js +7 -0
  126. package/templates/app/settings/page.js +5 -0
  127. package/templates/app/settings/secrets/page.js +5 -0
  128. package/templates/app/swarm/page.js +7 -0
  129. package/templates/app/triggers/page.js +7 -0
  130. package/templates/config/CRONS.json +2 -2
  131. package/templates/config/TRIGGERS.json +2 -2
  132. package/templates/docker/event_handler/Dockerfile +19 -0
  133. package/templates/docker/{entrypoint.sh → job/entrypoint.sh} +4 -4
  134. package/templates/docker/runner/Dockerfile +38 -0
  135. package/templates/docker/runner/entrypoint.sh +41 -0
  136. package/templates/docker-compose.yml +52 -0
  137. package/templates/instrumentation.js +6 -1
  138. package/templates/middleware.js +1 -0
  139. package/templates/postcss.config.mjs +5 -0
  140. package/lib/claude/conversation.js +0 -76
  141. package/lib/claude/index.js +0 -142
  142. package/lib/claude/tools.js +0 -54
  143. /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
+ };