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,232 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect, useCallback, useState } from 'react';
|
|
4
|
+
import { SendIcon, StopIcon, PaperclipIcon, XIcon, FileTextIcon } from './icons.js';
|
|
5
|
+
import { cn } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
const ACCEPTED_TYPES = [
|
|
8
|
+
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
|
9
|
+
'application/pdf',
|
|
10
|
+
'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/css',
|
|
11
|
+
'text/javascript', 'text/x-python', 'text/x-typescript',
|
|
12
|
+
'application/json',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const MAX_FILES = 5;
|
|
16
|
+
|
|
17
|
+
function isAcceptedType(file) {
|
|
18
|
+
if (ACCEPTED_TYPES.includes(file.type)) return true;
|
|
19
|
+
// Fall back to extension for files with generic MIME types
|
|
20
|
+
const ext = file.name?.split('.').pop()?.toLowerCase();
|
|
21
|
+
const textExts = ['txt', 'md', 'csv', 'json', 'js', 'ts', 'jsx', 'tsx', 'py', 'html', 'css', 'yml', 'yaml', 'xml', 'sh', 'bash', 'rb', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'hpp'];
|
|
22
|
+
return textExts.includes(ext);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getEffectiveType(file) {
|
|
26
|
+
if (ACCEPTED_TYPES.includes(file.type) && file.type !== '') return file.type;
|
|
27
|
+
const ext = file.name?.split('.').pop()?.toLowerCase();
|
|
28
|
+
const extMap = {
|
|
29
|
+
txt: 'text/plain', md: 'text/markdown', csv: 'text/csv',
|
|
30
|
+
json: 'application/json', js: 'text/javascript', ts: 'text/x-typescript',
|
|
31
|
+
jsx: 'text/javascript', tsx: 'text/x-typescript', py: 'text/x-python',
|
|
32
|
+
html: 'text/html', css: 'text/css', yml: 'text/plain', yaml: 'text/plain',
|
|
33
|
+
xml: 'text/plain', sh: 'text/plain', bash: 'text/plain', rb: 'text/plain',
|
|
34
|
+
go: 'text/plain', rs: 'text/plain', java: 'text/plain', c: 'text/plain',
|
|
35
|
+
cpp: 'text/plain', h: 'text/plain', hpp: 'text/plain',
|
|
36
|
+
};
|
|
37
|
+
return extMap[ext] || file.type || 'text/plain';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles }) {
|
|
41
|
+
const textareaRef = useRef(null);
|
|
42
|
+
const fileInputRef = useRef(null);
|
|
43
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
44
|
+
const isStreaming = status === 'streaming' || status === 'submitted';
|
|
45
|
+
|
|
46
|
+
// Auto-resize textarea
|
|
47
|
+
const adjustHeight = useCallback(() => {
|
|
48
|
+
const textarea = textareaRef.current;
|
|
49
|
+
if (!textarea) return;
|
|
50
|
+
textarea.style.height = 'auto';
|
|
51
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
adjustHeight();
|
|
56
|
+
}, [input, adjustHeight]);
|
|
57
|
+
|
|
58
|
+
// Focus textarea on mount
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
textareaRef.current?.focus();
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const handleFiles = useCallback((fileList) => {
|
|
64
|
+
const newFiles = Array.from(fileList).filter(isAcceptedType);
|
|
65
|
+
if (newFiles.length === 0) return;
|
|
66
|
+
|
|
67
|
+
// Read files outside state updater to avoid React strict mode double-invocation
|
|
68
|
+
newFiles.forEach((file) => {
|
|
69
|
+
const reader = new FileReader();
|
|
70
|
+
reader.onload = () => {
|
|
71
|
+
setFiles((current) => {
|
|
72
|
+
if (current.length >= MAX_FILES) return current;
|
|
73
|
+
return [...current, { file, previewUrl: reader.result }];
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
reader.readAsDataURL(file);
|
|
77
|
+
});
|
|
78
|
+
}, [setFiles]);
|
|
79
|
+
|
|
80
|
+
const removeFile = (index) => {
|
|
81
|
+
setFiles((prev) => prev.filter((_, i) => i !== index));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleSubmit = (e) => {
|
|
85
|
+
if (e) e.preventDefault();
|
|
86
|
+
if ((!input.trim() && files.length === 0) || isStreaming) return;
|
|
87
|
+
onSubmit();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleKeyDown = (e) => {
|
|
91
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
handleSubmit();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleDragOver = (e) => {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
setIsDragging(true);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleDragLeave = (e) => {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
setIsDragging(false);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleDrop = (e) => {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
setIsDragging(false);
|
|
110
|
+
if (e.dataTransfer?.files?.length) {
|
|
111
|
+
handleFiles(e.dataTransfer.files);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const canSend = input.trim() || files.length > 0;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="mx-auto w-full max-w-4xl px-2 pb-4 md:px-4">
|
|
119
|
+
<form onSubmit={handleSubmit} className="relative">
|
|
120
|
+
<div
|
|
121
|
+
className={cn(
|
|
122
|
+
'flex flex-col rounded-xl border bg-muted p-2 transition-colors',
|
|
123
|
+
isDragging ? 'border-primary bg-primary/5' : 'border-border'
|
|
124
|
+
)}
|
|
125
|
+
onDragOver={handleDragOver}
|
|
126
|
+
onDragLeave={handleDragLeave}
|
|
127
|
+
onDrop={handleDrop}
|
|
128
|
+
>
|
|
129
|
+
{/* File preview strip */}
|
|
130
|
+
{files.length > 0 && (
|
|
131
|
+
<div className="mb-2 flex gap-2 overflow-x-auto px-1 py-1">
|
|
132
|
+
{files.map((f, i) => {
|
|
133
|
+
const isImage = f.file.type.startsWith('image/');
|
|
134
|
+
return (
|
|
135
|
+
<div key={i} className="group relative flex-shrink-0">
|
|
136
|
+
{isImage ? (
|
|
137
|
+
<img
|
|
138
|
+
src={f.previewUrl}
|
|
139
|
+
alt={f.file.name}
|
|
140
|
+
className="h-16 w-16 rounded-lg object-cover"
|
|
141
|
+
/>
|
|
142
|
+
) : (
|
|
143
|
+
<div className="flex h-16 items-center gap-1.5 rounded-lg bg-foreground/10 px-3">
|
|
144
|
+
<FileTextIcon size={14} />
|
|
145
|
+
<span className="max-w-[100px] truncate text-xs">
|
|
146
|
+
{f.file.name}
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={() => removeFile(i)}
|
|
153
|
+
className="absolute -right-1.5 -top-1.5 hidden rounded-full bg-foreground p-0.5 text-background group-hover:flex items-center justify-center"
|
|
154
|
+
aria-label={`Remove ${f.file.name}`}
|
|
155
|
+
>
|
|
156
|
+
<XIcon size={10} />
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
})}
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
<div className="flex items-end gap-2">
|
|
165
|
+
{/* Paperclip button */}
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={() => fileInputRef.current?.click()}
|
|
169
|
+
className="inline-flex items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground"
|
|
170
|
+
aria-label="Attach files"
|
|
171
|
+
disabled={isStreaming}
|
|
172
|
+
>
|
|
173
|
+
<PaperclipIcon size={16} />
|
|
174
|
+
</button>
|
|
175
|
+
|
|
176
|
+
<input
|
|
177
|
+
ref={fileInputRef}
|
|
178
|
+
type="file"
|
|
179
|
+
multiple
|
|
180
|
+
accept="image/*,application/pdf,text/*,application/json,.md,.csv,.json,.js,.ts,.jsx,.tsx,.py,.html,.css,.yml,.yaml,.xml,.sh,.rb,.go,.rs,.java,.c,.cpp,.h"
|
|
181
|
+
className="hidden"
|
|
182
|
+
onChange={(e) => {
|
|
183
|
+
if (e.target.files?.length) handleFiles(e.target.files);
|
|
184
|
+
e.target.value = '';
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
|
|
188
|
+
<textarea
|
|
189
|
+
ref={textareaRef}
|
|
190
|
+
value={input}
|
|
191
|
+
onChange={(e) => setInput(e.target.value)}
|
|
192
|
+
onKeyDown={handleKeyDown}
|
|
193
|
+
placeholder="Send a message..."
|
|
194
|
+
rows={1}
|
|
195
|
+
className={cn(
|
|
196
|
+
'flex-1 resize-none bg-transparent px-2 py-1.5 text-sm text-foreground',
|
|
197
|
+
'placeholder:text-muted-foreground focus:outline-none',
|
|
198
|
+
'max-h-[200px]'
|
|
199
|
+
)}
|
|
200
|
+
disabled={isStreaming}
|
|
201
|
+
/>
|
|
202
|
+
|
|
203
|
+
{isStreaming ? (
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={stop}
|
|
207
|
+
className="inline-flex items-center justify-center rounded-lg bg-foreground p-2 text-background hover:opacity-80"
|
|
208
|
+
aria-label="Stop generating"
|
|
209
|
+
>
|
|
210
|
+
<StopIcon size={16} />
|
|
211
|
+
</button>
|
|
212
|
+
) : (
|
|
213
|
+
<button
|
|
214
|
+
type="submit"
|
|
215
|
+
disabled={!canSend}
|
|
216
|
+
className={cn(
|
|
217
|
+
'inline-flex items-center justify-center rounded-lg p-2',
|
|
218
|
+
canSend
|
|
219
|
+
? 'bg-foreground text-background hover:opacity-80'
|
|
220
|
+
: 'bg-muted-foreground/20 text-muted-foreground cursor-not-allowed'
|
|
221
|
+
)}
|
|
222
|
+
aria-label="Send message"
|
|
223
|
+
>
|
|
224
|
+
<SendIcon size={16} />
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</form>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const ChatNavContext = createContext(null);
|
|
4
|
+
const ChatNavProvider = ChatNavContext.Provider;
|
|
5
|
+
function useChatNav() {
|
|
6
|
+
return useContext(ChatNavContext);
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
ChatNavProvider,
|
|
10
|
+
useChatNav
|
|
11
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { AppSidebar } from "./app-sidebar.js";
|
|
5
|
+
import { Chat } from "./chat.js";
|
|
6
|
+
import { SidebarProvider, SidebarInset } from "./ui/sidebar.js";
|
|
7
|
+
import { ChatNavProvider } from "./chat-nav-context.js";
|
|
8
|
+
import { getChatMessages } from "../actions.js";
|
|
9
|
+
function ChatPage({ session, needsSetup, chatId }) {
|
|
10
|
+
const [activeChatId, setActiveChatId] = useState(chatId || null);
|
|
11
|
+
const [resolvedChatId, setResolvedChatId] = useState(() => chatId ? null : crypto.randomUUID());
|
|
12
|
+
const [initialMessages, setInitialMessages] = useState([]);
|
|
13
|
+
const navigateToChat = useCallback((id) => {
|
|
14
|
+
if (id) {
|
|
15
|
+
window.history.pushState({}, "", `/chat/${id}`);
|
|
16
|
+
} else {
|
|
17
|
+
window.history.pushState({}, "", "/");
|
|
18
|
+
}
|
|
19
|
+
setActiveChatId(id);
|
|
20
|
+
}, []);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const onPopState = () => {
|
|
23
|
+
const match = window.location.pathname.match(/^\/chat\/(.+)/);
|
|
24
|
+
setActiveChatId(match ? match[1] : null);
|
|
25
|
+
};
|
|
26
|
+
window.addEventListener("popstate", onPopState);
|
|
27
|
+
return () => window.removeEventListener("popstate", onPopState);
|
|
28
|
+
}, []);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (activeChatId) {
|
|
31
|
+
getChatMessages(activeChatId).then((dbMessages) => {
|
|
32
|
+
if (dbMessages.length === 0) {
|
|
33
|
+
setInitialMessages([]);
|
|
34
|
+
setResolvedChatId(crypto.randomUUID());
|
|
35
|
+
window.history.replaceState({}, "", "/");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const uiMessages = dbMessages.map((msg) => ({
|
|
39
|
+
id: msg.id,
|
|
40
|
+
role: msg.role,
|
|
41
|
+
content: msg.content,
|
|
42
|
+
parts: [{ type: "text", text: msg.content }],
|
|
43
|
+
createdAt: new Date(msg.createdAt)
|
|
44
|
+
}));
|
|
45
|
+
setInitialMessages(uiMessages);
|
|
46
|
+
setResolvedChatId(activeChatId);
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
setInitialMessages([]);
|
|
50
|
+
setResolvedChatId(crypto.randomUUID());
|
|
51
|
+
}
|
|
52
|
+
}, [activeChatId]);
|
|
53
|
+
if (needsSetup || !session) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return /* @__PURE__ */ jsx(ChatNavProvider, { value: { activeChatId: resolvedChatId, navigateToChat }, children: /* @__PURE__ */ jsxs(SidebarProvider, { children: [
|
|
57
|
+
/* @__PURE__ */ jsx(AppSidebar, { user: session.user }),
|
|
58
|
+
/* @__PURE__ */ jsx(SidebarInset, { children: resolvedChatId && /* @__PURE__ */ jsx(
|
|
59
|
+
Chat,
|
|
60
|
+
{
|
|
61
|
+
chatId: resolvedChatId,
|
|
62
|
+
initialMessages
|
|
63
|
+
},
|
|
64
|
+
resolvedChatId
|
|
65
|
+
) })
|
|
66
|
+
] }) });
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
ChatPage
|
|
70
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { AppSidebar } from './app-sidebar.js';
|
|
5
|
+
import { Chat } from './chat.js';
|
|
6
|
+
import { SidebarProvider, SidebarInset } from './ui/sidebar.js';
|
|
7
|
+
import { ChatNavProvider } from './chat-nav-context.js';
|
|
8
|
+
import { getChatMessages } from '../actions.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Main chat page component.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} props
|
|
14
|
+
* @param {object|null} props.session - Auth session (null = not logged in)
|
|
15
|
+
* @param {boolean} props.needsSetup - Whether setup is needed
|
|
16
|
+
* @param {string} [props.chatId] - Chat ID from URL (only used for initial mount)
|
|
17
|
+
*/
|
|
18
|
+
export function ChatPage({ session, needsSetup, chatId }) {
|
|
19
|
+
const [activeChatId, setActiveChatId] = useState(chatId || null);
|
|
20
|
+
const [resolvedChatId, setResolvedChatId] = useState(() => chatId ? null : crypto.randomUUID());
|
|
21
|
+
const [initialMessages, setInitialMessages] = useState([]);
|
|
22
|
+
|
|
23
|
+
const navigateToChat = useCallback((id) => {
|
|
24
|
+
if (id) {
|
|
25
|
+
window.history.pushState({}, '', `/chat/${id}`);
|
|
26
|
+
} else {
|
|
27
|
+
window.history.pushState({}, '', '/');
|
|
28
|
+
}
|
|
29
|
+
setActiveChatId(id);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
// Browser back/forward
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const onPopState = () => {
|
|
35
|
+
const match = window.location.pathname.match(/^\/chat\/(.+)/);
|
|
36
|
+
setActiveChatId(match ? match[1] : null);
|
|
37
|
+
};
|
|
38
|
+
window.addEventListener('popstate', onPopState);
|
|
39
|
+
return () => window.removeEventListener('popstate', onPopState);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
// Load messages when activeChatId changes
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (activeChatId) {
|
|
45
|
+
getChatMessages(activeChatId).then((dbMessages) => {
|
|
46
|
+
if (dbMessages.length === 0) {
|
|
47
|
+
// Stale chat (e.g. after login with old UUID) — start fresh
|
|
48
|
+
setInitialMessages([]);
|
|
49
|
+
setResolvedChatId(crypto.randomUUID());
|
|
50
|
+
window.history.replaceState({}, '', '/');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const uiMessages = dbMessages.map((msg) => ({
|
|
54
|
+
id: msg.id,
|
|
55
|
+
role: msg.role,
|
|
56
|
+
content: msg.content,
|
|
57
|
+
parts: [{ type: 'text', text: msg.content }],
|
|
58
|
+
createdAt: new Date(msg.createdAt),
|
|
59
|
+
}));
|
|
60
|
+
setInitialMessages(uiMessages);
|
|
61
|
+
setResolvedChatId(activeChatId);
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
setInitialMessages([]);
|
|
65
|
+
setResolvedChatId(crypto.randomUUID());
|
|
66
|
+
}
|
|
67
|
+
}, [activeChatId]);
|
|
68
|
+
|
|
69
|
+
if (needsSetup || !session) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<ChatNavProvider value={{ activeChatId: resolvedChatId, navigateToChat }}>
|
|
75
|
+
<SidebarProvider>
|
|
76
|
+
<AppSidebar user={session.user} />
|
|
77
|
+
<SidebarInset>
|
|
78
|
+
{resolvedChatId && (
|
|
79
|
+
<Chat
|
|
80
|
+
key={resolvedChatId}
|
|
81
|
+
chatId={resolvedChatId}
|
|
82
|
+
initialMessages={initialMessages}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
</SidebarInset>
|
|
86
|
+
</SidebarProvider>
|
|
87
|
+
</ChatNavProvider>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useChat } from "@ai-sdk/react";
|
|
4
|
+
import { DefaultChatTransport } from "ai";
|
|
5
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
6
|
+
import { Messages } from "./messages.js";
|
|
7
|
+
import { ChatInput } from "./chat-input.js";
|
|
8
|
+
import { ChatHeader } from "./chat-header.js";
|
|
9
|
+
function Chat({ chatId, initialMessages = [] }) {
|
|
10
|
+
const [input, setInput] = useState("");
|
|
11
|
+
const [files, setFiles] = useState([]);
|
|
12
|
+
const hasNavigated = useRef(false);
|
|
13
|
+
const transport = useMemo(
|
|
14
|
+
() => new DefaultChatTransport({
|
|
15
|
+
api: "/api/chat",
|
|
16
|
+
body: { chatId }
|
|
17
|
+
}),
|
|
18
|
+
[chatId]
|
|
19
|
+
);
|
|
20
|
+
const {
|
|
21
|
+
messages,
|
|
22
|
+
status,
|
|
23
|
+
stop,
|
|
24
|
+
error,
|
|
25
|
+
sendMessage
|
|
26
|
+
} = useChat({
|
|
27
|
+
id: chatId,
|
|
28
|
+
messages: initialMessages,
|
|
29
|
+
transport,
|
|
30
|
+
onError: (err) => console.error("Chat error:", err)
|
|
31
|
+
});
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!hasNavigated.current && messages.length >= 2 && !window.location.pathname.includes(chatId)) {
|
|
34
|
+
hasNavigated.current = true;
|
|
35
|
+
window.history.replaceState({}, "", `/chat/${chatId}`);
|
|
36
|
+
window.dispatchEvent(new Event("chatsupdated"));
|
|
37
|
+
setTimeout(() => window.dispatchEvent(new Event("chatsupdated")), 5e3);
|
|
38
|
+
}
|
|
39
|
+
}, [messages.length, chatId]);
|
|
40
|
+
const handleSend = () => {
|
|
41
|
+
if (!input.trim() && files.length === 0) return;
|
|
42
|
+
const text = input;
|
|
43
|
+
const currentFiles = files;
|
|
44
|
+
setInput("");
|
|
45
|
+
setFiles([]);
|
|
46
|
+
if (currentFiles.length === 0) {
|
|
47
|
+
sendMessage({ text });
|
|
48
|
+
} else {
|
|
49
|
+
const fileParts = currentFiles.map((f) => ({
|
|
50
|
+
type: "file",
|
|
51
|
+
mediaType: f.file.type || "text/plain",
|
|
52
|
+
url: f.previewUrl,
|
|
53
|
+
filename: f.file.name
|
|
54
|
+
}));
|
|
55
|
+
sendMessage({ text: text || void 0, files: fileParts });
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-svh flex-col", children: [
|
|
59
|
+
/* @__PURE__ */ jsx(ChatHeader, { chatId }),
|
|
60
|
+
/* @__PURE__ */ jsx(Messages, { messages, status }),
|
|
61
|
+
error && /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-4xl px-2 md:px-4", children: /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive", children: error.message || "Something went wrong. Please try again." }) }),
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
ChatInput,
|
|
64
|
+
{
|
|
65
|
+
input,
|
|
66
|
+
setInput,
|
|
67
|
+
onSubmit: handleSend,
|
|
68
|
+
status,
|
|
69
|
+
stop,
|
|
70
|
+
files,
|
|
71
|
+
setFiles
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
] });
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
Chat
|
|
78
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useChat } from '@ai-sdk/react';
|
|
4
|
+
import { DefaultChatTransport } from 'ai';
|
|
5
|
+
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
6
|
+
import { Messages } from './messages.js';
|
|
7
|
+
import { ChatInput } from './chat-input.js';
|
|
8
|
+
import { ChatHeader } from './chat-header.js';
|
|
9
|
+
|
|
10
|
+
export function Chat({ chatId, initialMessages = [] }) {
|
|
11
|
+
const [input, setInput] = useState('');
|
|
12
|
+
const [files, setFiles] = useState([]);
|
|
13
|
+
const hasNavigated = useRef(false);
|
|
14
|
+
|
|
15
|
+
const transport = useMemo(
|
|
16
|
+
() =>
|
|
17
|
+
new DefaultChatTransport({
|
|
18
|
+
api: '/api/chat',
|
|
19
|
+
body: { chatId },
|
|
20
|
+
}),
|
|
21
|
+
[chatId]
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
messages,
|
|
26
|
+
status,
|
|
27
|
+
stop,
|
|
28
|
+
error,
|
|
29
|
+
sendMessage,
|
|
30
|
+
} = useChat({
|
|
31
|
+
id: chatId,
|
|
32
|
+
messages: initialMessages,
|
|
33
|
+
transport,
|
|
34
|
+
onError: (err) => console.error('Chat error:', err),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// After first exchange, update URL and notify sidebar
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!hasNavigated.current && messages.length >= 2 && !window.location.pathname.includes(chatId)) {
|
|
40
|
+
hasNavigated.current = true;
|
|
41
|
+
window.history.replaceState({}, '', `/chat/${chatId}`);
|
|
42
|
+
window.dispatchEvent(new Event('chatsupdated'));
|
|
43
|
+
// Dispatch again after delay to pick up async title update
|
|
44
|
+
setTimeout(() => window.dispatchEvent(new Event('chatsupdated')), 5000);
|
|
45
|
+
}
|
|
46
|
+
}, [messages.length, chatId]);
|
|
47
|
+
|
|
48
|
+
const handleSend = () => {
|
|
49
|
+
if (!input.trim() && files.length === 0) return;
|
|
50
|
+
const text = input;
|
|
51
|
+
const currentFiles = files;
|
|
52
|
+
setInput('');
|
|
53
|
+
setFiles([]);
|
|
54
|
+
|
|
55
|
+
if (currentFiles.length === 0) {
|
|
56
|
+
sendMessage({ text });
|
|
57
|
+
} else {
|
|
58
|
+
// Build FileUIPart[] from pre-read data URLs (File[] isn't a valid type)
|
|
59
|
+
const fileParts = currentFiles.map((f) => ({
|
|
60
|
+
type: 'file',
|
|
61
|
+
mediaType: f.file.type || 'text/plain',
|
|
62
|
+
url: f.previewUrl,
|
|
63
|
+
filename: f.file.name,
|
|
64
|
+
}));
|
|
65
|
+
sendMessage({ text: text || undefined, files: fileParts });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex h-svh flex-col">
|
|
71
|
+
<ChatHeader chatId={chatId} />
|
|
72
|
+
<Messages messages={messages} status={status} />
|
|
73
|
+
{error && (
|
|
74
|
+
<div className="mx-auto w-full max-w-4xl px-2 md:px-4">
|
|
75
|
+
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
|
|
76
|
+
{error.message || 'Something went wrong. Please try again.'}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
<ChatInput
|
|
81
|
+
input={input}
|
|
82
|
+
setInput={setInput}
|
|
83
|
+
onSubmit={handleSend}
|
|
84
|
+
status={status}
|
|
85
|
+
stop={stop}
|
|
86
|
+
files={files}
|
|
87
|
+
setFiles={setFiles}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|