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,146 @@
1
+ import { ChannelAdapter } from './base.js';
2
+ import {
3
+ sendMessage,
4
+ downloadFile,
5
+ reactToMessage,
6
+ startTypingIndicator,
7
+ } from '../tools/telegram.js';
8
+ import { isWhisperEnabled, transcribeAudio } from '../tools/openai.js';
9
+
10
+ class TelegramAdapter extends ChannelAdapter {
11
+ constructor(botToken) {
12
+ super();
13
+ this.botToken = botToken;
14
+ }
15
+
16
+ /**
17
+ * Parse a Telegram webhook update into normalized message data.
18
+ * Handles: text, voice/audio (transcribed), photos, documents.
19
+ * Returns null if the update should be ignored.
20
+ */
21
+ async receive(request) {
22
+ const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
23
+
24
+ // Validate secret token if configured
25
+ if (TELEGRAM_WEBHOOK_SECRET) {
26
+ const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
27
+ if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ const update = await request.json();
33
+ const message = update.message || update.edited_message;
34
+
35
+ if (!message || !message.chat || !this.botToken) return null;
36
+
37
+ const chatId = String(message.chat.id);
38
+ let text = message.text || null;
39
+ const attachments = [];
40
+
41
+ // Check for verification code — works even before TELEGRAM_CHAT_ID is set
42
+ if (TELEGRAM_VERIFICATION && text === TELEGRAM_VERIFICATION) {
43
+ await sendMessage(this.botToken, chatId, `Your chat ID:\n<code>${chatId}</code>`);
44
+ return null;
45
+ }
46
+
47
+ // Security: if no TELEGRAM_CHAT_ID configured, ignore all messages
48
+ if (!TELEGRAM_CHAT_ID) return null;
49
+
50
+ // Security: only accept messages from configured chat
51
+ if (chatId !== TELEGRAM_CHAT_ID) return null;
52
+
53
+ // Voice messages → transcribe to text
54
+ if (message.voice) {
55
+ if (!isWhisperEnabled()) {
56
+ await sendMessage(
57
+ this.botToken,
58
+ chatId,
59
+ 'Voice messages are not supported. Please set OPENAI_API_KEY to enable transcription.'
60
+ );
61
+ return null;
62
+ }
63
+ try {
64
+ const { buffer, filename } = await downloadFile(this.botToken, message.voice.file_id);
65
+ text = await transcribeAudio(buffer, filename);
66
+ } catch (err) {
67
+ console.error('Failed to transcribe voice:', err);
68
+ await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your voice message.');
69
+ return null;
70
+ }
71
+ }
72
+
73
+ // Audio messages → transcribe to text
74
+ if (message.audio && !text) {
75
+ if (!isWhisperEnabled()) {
76
+ await sendMessage(
77
+ this.botToken,
78
+ chatId,
79
+ 'Audio messages are not supported. Please set OPENAI_API_KEY to enable transcription.'
80
+ );
81
+ return null;
82
+ }
83
+ try {
84
+ const { buffer, filename } = await downloadFile(this.botToken, message.audio.file_id);
85
+ text = await transcribeAudio(buffer, filename);
86
+ } catch (err) {
87
+ console.error('Failed to transcribe audio:', err);
88
+ await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your audio message.');
89
+ return null;
90
+ }
91
+ }
92
+
93
+ // Photo → download largest size, add as image attachment
94
+ if (message.photo && message.photo.length > 0) {
95
+ try {
96
+ const largest = message.photo[message.photo.length - 1];
97
+ const { buffer } = await downloadFile(this.botToken, largest.file_id);
98
+ attachments.push({ category: 'image', mimeType: 'image/jpeg', data: buffer });
99
+ // Use caption as text if no text yet
100
+ if (!text && message.caption) text = message.caption;
101
+ } catch (err) {
102
+ console.error('Failed to download photo:', err);
103
+ }
104
+ }
105
+
106
+ // Document → download, add as document attachment
107
+ if (message.document) {
108
+ try {
109
+ const { buffer, filename } = await downloadFile(this.botToken, message.document.file_id);
110
+ const mimeType = message.document.mime_type || 'application/octet-stream';
111
+ attachments.push({ category: 'document', mimeType, data: buffer });
112
+ if (!text && message.caption) text = message.caption;
113
+ } catch (err) {
114
+ console.error('Failed to download document:', err);
115
+ }
116
+ }
117
+
118
+ // Nothing actionable
119
+ if (!text && attachments.length === 0) return null;
120
+
121
+ return {
122
+ threadId: chatId,
123
+ text: text || '',
124
+ attachments,
125
+ metadata: { messageId: message.message_id, chatId },
126
+ };
127
+ }
128
+
129
+ async acknowledge(metadata) {
130
+ await reactToMessage(this.botToken, metadata.chatId, metadata.messageId).catch(() => {});
131
+ }
132
+
133
+ startProcessingIndicator(metadata) {
134
+ return startTypingIndicator(this.botToken, metadata.chatId);
135
+ }
136
+
137
+ async sendResponse(threadId, text, metadata) {
138
+ await sendMessage(this.botToken, threadId, text);
139
+ }
140
+
141
+ get supportsStreaming() {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ export { TelegramAdapter };
@@ -0,0 +1,239 @@
1
+ 'use server';
2
+
3
+ import { auth } from '../auth/index.js';
4
+ import {
5
+ createChat as dbCreateChat,
6
+ getChatById,
7
+ getMessagesByChatId,
8
+ deleteChat as dbDeleteChat,
9
+ deleteAllChatsByUser,
10
+ } from '../db/chats.js';
11
+ import {
12
+ getNotifications as dbGetNotifications,
13
+ getUnreadCount as dbGetUnreadCount,
14
+ markAllRead as dbMarkAllRead,
15
+ } from '../db/notifications.js';
16
+
17
+ /**
18
+ * Get the authenticated user or throw.
19
+ */
20
+ async function requireAuth() {
21
+ const session = await auth();
22
+ if (!session?.user?.id) {
23
+ throw new Error('Unauthorized');
24
+ }
25
+ return session.user;
26
+ }
27
+
28
+ /**
29
+ * Get all chats for the authenticated user (includes Telegram chats).
30
+ * @returns {Promise<object[]>}
31
+ */
32
+ export async function getChats() {
33
+ const user = await requireAuth();
34
+ const { or, eq, desc } = await import('drizzle-orm');
35
+ const { getDb } = await import('../db/index.js');
36
+ const { chats } = await import('../db/schema.js');
37
+ const db = getDb();
38
+ return db
39
+ .select()
40
+ .from(chats)
41
+ .where(or(eq(chats.userId, user.id), eq(chats.userId, 'telegram')))
42
+ .orderBy(desc(chats.updatedAt))
43
+ .all();
44
+ }
45
+
46
+ /**
47
+ * Get messages for a specific chat (with ownership check).
48
+ * @param {string} chatId
49
+ * @returns {Promise<object[]>}
50
+ */
51
+ export async function getChatMessages(chatId) {
52
+ const user = await requireAuth();
53
+ const chat = getChatById(chatId);
54
+ if (!chat || (chat.userId !== user.id && chat.userId !== 'telegram')) {
55
+ return [];
56
+ }
57
+ return getMessagesByChatId(chatId);
58
+ }
59
+
60
+ /**
61
+ * Create a new chat.
62
+ * @param {string} [id] - Optional chat ID
63
+ * @param {string} [title='New Chat']
64
+ * @returns {Promise<object>}
65
+ */
66
+ export async function createChat(id, title = 'New Chat') {
67
+ const user = await requireAuth();
68
+ return dbCreateChat(user.id, title, id);
69
+ }
70
+
71
+ /**
72
+ * Delete a chat (with ownership check).
73
+ * @param {string} chatId
74
+ * @returns {Promise<{success: boolean}>}
75
+ */
76
+ export async function deleteChat(chatId) {
77
+ const user = await requireAuth();
78
+ const chat = getChatById(chatId);
79
+ if (!chat || chat.userId !== user.id) {
80
+ return { success: false };
81
+ }
82
+ dbDeleteChat(chatId);
83
+ return { success: true };
84
+ }
85
+
86
+ /**
87
+ * Delete all chats for the authenticated user.
88
+ * @returns {Promise<{success: boolean}>}
89
+ */
90
+ export async function deleteAllChats() {
91
+ const user = await requireAuth();
92
+ deleteAllChatsByUser(user.id);
93
+ return { success: true };
94
+ }
95
+
96
+ /**
97
+ * Get all notifications, newest first.
98
+ * @returns {Promise<object[]>}
99
+ */
100
+ export async function getNotifications() {
101
+ await requireAuth();
102
+ return dbGetNotifications();
103
+ }
104
+
105
+ /**
106
+ * Get count of unread notifications.
107
+ * @returns {Promise<number>}
108
+ */
109
+ export async function getUnreadNotificationCount() {
110
+ await requireAuth();
111
+ return dbGetUnreadCount();
112
+ }
113
+
114
+ /**
115
+ * Mark all notifications as read.
116
+ * @returns {Promise<{success: boolean}>}
117
+ */
118
+ export async function markNotificationsRead() {
119
+ await requireAuth();
120
+ dbMarkAllRead();
121
+ return { success: true };
122
+ }
123
+
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ // API Key actions
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Create (or replace) the API key.
130
+ * @returns {Promise<{ key: string, record: object } | { error: string }>}
131
+ */
132
+ export async function createNewApiKey() {
133
+ const user = await requireAuth();
134
+ try {
135
+ const { createApiKeyRecord } = await import('../db/api-keys.js');
136
+ return createApiKeyRecord(user.id);
137
+ } catch (err) {
138
+ console.error('Failed to create API key:', err);
139
+ return { error: 'Failed to create API key' };
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get the current API key metadata (no hash).
145
+ * @returns {Promise<object|null>}
146
+ */
147
+ export async function getApiKeys() {
148
+ await requireAuth();
149
+ try {
150
+ const { getApiKey } = await import('../db/api-keys.js');
151
+ return getApiKey();
152
+ } catch (err) {
153
+ console.error('Failed to get API key:', err);
154
+ return null;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Delete the API key.
160
+ * @returns {Promise<{ success: boolean } | { error: string }>}
161
+ */
162
+ export async function deleteApiKey() {
163
+ await requireAuth();
164
+ try {
165
+ const mod = await import('../db/api-keys.js');
166
+ mod.deleteApiKey();
167
+ return { success: true };
168
+ } catch (err) {
169
+ console.error('Failed to delete API key:', err);
170
+ return { error: 'Failed to delete API key' };
171
+ }
172
+ }
173
+
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+ // Swarm actions
176
+ // ─────────────────────────────────────────────────────────────────────────────
177
+
178
+ /**
179
+ * Get swarm status (active + completed jobs with counts).
180
+ * @returns {Promise<object>}
181
+ */
182
+ export async function getSwarmStatus() {
183
+ await requireAuth();
184
+ try {
185
+ const { getSwarmStatus: fetchStatus } = await import('../tools/github.js');
186
+ return await fetchStatus();
187
+ } catch (err) {
188
+ console.error('Failed to get swarm status:', err);
189
+ return { error: 'Failed to get swarm status', active: [], completed: [], counts: { running: 0, queued: 0, succeeded: 0, failed: 0 } };
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Get swarm config (crons + triggers).
195
+ * @returns {Promise<{ crons: object[], triggers: object[] }>}
196
+ */
197
+ export async function getSwarmConfig() {
198
+ await requireAuth();
199
+ const { cronsFile, triggersFile } = await import('../paths.js');
200
+ const fs = await import('fs');
201
+ let crons = [];
202
+ let triggers = [];
203
+ try { crons = JSON.parse(fs.readFileSync(cronsFile, 'utf8')); } catch {}
204
+ try { triggers = JSON.parse(fs.readFileSync(triggersFile, 'utf8')); } catch {}
205
+ return { crons, triggers };
206
+ }
207
+
208
+ /**
209
+ * Cancel a running swarm job.
210
+ * @param {number} runId - Workflow run ID
211
+ * @returns {Promise<object>}
212
+ */
213
+ export async function cancelSwarmJob(runId) {
214
+ await requireAuth();
215
+ try {
216
+ const { cancelWorkflowRun } = await import('../tools/github.js');
217
+ return await cancelWorkflowRun(runId);
218
+ } catch (err) {
219
+ console.error('Failed to cancel workflow run:', err);
220
+ return { error: err.message || 'Failed to cancel workflow run' };
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Rerun a swarm job (all or failed only).
226
+ * @param {number} runId - Workflow run ID
227
+ * @param {boolean} [failedOnly=false]
228
+ * @returns {Promise<object>}
229
+ */
230
+ export async function rerunSwarmJob(runId, failedOnly = false) {
231
+ await requireAuth();
232
+ try {
233
+ const { rerunWorkflowRun } = await import('../tools/github.js');
234
+ return await rerunWorkflowRun(runId, !!failedOnly);
235
+ } catch (err) {
236
+ console.error('Failed to rerun workflow:', err);
237
+ return { error: err.message || 'Failed to rerun workflow' };
238
+ }
239
+ }
@@ -0,0 +1,103 @@
1
+ import { auth } from '../auth/index.js';
2
+ import { chatStream } from '../ai/index.js';
3
+
4
+ /**
5
+ * POST handler for /api/chat — streaming chat with session auth.
6
+ * Dedicated route handler separate from the catch-all api/index.js.
7
+ */
8
+ export async function POST(request) {
9
+ const session = await auth();
10
+ if (!session?.user?.id) {
11
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
12
+ }
13
+
14
+ const body = await request.json();
15
+ const { messages, chatId: rawChatId } = body;
16
+
17
+ if (!messages?.length) {
18
+ return Response.json({ error: 'No messages' }, { status: 400 });
19
+ }
20
+
21
+ // Get the last user message — AI SDK v5 sends UIMessage[] with parts
22
+ const lastUserMessage = [...messages].reverse().find((m) => m.role === 'user');
23
+ if (!lastUserMessage) {
24
+ return Response.json({ error: 'No user message' }, { status: 400 });
25
+ }
26
+
27
+ // Extract text from message parts (AI SDK v5+) or fall back to content
28
+ let userText =
29
+ lastUserMessage.parts
30
+ ?.filter((p) => p.type === 'text')
31
+ .map((p) => p.text)
32
+ .join('\n') ||
33
+ lastUserMessage.content ||
34
+ '';
35
+
36
+ // Extract file parts from message
37
+ const fileParts = lastUserMessage.parts?.filter((p) => p.type === 'file') || [];
38
+ const attachments = [];
39
+
40
+ for (const part of fileParts) {
41
+ const { mediaType, url } = part;
42
+ if (!mediaType || !url) continue;
43
+
44
+ if (mediaType.startsWith('image/') || mediaType === 'application/pdf') {
45
+ // Images and PDFs → pass as visual attachments for the LLM
46
+ attachments.push({ category: 'image', mimeType: mediaType, dataUrl: url });
47
+ } else if (mediaType.startsWith('text/') || mediaType === 'application/json') {
48
+ // Text files → decode base64 data URL and inline into message text
49
+ try {
50
+ const base64Data = url.split(',')[1];
51
+ const textContent = Buffer.from(base64Data, 'base64').toString('utf-8');
52
+ const fileName = part.name || 'file';
53
+ userText += `\n\nFile: ${fileName}\n\`\`\`\n${textContent}\n\`\`\``;
54
+ } catch (e) {
55
+ console.error('Failed to decode text file:', e);
56
+ }
57
+ }
58
+ }
59
+
60
+ if (!userText.trim() && attachments.length === 0) {
61
+ return Response.json({ error: 'Empty message' }, { status: 400 });
62
+ }
63
+
64
+ // Map web channel to thread_id — AI layer handles DB persistence
65
+ const threadId = rawChatId || crypto.randomUUID();
66
+ const { createUIMessageStream, createUIMessageStreamResponse } = await import('ai');
67
+
68
+ const stream = createUIMessageStream({
69
+ onError: (error) => {
70
+ console.error('Chat stream error:', error);
71
+ return error?.message || 'An error occurred while processing your message.';
72
+ },
73
+ execute: async ({ writer }) => {
74
+ // chatStream handles: save user msg, invoke agent, save assistant msg, auto-title
75
+ const chunks = chatStream(threadId, userText, attachments, {
76
+ userId: session.user.id,
77
+ });
78
+
79
+ // Signal start of assistant message
80
+ writer.write({ type: 'start' });
81
+
82
+ const textId = crypto.randomUUID();
83
+ let textStarted = false;
84
+
85
+ for await (const chunk of chunks) {
86
+ if (!textStarted) {
87
+ writer.write({ type: 'text-start', id: textId });
88
+ textStarted = true;
89
+ }
90
+ writer.write({ type: 'text-delta', id: textId, delta: chunk });
91
+ }
92
+
93
+ if (textStarted) {
94
+ writer.write({ type: 'text-end', id: textId });
95
+ }
96
+
97
+ // Signal end of assistant message
98
+ writer.write({ type: 'finish' });
99
+ },
100
+ });
101
+
102
+ return createUIMessageStreamResponse({ stream });
103
+ }
@@ -0,0 +1,161 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { SquarePenIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ClockIcon, ZapIcon } from "./icons.js";
5
+ import { getUnreadNotificationCount } from "../actions.js";
6
+ import { SidebarHistory } from "./sidebar-history.js";
7
+ import { SidebarUserNav } from "./sidebar-user-nav.js";
8
+ import {
9
+ Sidebar,
10
+ SidebarContent,
11
+ SidebarFooter,
12
+ SidebarHeader,
13
+ SidebarGroup,
14
+ SidebarGroupLabel,
15
+ SidebarMenu,
16
+ SidebarMenuItem,
17
+ SidebarMenuButton,
18
+ useSidebar
19
+ } from "./ui/sidebar.js";
20
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip.js";
21
+ import { useChatNav } from "./chat-nav-context.js";
22
+ function AppSidebar({ user }) {
23
+ const { navigateToChat } = useChatNav();
24
+ const { state, open, setOpenMobile, toggleSidebar } = useSidebar();
25
+ const collapsed = state === "collapsed";
26
+ const [unreadCount, setUnreadCount] = useState(0);
27
+ useEffect(() => {
28
+ getUnreadNotificationCount().then((count) => setUnreadCount(count)).catch(() => {
29
+ });
30
+ }, []);
31
+ return /* @__PURE__ */ jsxs(Sidebar, { children: [
32
+ /* @__PURE__ */ jsxs(SidebarHeader, { children: [
33
+ /* @__PURE__ */ jsxs("div", { className: collapsed ? "flex justify-center" : "flex items-center justify-between", children: [
34
+ !collapsed && /* @__PURE__ */ jsx("span", { className: "px-2 font-semibold text-lg", children: "The Pope Bot" }),
35
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
36
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
37
+ "button",
38
+ {
39
+ className: "inline-flex shrink-0 items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-background hover:text-foreground",
40
+ onClick: toggleSidebar,
41
+ children: /* @__PURE__ */ jsx(PanelLeftIcon, { size: 16 })
42
+ }
43
+ ) }),
44
+ /* @__PURE__ */ jsx(TooltipContent, { side: collapsed ? "right" : "bottom", children: collapsed ? "Open sidebar" : "Close sidebar" })
45
+ ] })
46
+ ] }),
47
+ /* @__PURE__ */ jsxs(SidebarMenu, { children: [
48
+ /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
49
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
50
+ SidebarMenuButton,
51
+ {
52
+ className: collapsed ? "justify-center" : "",
53
+ onClick: () => {
54
+ navigateToChat(null);
55
+ setOpenMobile(false);
56
+ },
57
+ children: [
58
+ /* @__PURE__ */ jsx(SquarePenIcon, { size: 16 }),
59
+ !collapsed && /* @__PURE__ */ jsx("span", { children: "New chat" })
60
+ ]
61
+ }
62
+ ) }),
63
+ collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "New chat" })
64
+ ] }) }),
65
+ /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
66
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
67
+ SidebarMenuButton,
68
+ {
69
+ className: collapsed ? "justify-center" : "",
70
+ onClick: () => {
71
+ window.location.href = "/chats";
72
+ },
73
+ children: [
74
+ /* @__PURE__ */ jsx(MessageIcon, { size: 16 }),
75
+ !collapsed && /* @__PURE__ */ jsx("span", { children: "Chats" })
76
+ ]
77
+ }
78
+ ) }),
79
+ collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Chats" })
80
+ ] }) }),
81
+ /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
82
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
83
+ SidebarMenuButton,
84
+ {
85
+ className: collapsed ? "justify-center" : "",
86
+ onClick: () => {
87
+ window.location.href = "/notifications";
88
+ },
89
+ children: [
90
+ /* @__PURE__ */ jsx(BellIcon, { size: 16 }),
91
+ !collapsed && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
92
+ "Notifications",
93
+ unreadCount > 0 && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center justify-center rounded-full bg-destructive px-1.5 py-0.5 text-[10px] font-medium leading-none text-destructive-foreground", children: unreadCount })
94
+ ] }),
95
+ collapsed && unreadCount > 0 && /* @__PURE__ */ jsx("span", { className: "absolute -top-1 -right-1 inline-flex h-4 w-4 items-center justify-center rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground", children: unreadCount })
96
+ ]
97
+ }
98
+ ) }),
99
+ collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Notifications" })
100
+ ] }) }),
101
+ /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
102
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
103
+ SidebarMenuButton,
104
+ {
105
+ className: collapsed ? "justify-center" : "",
106
+ onClick: () => {
107
+ window.location.href = "/swarm";
108
+ },
109
+ children: [
110
+ /* @__PURE__ */ jsx(SwarmIcon, { size: 16 }),
111
+ !collapsed && /* @__PURE__ */ jsx("span", { children: "Swarm" })
112
+ ]
113
+ }
114
+ ) }),
115
+ collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Swarm" })
116
+ ] }) }),
117
+ /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
118
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
119
+ SidebarMenuButton,
120
+ {
121
+ className: collapsed ? "justify-center" : "",
122
+ onClick: () => {
123
+ window.location.href = "/crons";
124
+ },
125
+ children: [
126
+ /* @__PURE__ */ jsx(ClockIcon, { size: 16 }),
127
+ !collapsed && /* @__PURE__ */ jsx("span", { children: "Cron Jobs" })
128
+ ]
129
+ }
130
+ ) }),
131
+ collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Cron Jobs" })
132
+ ] }) }),
133
+ /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
134
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
135
+ SidebarMenuButton,
136
+ {
137
+ className: collapsed ? "justify-center" : "",
138
+ onClick: () => {
139
+ window.location.href = "/triggers";
140
+ },
141
+ children: [
142
+ /* @__PURE__ */ jsx(ZapIcon, { size: 16 }),
143
+ !collapsed && /* @__PURE__ */ jsx("span", { children: "Triggers" })
144
+ ]
145
+ }
146
+ ) }),
147
+ collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Triggers" })
148
+ ] }) })
149
+ ] })
150
+ ] }),
151
+ !collapsed && /* @__PURE__ */ jsxs(SidebarContent, { children: [
152
+ /* @__PURE__ */ jsx(SidebarGroup, { className: "pt-0", children: /* @__PURE__ */ jsx(SidebarGroupLabel, { children: "Chats" }) }),
153
+ /* @__PURE__ */ jsx(SidebarHistory, {})
154
+ ] }),
155
+ collapsed && /* @__PURE__ */ jsx("div", { className: "flex-1" }),
156
+ /* @__PURE__ */ jsx(SidebarFooter, { children: user && /* @__PURE__ */ jsx(SidebarUserNav, { user, collapsed }) })
157
+ ] });
158
+ }
159
+ export {
160
+ AppSidebar
161
+ };