@cortexmemory/cli 0.26.2 → 0.27.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/dist/commands/convex.js +1 -1
- package/dist/commands/convex.js.map +1 -1
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +771 -144
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +210 -36
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +273 -43
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +102 -46
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +94 -7
- package/dist/commands/status.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/app-template-sync.d.ts +95 -0
- package/dist/utils/app-template-sync.d.ts.map +1 -0
- package/dist/utils/app-template-sync.js +425 -0
- package/dist/utils/app-template-sync.js.map +1 -0
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +20 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/deployment-selector.d.ts +21 -0
- package/dist/utils/deployment-selector.d.ts.map +1 -1
- package/dist/utils/deployment-selector.js +32 -0
- package/dist/utils/deployment-selector.js.map +1 -1
- package/dist/utils/init/graph-setup.d.ts.map +1 -1
- package/dist/utils/init/graph-setup.js +25 -2
- package/dist/utils/init/graph-setup.js.map +1 -1
- package/dist/utils/init/quickstart-setup.d.ts +87 -0
- package/dist/utils/init/quickstart-setup.d.ts.map +1 -0
- package/dist/utils/init/quickstart-setup.js +462 -0
- package/dist/utils/init/quickstart-setup.js.map +1 -0
- package/dist/utils/schema-sync.d.ts.map +1 -1
- package/dist/utils/schema-sync.js +27 -21
- package/dist/utils/schema-sync.js.map +1 -1
- package/package.json +3 -2
- package/templates/vercel-ai-quickstart/.env.local.example +45 -0
- package/templates/vercel-ai-quickstart/README.md +280 -0
- package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
- package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +83 -0
- package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
- package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
- package/templates/vercel-ai-quickstart/app/api/chat/route.ts +277 -0
- package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
- package/templates/vercel-ai-quickstart/app/api/facts/route.ts +39 -0
- package/templates/vercel-ai-quickstart/app/api/health/route.ts +99 -0
- package/templates/vercel-ai-quickstart/app/api/memories/route.ts +37 -0
- package/templates/vercel-ai-quickstart/app/globals.css +275 -0
- package/templates/vercel-ai-quickstart/app/layout.tsx +19 -0
- package/templates/vercel-ai-quickstart/app/page.tsx +216 -0
- package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
- package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
- package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
- package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +334 -0
- package/templates/vercel-ai-quickstart/components/ConvexClientProvider.tsx +21 -0
- package/templates/vercel-ai-quickstart/components/DataPreview.tsx +57 -0
- package/templates/vercel-ai-quickstart/components/HealthStatus.tsx +214 -0
- package/templates/vercel-ai-quickstart/components/LayerCard.tsx +263 -0
- package/templates/vercel-ai-quickstart/components/LayerFlowDiagram.tsx +195 -0
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/components/MemorySpaceSwitcher.tsx +93 -0
- package/templates/vercel-ai-quickstart/convex/conversations.ts +67 -0
- package/templates/vercel-ai-quickstart/convex/facts.ts +131 -0
- package/templates/vercel-ai-quickstart/convex/health.ts +15 -0
- package/templates/vercel-ai-quickstart/convex/memories.ts +104 -0
- package/templates/vercel-ai-quickstart/convex/schema.ts +20 -0
- package/templates/vercel-ai-quickstart/convex/users.ts +105 -0
- package/templates/vercel-ai-quickstart/jest.config.js +45 -0
- package/templates/vercel-ai-quickstart/lib/animations.ts +146 -0
- package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
- package/templates/vercel-ai-quickstart/lib/layer-tracking.ts +214 -0
- package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
- package/templates/vercel-ai-quickstart/next.config.js +27 -0
- package/templates/vercel-ai-quickstart/package.json +46 -0
- package/templates/vercel-ai-quickstart/postcss.config.js +5 -0
- package/templates/vercel-ai-quickstart/tailwind.config.js +37 -0
- package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
- package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
- package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
- package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
- package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
- package/templates/vercel-ai-quickstart/tsconfig.json +33 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { useAuth } from "./AuthProvider";
|
|
5
|
+
|
|
6
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
7
|
+
// Types
|
|
8
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
9
|
+
|
|
10
|
+
interface Conversation {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
messageCount: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ChatHistorySidebarProps {
|
|
19
|
+
memorySpaceId: string;
|
|
20
|
+
currentConversationId: string | null;
|
|
21
|
+
onSelectConversation: (conversationId: string) => void;
|
|
22
|
+
onNewChat: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
|
+
// Helpers
|
|
27
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
28
|
+
|
|
29
|
+
function groupByDate(conversations: Conversation[]): Record<string, Conversation[]> {
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
32
|
+
const yesterday = today - 86400000;
|
|
33
|
+
const weekAgo = today - 7 * 86400000;
|
|
34
|
+
|
|
35
|
+
const groups: Record<string, Conversation[]> = {
|
|
36
|
+
Today: [],
|
|
37
|
+
Yesterday: [],
|
|
38
|
+
"Last 7 Days": [],
|
|
39
|
+
Older: [],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const conv of conversations) {
|
|
43
|
+
const convDate = new Date(conv.updatedAt);
|
|
44
|
+
const convDay = new Date(
|
|
45
|
+
convDate.getFullYear(),
|
|
46
|
+
convDate.getMonth(),
|
|
47
|
+
convDate.getDate()
|
|
48
|
+
).getTime();
|
|
49
|
+
|
|
50
|
+
if (convDay >= today) {
|
|
51
|
+
groups.Today.push(conv);
|
|
52
|
+
} else if (convDay >= yesterday) {
|
|
53
|
+
groups.Yesterday.push(conv);
|
|
54
|
+
} else if (convDay >= weekAgo) {
|
|
55
|
+
groups["Last 7 Days"].push(conv);
|
|
56
|
+
} else {
|
|
57
|
+
groups.Older.push(conv);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return groups;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatTime(timestamp: number): string {
|
|
65
|
+
return new Date(timestamp).toLocaleTimeString("en-US", {
|
|
66
|
+
hour: "numeric",
|
|
67
|
+
minute: "2-digit",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
72
|
+
// Component
|
|
73
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
74
|
+
|
|
75
|
+
export function ChatHistorySidebar({
|
|
76
|
+
memorySpaceId,
|
|
77
|
+
currentConversationId,
|
|
78
|
+
onSelectConversation,
|
|
79
|
+
onNewChat,
|
|
80
|
+
}: ChatHistorySidebarProps) {
|
|
81
|
+
const { user, logout } = useAuth();
|
|
82
|
+
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
83
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
84
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
85
|
+
|
|
86
|
+
// Fetch conversations
|
|
87
|
+
const fetchConversations = useCallback(async () => {
|
|
88
|
+
if (!user) return;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(
|
|
92
|
+
`/api/conversations?userId=${encodeURIComponent(user.id)}&memorySpaceId=${encodeURIComponent(memorySpaceId)}`
|
|
93
|
+
);
|
|
94
|
+
const data = await response.json();
|
|
95
|
+
if (data.conversations) {
|
|
96
|
+
setConversations(data.conversations);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("Failed to fetch conversations:", error);
|
|
100
|
+
} finally {
|
|
101
|
+
setIsLoading(false);
|
|
102
|
+
}
|
|
103
|
+
}, [user, memorySpaceId]);
|
|
104
|
+
|
|
105
|
+
// Fetch on mount and when dependencies change
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
fetchConversations();
|
|
108
|
+
}, [fetchConversations]);
|
|
109
|
+
|
|
110
|
+
// Refresh conversations periodically (every 10 seconds)
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const interval = setInterval(fetchConversations, 10000);
|
|
113
|
+
return () => clearInterval(interval);
|
|
114
|
+
}, [fetchConversations]);
|
|
115
|
+
|
|
116
|
+
// Delete conversation
|
|
117
|
+
const handleDelete = async (e: React.MouseEvent, conversationId: string) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
if (deletingId) return;
|
|
120
|
+
|
|
121
|
+
setDeletingId(conversationId);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(`/api/conversations?conversationId=${encodeURIComponent(conversationId)}`, {
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const data = await response.json().catch(() => ({}));
|
|
130
|
+
throw new Error(data.error || `Delete failed with status ${response.status}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Remove from local state only after successful deletion
|
|
134
|
+
setConversations((prev) => prev.filter((c) => c.id !== conversationId));
|
|
135
|
+
|
|
136
|
+
// If deleted conversation was selected, trigger new chat
|
|
137
|
+
if (conversationId === currentConversationId) {
|
|
138
|
+
onNewChat();
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error("Failed to delete conversation:", error);
|
|
142
|
+
} finally {
|
|
143
|
+
setDeletingId(null);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const groupedConversations = groupByDate(conversations);
|
|
148
|
+
const groups = ["Today", "Yesterday", "Last 7 Days", "Older"];
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="w-64 h-full flex flex-col bg-black/40 border-r border-white/10">
|
|
152
|
+
{/* Header */}
|
|
153
|
+
<div className="p-4 border-b border-white/10">
|
|
154
|
+
<div className="flex items-center gap-2 mb-4">
|
|
155
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center">
|
|
156
|
+
<span className="text-sm">🧠</span>
|
|
157
|
+
</div>
|
|
158
|
+
<span className="font-semibold text-sm">Cortex Demo</span>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<button
|
|
162
|
+
onClick={onNewChat}
|
|
163
|
+
className="w-full py-2.5 px-4 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-sm font-medium transition-colors flex items-center gap-2"
|
|
164
|
+
>
|
|
165
|
+
<svg
|
|
166
|
+
className="w-4 h-4"
|
|
167
|
+
fill="none"
|
|
168
|
+
viewBox="0 0 24 24"
|
|
169
|
+
stroke="currentColor"
|
|
170
|
+
>
|
|
171
|
+
<path
|
|
172
|
+
strokeLinecap="round"
|
|
173
|
+
strokeLinejoin="round"
|
|
174
|
+
strokeWidth={2}
|
|
175
|
+
d="M12 4v16m8-8H4"
|
|
176
|
+
/>
|
|
177
|
+
</svg>
|
|
178
|
+
New Chat
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Conversation List */}
|
|
183
|
+
<div className="flex-1 overflow-y-auto p-2">
|
|
184
|
+
{isLoading ? (
|
|
185
|
+
<div className="flex items-center justify-center py-8">
|
|
186
|
+
<svg
|
|
187
|
+
className="animate-spin h-5 w-5 text-gray-400"
|
|
188
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
189
|
+
fill="none"
|
|
190
|
+
viewBox="0 0 24 24"
|
|
191
|
+
>
|
|
192
|
+
<circle
|
|
193
|
+
className="opacity-25"
|
|
194
|
+
cx="12"
|
|
195
|
+
cy="12"
|
|
196
|
+
r="10"
|
|
197
|
+
stroke="currentColor"
|
|
198
|
+
strokeWidth="4"
|
|
199
|
+
/>
|
|
200
|
+
<path
|
|
201
|
+
className="opacity-75"
|
|
202
|
+
fill="currentColor"
|
|
203
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
204
|
+
/>
|
|
205
|
+
</svg>
|
|
206
|
+
</div>
|
|
207
|
+
) : conversations.length === 0 ? (
|
|
208
|
+
<div className="text-center py-8 text-gray-500 text-sm">
|
|
209
|
+
<p>No conversations yet</p>
|
|
210
|
+
<p className="mt-1 text-xs">Start a new chat to begin</p>
|
|
211
|
+
</div>
|
|
212
|
+
) : (
|
|
213
|
+
<div className="space-y-4">
|
|
214
|
+
{groups.map((group) => {
|
|
215
|
+
const groupConvos = groupedConversations[group];
|
|
216
|
+
if (groupConvos.length === 0) return null;
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div key={group}>
|
|
220
|
+
<div className="px-2 py-1 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
221
|
+
{group}
|
|
222
|
+
</div>
|
|
223
|
+
<div className="space-y-1">
|
|
224
|
+
{groupConvos.map((conv) => (
|
|
225
|
+
<div
|
|
226
|
+
key={conv.id}
|
|
227
|
+
onClick={() => onSelectConversation(conv.id)}
|
|
228
|
+
className={`group relative px-3 py-2.5 rounded-xl cursor-pointer transition-colors ${
|
|
229
|
+
conv.id === currentConversationId
|
|
230
|
+
? "bg-cortex-600/20 text-white"
|
|
231
|
+
: "hover:bg-white/5 text-gray-300"
|
|
232
|
+
}`}
|
|
233
|
+
>
|
|
234
|
+
<div className="pr-6">
|
|
235
|
+
<div className="text-sm font-medium truncate">
|
|
236
|
+
{conv.title}
|
|
237
|
+
</div>
|
|
238
|
+
<div className="text-xs text-gray-500 mt-0.5">
|
|
239
|
+
{formatTime(conv.updatedAt)}
|
|
240
|
+
{conv.messageCount > 0 && (
|
|
241
|
+
<span className="ml-2">
|
|
242
|
+
{conv.messageCount} message
|
|
243
|
+
{conv.messageCount !== 1 ? "s" : ""}
|
|
244
|
+
</span>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* Delete button */}
|
|
250
|
+
<button
|
|
251
|
+
onClick={(e) => handleDelete(e, conv.id)}
|
|
252
|
+
disabled={deletingId === conv.id}
|
|
253
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 opacity-0 group-hover:opacity-100 hover:bg-white/10 rounded transition-all"
|
|
254
|
+
title="Delete conversation"
|
|
255
|
+
>
|
|
256
|
+
{deletingId === conv.id ? (
|
|
257
|
+
<svg
|
|
258
|
+
className="w-4 h-4 animate-spin text-gray-400"
|
|
259
|
+
fill="none"
|
|
260
|
+
viewBox="0 0 24 24"
|
|
261
|
+
>
|
|
262
|
+
<circle
|
|
263
|
+
className="opacity-25"
|
|
264
|
+
cx="12"
|
|
265
|
+
cy="12"
|
|
266
|
+
r="10"
|
|
267
|
+
stroke="currentColor"
|
|
268
|
+
strokeWidth="4"
|
|
269
|
+
/>
|
|
270
|
+
<path
|
|
271
|
+
className="opacity-75"
|
|
272
|
+
fill="currentColor"
|
|
273
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
274
|
+
/>
|
|
275
|
+
</svg>
|
|
276
|
+
) : (
|
|
277
|
+
<svg
|
|
278
|
+
className="w-4 h-4 text-gray-400 hover:text-red-400"
|
|
279
|
+
fill="none"
|
|
280
|
+
viewBox="0 0 24 24"
|
|
281
|
+
stroke="currentColor"
|
|
282
|
+
>
|
|
283
|
+
<path
|
|
284
|
+
strokeLinecap="round"
|
|
285
|
+
strokeLinejoin="round"
|
|
286
|
+
strokeWidth={2}
|
|
287
|
+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
288
|
+
/>
|
|
289
|
+
</svg>
|
|
290
|
+
)}
|
|
291
|
+
</button>
|
|
292
|
+
</div>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
})}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{/* User Section */}
|
|
303
|
+
<div className="p-4 border-t border-white/10">
|
|
304
|
+
<div className="flex items-center gap-3">
|
|
305
|
+
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center text-sm font-medium">
|
|
306
|
+
{user?.displayName?.charAt(0).toUpperCase() || "U"}
|
|
307
|
+
</div>
|
|
308
|
+
<div className="flex-1 min-w-0">
|
|
309
|
+
<div className="text-sm font-medium truncate">
|
|
310
|
+
{user?.displayName || user?.id}
|
|
311
|
+
</div>
|
|
312
|
+
<button
|
|
313
|
+
onClick={logout}
|
|
314
|
+
className="text-xs text-gray-500 hover:text-white transition-colors"
|
|
315
|
+
>
|
|
316
|
+
Sign out
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useChat } from "@ai-sdk/react";
|
|
4
|
+
import { DefaultChatTransport } from "ai";
|
|
5
|
+
import { useState, useRef, useEffect, useMemo, useCallback } from "react";
|
|
6
|
+
import type {
|
|
7
|
+
LayerStatus,
|
|
8
|
+
MemoryLayer,
|
|
9
|
+
LayerState,
|
|
10
|
+
RevisionAction,
|
|
11
|
+
} from "@/lib/layer-tracking";
|
|
12
|
+
|
|
13
|
+
// Type for layer update data parts from the stream
|
|
14
|
+
interface LayerUpdateData {
|
|
15
|
+
layer: MemoryLayer;
|
|
16
|
+
status: LayerStatus;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
latencyMs?: number;
|
|
19
|
+
data?: LayerState["data"];
|
|
20
|
+
error?: { message: string; code?: string };
|
|
21
|
+
revisionAction?: RevisionAction;
|
|
22
|
+
supersededFacts?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ChatInterfaceProps {
|
|
26
|
+
memorySpaceId: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
conversationId: string | null;
|
|
29
|
+
onOrchestrationStart?: () => void;
|
|
30
|
+
onLayerUpdate?: (
|
|
31
|
+
layer: MemoryLayer,
|
|
32
|
+
status: LayerStatus,
|
|
33
|
+
data?: LayerState["data"],
|
|
34
|
+
revisionInfo?: {
|
|
35
|
+
action?: RevisionAction;
|
|
36
|
+
supersededFacts?: string[];
|
|
37
|
+
},
|
|
38
|
+
) => void;
|
|
39
|
+
onReset?: () => void;
|
|
40
|
+
onConversationUpdate?: (conversationId: string, title: string) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ChatInterface({
|
|
44
|
+
memorySpaceId,
|
|
45
|
+
userId,
|
|
46
|
+
conversationId,
|
|
47
|
+
onOrchestrationStart,
|
|
48
|
+
onLayerUpdate,
|
|
49
|
+
onReset,
|
|
50
|
+
onConversationUpdate,
|
|
51
|
+
}: ChatInterfaceProps) {
|
|
52
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const [input, setInput] = useState("");
|
|
54
|
+
const [suggestedMessages] = useState([
|
|
55
|
+
"Hi! My name is Alex and I work at Acme Corp as a senior engineer.",
|
|
56
|
+
"My favorite color is blue and I love hiking on weekends.",
|
|
57
|
+
"I'm learning Spanish and prefer dark mode interfaces.",
|
|
58
|
+
"What do you remember about me?",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
// Track conversation ID in a ref for immediate access in callbacks
|
|
62
|
+
// This ensures the latest conversationId is always used when sending messages,
|
|
63
|
+
// even before React re-renders and recreates the transport
|
|
64
|
+
const conversationIdRef = useRef<string | null>(conversationId);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
conversationIdRef.current = conversationId;
|
|
67
|
+
}, [conversationId]);
|
|
68
|
+
|
|
69
|
+
// Create transport with a function that reads from ref for conversationId
|
|
70
|
+
// This ensures we always send the latest conversationId
|
|
71
|
+
const transport = useMemo(
|
|
72
|
+
() =>
|
|
73
|
+
new DefaultChatTransport({
|
|
74
|
+
api: "/api/chat",
|
|
75
|
+
// Use a function to get body so it reads latest conversationId from ref
|
|
76
|
+
body: () => ({
|
|
77
|
+
memorySpaceId,
|
|
78
|
+
userId,
|
|
79
|
+
conversationId: conversationIdRef.current,
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
[memorySpaceId, userId], // Note: conversationId removed - ref handles updates
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Handle layer data parts from the stream
|
|
86
|
+
const handleDataPart = useCallback(
|
|
87
|
+
(dataPart: unknown) => {
|
|
88
|
+
const part = dataPart as { type: string; data?: unknown };
|
|
89
|
+
if (part.type === "data-orchestration-start") {
|
|
90
|
+
onOrchestrationStart?.();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (part.type === "data-layer-update") {
|
|
94
|
+
const event = part.data as LayerUpdateData;
|
|
95
|
+
onLayerUpdate?.(event.layer, event.status, event.data, {
|
|
96
|
+
action: event.revisionAction,
|
|
97
|
+
supersededFacts: event.supersededFacts,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle conversation title update
|
|
102
|
+
if (part.type === "data-conversation-update") {
|
|
103
|
+
const update = part.data as { conversationId: string; title: string };
|
|
104
|
+
onConversationUpdate?.(update.conversationId, update.title);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[onOrchestrationStart, onLayerUpdate, onConversationUpdate],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const { messages, sendMessage, status, setMessages } = useChat({
|
|
111
|
+
transport,
|
|
112
|
+
onData: handleDataPart,
|
|
113
|
+
onError: (error) => {
|
|
114
|
+
console.error("Chat error:", error);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
|
119
|
+
|
|
120
|
+
// Load messages when conversation changes
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
// Clear messages first
|
|
123
|
+
setMessages([]);
|
|
124
|
+
|
|
125
|
+
// If no conversation selected, nothing more to do
|
|
126
|
+
if (!conversationId) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fetch conversation history
|
|
131
|
+
const loadConversationHistory = async () => {
|
|
132
|
+
setIsLoadingHistory(true);
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetch(
|
|
135
|
+
`/api/conversations?conversationId=${encodeURIComponent(conversationId)}`
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
console.error("Failed to load conversation history");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const data = await response.json();
|
|
144
|
+
|
|
145
|
+
if (data.messages && data.messages.length > 0) {
|
|
146
|
+
// Transform to the format expected by useChat
|
|
147
|
+
const loadedMessages = data.messages.map((msg: { id: string; role: string; content: string; createdAt: string }) => ({
|
|
148
|
+
id: msg.id,
|
|
149
|
+
role: msg.role,
|
|
150
|
+
content: msg.content,
|
|
151
|
+
createdAt: new Date(msg.createdAt),
|
|
152
|
+
}));
|
|
153
|
+
setMessages(loadedMessages);
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error("Error loading conversation history:", error);
|
|
157
|
+
} finally {
|
|
158
|
+
setIsLoadingHistory(false);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
loadConversationHistory();
|
|
163
|
+
}, [conversationId, setMessages]);
|
|
164
|
+
|
|
165
|
+
// Determine if we're actively streaming (only time to show typing indicator)
|
|
166
|
+
const isStreaming = status === "streaming";
|
|
167
|
+
|
|
168
|
+
// Auto-scroll to latest message
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
171
|
+
}, [messages]);
|
|
172
|
+
|
|
173
|
+
// Handle form submission
|
|
174
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
if (!input.trim() || isStreaming) return;
|
|
177
|
+
|
|
178
|
+
const message = input.trim();
|
|
179
|
+
setInput("");
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await sendMessage({ text: message });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("Failed to send message:", error);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleSuggestedMessage = (message: string) => {
|
|
189
|
+
setInput(message);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Extract text content from message parts (AI SDK v5 format)
|
|
193
|
+
const getMessageContent = (message: { content?: string; parts?: Array<{ type: string; text?: string }> }): string => {
|
|
194
|
+
if (typeof message.content === "string") {
|
|
195
|
+
return message.content;
|
|
196
|
+
}
|
|
197
|
+
if (message.parts) {
|
|
198
|
+
return message.parts
|
|
199
|
+
.filter((part) => part.type === "text")
|
|
200
|
+
.map((part) => part.text)
|
|
201
|
+
.join("");
|
|
202
|
+
}
|
|
203
|
+
return "";
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className="flex flex-col h-full">
|
|
208
|
+
{/* Messages */}
|
|
209
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
210
|
+
{isLoadingHistory && (
|
|
211
|
+
<div className="flex items-center justify-center py-12">
|
|
212
|
+
<div className="flex flex-col items-center gap-3">
|
|
213
|
+
<svg
|
|
214
|
+
className="animate-spin h-8 w-8 text-cortex-500"
|
|
215
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
216
|
+
fill="none"
|
|
217
|
+
viewBox="0 0 24 24"
|
|
218
|
+
>
|
|
219
|
+
<circle
|
|
220
|
+
className="opacity-25"
|
|
221
|
+
cx="12"
|
|
222
|
+
cy="12"
|
|
223
|
+
r="10"
|
|
224
|
+
stroke="currentColor"
|
|
225
|
+
strokeWidth="4"
|
|
226
|
+
/>
|
|
227
|
+
<path
|
|
228
|
+
className="opacity-75"
|
|
229
|
+
fill="currentColor"
|
|
230
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
231
|
+
/>
|
|
232
|
+
</svg>
|
|
233
|
+
<span className="text-gray-400 text-sm">Loading conversation...</span>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{!isLoadingHistory && messages.length === 0 && (
|
|
239
|
+
<div className="text-center py-12">
|
|
240
|
+
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-cortex-500/20 to-cortex-700/20 flex items-center justify-center">
|
|
241
|
+
<span className="text-3xl">🧠</span>
|
|
242
|
+
</div>
|
|
243
|
+
<h2 className="text-xl font-semibold mb-2">
|
|
244
|
+
{conversationId ? "Continue your conversation" : "Start a new conversation"}
|
|
245
|
+
</h2>
|
|
246
|
+
<p className="text-gray-400 max-w-md mx-auto mb-6">
|
|
247
|
+
This demo shows how Cortex orchestrates memory across multiple
|
|
248
|
+
layers in real-time. Try telling me about yourself!
|
|
249
|
+
</p>
|
|
250
|
+
|
|
251
|
+
{/* Suggested messages */}
|
|
252
|
+
<div className="flex flex-wrap justify-center gap-2 max-w-lg mx-auto">
|
|
253
|
+
{suggestedMessages.map((msg, i) => (
|
|
254
|
+
<button
|
|
255
|
+
key={i}
|
|
256
|
+
onClick={() => handleSuggestedMessage(msg)}
|
|
257
|
+
className="px-3 py-2 text-sm bg-white/5 hover:bg-white/10 rounded-lg border border-white/10 transition-colors text-left"
|
|
258
|
+
>
|
|
259
|
+
{msg.length > 40 ? msg.slice(0, 40) + "..." : msg}
|
|
260
|
+
</button>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{messages.map((message, i) => (
|
|
267
|
+
<div
|
|
268
|
+
key={message.id || i}
|
|
269
|
+
className={`message-animate flex ${
|
|
270
|
+
message.role === "user" ? "justify-end" : "justify-start"
|
|
271
|
+
}`}
|
|
272
|
+
>
|
|
273
|
+
<div
|
|
274
|
+
className={`max-w-[80%] px-4 py-3 rounded-2xl ${
|
|
275
|
+
message.role === "user"
|
|
276
|
+
? "bg-cortex-600 text-white"
|
|
277
|
+
: "bg-white/10 text-white"
|
|
278
|
+
}`}
|
|
279
|
+
>
|
|
280
|
+
<p className="whitespace-pre-wrap">
|
|
281
|
+
{getMessageContent(message)}
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
))}
|
|
286
|
+
|
|
287
|
+
{/* Typing indicator - only show during active streaming */}
|
|
288
|
+
{isStreaming && (
|
|
289
|
+
<div className="flex justify-start">
|
|
290
|
+
<div className="bg-white/10 px-4 py-3 rounded-2xl">
|
|
291
|
+
<div className="flex gap-1">
|
|
292
|
+
<span className="w-2 h-2 bg-white/50 rounded-full animate-bounce" />
|
|
293
|
+
<span className="w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.1s]" />
|
|
294
|
+
<span className="w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.2s]" />
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
<div ref={messagesEndRef} />
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{/* Input - only disabled during active streaming, NOT during background orchestration */}
|
|
304
|
+
<div className="border-t border-white/10 p-4">
|
|
305
|
+
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
306
|
+
<input
|
|
307
|
+
type="text"
|
|
308
|
+
value={input}
|
|
309
|
+
onChange={(e) => setInput(e.target.value)}
|
|
310
|
+
placeholder="Tell me about yourself..."
|
|
311
|
+
className="flex-1 px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:border-cortex-500 transition-colors"
|
|
312
|
+
disabled={isStreaming}
|
|
313
|
+
/>
|
|
314
|
+
<button
|
|
315
|
+
type="submit"
|
|
316
|
+
disabled={isStreaming || !input.trim()}
|
|
317
|
+
className="px-6 py-3 bg-cortex-600 hover:bg-cortex-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-xl font-medium transition-colors"
|
|
318
|
+
>
|
|
319
|
+
Send
|
|
320
|
+
</button>
|
|
321
|
+
</form>
|
|
322
|
+
|
|
323
|
+
{messages.length > 0 && (
|
|
324
|
+
<button
|
|
325
|
+
onClick={onReset}
|
|
326
|
+
className="mt-2 text-sm text-gray-500 hover:text-white transition-colors"
|
|
327
|
+
>
|
|
328
|
+
Clear visualization →
|
|
329
|
+
</button>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
|
4
|
+
import { ReactNode, useMemo } from "react";
|
|
5
|
+
|
|
6
|
+
export function ConvexClientProvider({ children }: { children: ReactNode }) {
|
|
7
|
+
const convex = useMemo(() => {
|
|
8
|
+
if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
|
|
9
|
+
console.warn("NEXT_PUBLIC_CONVEX_URL not set");
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL);
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
if (!convex) {
|
|
16
|
+
// Render without Convex for development without backend
|
|
17
|
+
return <>{children}</>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
|
|
21
|
+
}
|