@cortexmemory/cli 0.26.2 → 0.27.1
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/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +121 -10
- 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/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -1
- 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/init/graph-setup.d.ts.map +1 -1
- package/dist/utils/init/graph-setup.js +12 -0
- 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/chat/route.ts +196 -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 +114 -0
- package/templates/vercel-ai-quickstart/app/layout.tsx +19 -0
- package/templates/vercel-ai-quickstart/app/page.tsx +131 -0
- package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +237 -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/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/lib/animations.ts +146 -0
- package/templates/vercel-ai-quickstart/lib/layer-tracking.ts +214 -0
- package/templates/vercel-ai-quickstart/next.config.js +7 -0
- package/templates/vercel-ai-quickstart/package.json +41 -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/tsconfig.json +33 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* Source paths for v4 */
|
|
4
|
+
@source "../components/**/*.{js,ts,jsx,tsx}";
|
|
5
|
+
@source "./**/*.{js,ts,jsx,tsx}";
|
|
6
|
+
|
|
7
|
+
/* Custom theme for v4 */
|
|
8
|
+
@theme {
|
|
9
|
+
--color-cortex-50: #f0f7ff;
|
|
10
|
+
--color-cortex-100: #e0effe;
|
|
11
|
+
--color-cortex-200: #b9dffc;
|
|
12
|
+
--color-cortex-300: #7cc5fa;
|
|
13
|
+
--color-cortex-400: #36a8f5;
|
|
14
|
+
--color-cortex-500: #0c8ce6;
|
|
15
|
+
--color-cortex-600: #006fc4;
|
|
16
|
+
--color-cortex-700: #0159a0;
|
|
17
|
+
--color-cortex-800: #064b84;
|
|
18
|
+
--color-cortex-900: #0b3f6d;
|
|
19
|
+
--color-cortex-950: #072848;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Custom scrollbar */
|
|
23
|
+
::-webkit-scrollbar {
|
|
24
|
+
width: 8px;
|
|
25
|
+
height: 8px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
::-webkit-scrollbar-track {
|
|
29
|
+
background: rgba(255, 255, 255, 0.05);
|
|
30
|
+
border-radius: 4px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
::-webkit-scrollbar-thumb {
|
|
34
|
+
background: rgba(255, 255, 255, 0.2);
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
::-webkit-scrollbar-thumb:hover {
|
|
39
|
+
background: rgba(255, 255, 255, 0.3);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Layer flow animations */
|
|
43
|
+
@keyframes pulse-glow {
|
|
44
|
+
0%,
|
|
45
|
+
100% {
|
|
46
|
+
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
|
|
47
|
+
}
|
|
48
|
+
50% {
|
|
49
|
+
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@keyframes flow-down {
|
|
54
|
+
0% {
|
|
55
|
+
transform: translateY(-100%);
|
|
56
|
+
opacity: 0;
|
|
57
|
+
}
|
|
58
|
+
50% {
|
|
59
|
+
opacity: 1;
|
|
60
|
+
}
|
|
61
|
+
100% {
|
|
62
|
+
transform: translateY(100%);
|
|
63
|
+
opacity: 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.layer-complete {
|
|
68
|
+
animation: pulse-glow 1.5s ease-in-out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.flow-indicator {
|
|
72
|
+
animation: flow-down 1.5s ease-in-out infinite;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Chat message animations */
|
|
76
|
+
@keyframes message-in {
|
|
77
|
+
from {
|
|
78
|
+
opacity: 0;
|
|
79
|
+
transform: translateY(10px);
|
|
80
|
+
}
|
|
81
|
+
to {
|
|
82
|
+
opacity: 1;
|
|
83
|
+
transform: translateY(0);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.message-animate {
|
|
88
|
+
animation: message-in 0.3s ease-out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Gradient backgrounds */
|
|
92
|
+
.gradient-radial {
|
|
93
|
+
background: radial-gradient(
|
|
94
|
+
ellipse at center,
|
|
95
|
+
var(--tw-gradient-from) 0%,
|
|
96
|
+
var(--tw-gradient-to) 70%
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Glass effect */
|
|
101
|
+
.glass {
|
|
102
|
+
background: rgba(255, 255, 255, 0.05);
|
|
103
|
+
backdrop-filter: blur(10px);
|
|
104
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Code block styling */
|
|
108
|
+
.code-block {
|
|
109
|
+
background: rgba(0, 0, 0, 0.3);
|
|
110
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
111
|
+
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
|
112
|
+
font-size: 0.875rem;
|
|
113
|
+
line-height: 1.5;
|
|
114
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import "./globals.css";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: "Cortex Memory Quickstart",
|
|
6
|
+
description: "Interactive demo of Cortex Memory with Vercel AI SDK",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<body className="bg-gray-900 text-white min-h-screen">{children}</body>
|
|
17
|
+
</html>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { useLayerTracking } from "@/lib/layer-tracking";
|
|
6
|
+
|
|
7
|
+
// Dynamic imports to avoid SSR issues with framer-motion
|
|
8
|
+
const ChatInterface = dynamic(
|
|
9
|
+
() =>
|
|
10
|
+
import("@/components/ChatInterface").then((m) => ({
|
|
11
|
+
default: m.ChatInterface,
|
|
12
|
+
})),
|
|
13
|
+
{ ssr: false },
|
|
14
|
+
);
|
|
15
|
+
const LayerFlowDiagram = dynamic(
|
|
16
|
+
() =>
|
|
17
|
+
import("@/components/LayerFlowDiagram").then((m) => ({
|
|
18
|
+
default: m.LayerFlowDiagram,
|
|
19
|
+
})),
|
|
20
|
+
{ ssr: false },
|
|
21
|
+
);
|
|
22
|
+
const MemorySpaceSwitcher = dynamic(
|
|
23
|
+
() =>
|
|
24
|
+
import("@/components/MemorySpaceSwitcher").then((m) => ({
|
|
25
|
+
default: m.MemorySpaceSwitcher,
|
|
26
|
+
})),
|
|
27
|
+
{ ssr: false },
|
|
28
|
+
);
|
|
29
|
+
const HealthStatus = dynamic(
|
|
30
|
+
() =>
|
|
31
|
+
import("@/components/HealthStatus").then((m) => ({
|
|
32
|
+
default: m.HealthStatus,
|
|
33
|
+
})),
|
|
34
|
+
{ ssr: false },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export default function Home() {
|
|
38
|
+
const [memorySpaceId, setMemorySpaceId] = useState("quickstart-demo");
|
|
39
|
+
const [userId] = useState("demo-user");
|
|
40
|
+
const {
|
|
41
|
+
layers,
|
|
42
|
+
isOrchestrating,
|
|
43
|
+
startOrchestration,
|
|
44
|
+
updateLayer,
|
|
45
|
+
resetLayers,
|
|
46
|
+
} = useLayerTracking();
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<main className="min-h-screen flex flex-col">
|
|
50
|
+
{/* Header */}
|
|
51
|
+
<header className="border-b border-white/10 px-6 py-4">
|
|
52
|
+
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
|
53
|
+
<div className="flex items-center gap-3">
|
|
54
|
+
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center">
|
|
55
|
+
<span className="text-xl">🧠</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div>
|
|
58
|
+
<h1 className="text-xl font-bold">Cortex Memory Quickstart</h1>
|
|
59
|
+
<p className="text-sm text-gray-400">
|
|
60
|
+
Real-time memory orchestration demo
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="flex items-center gap-4">
|
|
66
|
+
<HealthStatus />
|
|
67
|
+
<MemorySpaceSwitcher
|
|
68
|
+
value={memorySpaceId}
|
|
69
|
+
onChange={setMemorySpaceId}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
|
|
75
|
+
{/* Main Content */}
|
|
76
|
+
<div className="flex-1 flex overflow-hidden">
|
|
77
|
+
{/* Chat Section */}
|
|
78
|
+
<div className="flex-1 flex flex-col border-r border-white/10">
|
|
79
|
+
<ChatInterface
|
|
80
|
+
memorySpaceId={memorySpaceId}
|
|
81
|
+
userId={userId}
|
|
82
|
+
onOrchestrationStart={startOrchestration}
|
|
83
|
+
onLayerUpdate={updateLayer}
|
|
84
|
+
onReset={resetLayers}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Layer Flow Visualization */}
|
|
89
|
+
<div className="w-[480px] flex flex-col bg-black/20">
|
|
90
|
+
<div className="p-4 border-b border-white/10">
|
|
91
|
+
<h2 className="font-semibold flex items-center gap-2">
|
|
92
|
+
<span className="text-lg">📊</span>
|
|
93
|
+
Memory Orchestration Flow
|
|
94
|
+
</h2>
|
|
95
|
+
<p className="text-sm text-gray-400 mt-1">
|
|
96
|
+
Watch data flow through Cortex layers in real-time
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
101
|
+
<LayerFlowDiagram
|
|
102
|
+
layers={layers}
|
|
103
|
+
isOrchestrating={isOrchestrating}
|
|
104
|
+
memorySpaceId={memorySpaceId}
|
|
105
|
+
userId={userId}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Footer */}
|
|
112
|
+
<footer className="border-t border-white/10 px-6 py-3">
|
|
113
|
+
<div className="max-w-7xl mx-auto flex items-center justify-between text-sm text-gray-500">
|
|
114
|
+
<div className="flex items-center gap-4">
|
|
115
|
+
<span>Cortex SDK v0.24.0</span>
|
|
116
|
+
<span>•</span>
|
|
117
|
+
<span>Vercel AI SDK v5</span>
|
|
118
|
+
</div>
|
|
119
|
+
<a
|
|
120
|
+
href="https://cortexmemory.dev/docs"
|
|
121
|
+
target="_blank"
|
|
122
|
+
rel="noopener noreferrer"
|
|
123
|
+
className="hover:text-white transition-colors"
|
|
124
|
+
>
|
|
125
|
+
Documentation →
|
|
126
|
+
</a>
|
|
127
|
+
</div>
|
|
128
|
+
</footer>
|
|
129
|
+
</main>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
onOrchestrationStart?: () => void;
|
|
29
|
+
onLayerUpdate?: (
|
|
30
|
+
layer: MemoryLayer,
|
|
31
|
+
status: LayerStatus,
|
|
32
|
+
data?: LayerState["data"],
|
|
33
|
+
revisionInfo?: {
|
|
34
|
+
action?: RevisionAction;
|
|
35
|
+
supersededFacts?: string[];
|
|
36
|
+
},
|
|
37
|
+
) => void;
|
|
38
|
+
onReset?: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ChatInterface({
|
|
42
|
+
memorySpaceId,
|
|
43
|
+
userId,
|
|
44
|
+
onOrchestrationStart,
|
|
45
|
+
onLayerUpdate,
|
|
46
|
+
onReset,
|
|
47
|
+
}: ChatInterfaceProps) {
|
|
48
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
const [input, setInput] = useState("");
|
|
50
|
+
const [suggestedMessages] = useState([
|
|
51
|
+
"Hi! My name is Alex and I work at Acme Corp as a senior engineer.",
|
|
52
|
+
"My favorite color is blue and I love hiking on weekends.",
|
|
53
|
+
"I'm learning Spanish and prefer dark mode interfaces.",
|
|
54
|
+
"What do you remember about me?",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// Create transport with body parameters - memoized to prevent recreation
|
|
58
|
+
const transport = useMemo(
|
|
59
|
+
() =>
|
|
60
|
+
new DefaultChatTransport({
|
|
61
|
+
api: "/api/chat",
|
|
62
|
+
body: { memorySpaceId, userId },
|
|
63
|
+
}),
|
|
64
|
+
[memorySpaceId, userId],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Handle layer data parts from the stream
|
|
68
|
+
const handleDataPart = useCallback(
|
|
69
|
+
(dataPart: any) => {
|
|
70
|
+
if (dataPart.type === "data-orchestration-start") {
|
|
71
|
+
onOrchestrationStart?.();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (dataPart.type === "data-layer-update") {
|
|
75
|
+
const event = dataPart.data as LayerUpdateData;
|
|
76
|
+
onLayerUpdate?.(event.layer, event.status, event.data, {
|
|
77
|
+
action: event.revisionAction,
|
|
78
|
+
supersededFacts: event.supersededFacts,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// orchestration-complete is informational - layer diagram already updated
|
|
83
|
+
// via individual layer events
|
|
84
|
+
},
|
|
85
|
+
[onOrchestrationStart, onLayerUpdate],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const { messages, sendMessage, status } = useChat({
|
|
89
|
+
transport,
|
|
90
|
+
onData: handleDataPart,
|
|
91
|
+
onError: (error) => {
|
|
92
|
+
console.error("Chat error:", error);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Determine if we're actively streaming (only time to show typing indicator)
|
|
97
|
+
const isStreaming = status === "streaming";
|
|
98
|
+
|
|
99
|
+
// Auto-scroll to latest message
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
102
|
+
}, [messages]);
|
|
103
|
+
|
|
104
|
+
// Handle form submission
|
|
105
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
if (!input.trim() || isStreaming) return;
|
|
108
|
+
|
|
109
|
+
const message = input.trim();
|
|
110
|
+
setInput("");
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await sendMessage({ text: message });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("Failed to send message:", error);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleSuggestedMessage = (message: string) => {
|
|
120
|
+
setInput(message);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Extract text content from message parts (AI SDK v5 format)
|
|
124
|
+
const getMessageContent = (message: any): string => {
|
|
125
|
+
if (typeof message.content === "string") {
|
|
126
|
+
return message.content;
|
|
127
|
+
}
|
|
128
|
+
if (message.parts) {
|
|
129
|
+
return message.parts
|
|
130
|
+
.filter((part: any) => part.type === "text")
|
|
131
|
+
.map((part: any) => part.text)
|
|
132
|
+
.join("");
|
|
133
|
+
}
|
|
134
|
+
return "";
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="flex flex-col h-full">
|
|
139
|
+
{/* Messages */}
|
|
140
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
141
|
+
{messages.length === 0 && (
|
|
142
|
+
<div className="text-center py-12">
|
|
143
|
+
<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">
|
|
144
|
+
<span className="text-3xl">🧠</span>
|
|
145
|
+
</div>
|
|
146
|
+
<h2 className="text-xl font-semibold mb-2">
|
|
147
|
+
Welcome to Cortex Memory Demo
|
|
148
|
+
</h2>
|
|
149
|
+
<p className="text-gray-400 max-w-md mx-auto mb-6">
|
|
150
|
+
This demo shows how Cortex orchestrates memory across multiple
|
|
151
|
+
layers in real-time. Try telling me about yourself!
|
|
152
|
+
</p>
|
|
153
|
+
|
|
154
|
+
{/* Suggested messages */}
|
|
155
|
+
<div className="flex flex-wrap justify-center gap-2 max-w-lg mx-auto">
|
|
156
|
+
{suggestedMessages.map((msg, i) => (
|
|
157
|
+
<button
|
|
158
|
+
key={i}
|
|
159
|
+
onClick={() => handleSuggestedMessage(msg)}
|
|
160
|
+
className="px-3 py-2 text-sm bg-white/5 hover:bg-white/10 rounded-lg border border-white/10 transition-colors text-left"
|
|
161
|
+
>
|
|
162
|
+
{msg.length > 40 ? msg.slice(0, 40) + "..." : msg}
|
|
163
|
+
</button>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{messages.map((message, i) => (
|
|
170
|
+
<div
|
|
171
|
+
key={message.id || i}
|
|
172
|
+
className={`message-animate flex ${
|
|
173
|
+
message.role === "user" ? "justify-end" : "justify-start"
|
|
174
|
+
}`}
|
|
175
|
+
>
|
|
176
|
+
<div
|
|
177
|
+
className={`max-w-[80%] px-4 py-3 rounded-2xl ${
|
|
178
|
+
message.role === "user"
|
|
179
|
+
? "bg-cortex-600 text-white"
|
|
180
|
+
: "bg-white/10 text-white"
|
|
181
|
+
}`}
|
|
182
|
+
>
|
|
183
|
+
<p className="whitespace-pre-wrap">
|
|
184
|
+
{getMessageContent(message)}
|
|
185
|
+
</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
))}
|
|
189
|
+
|
|
190
|
+
{/* Typing indicator - only show during active streaming */}
|
|
191
|
+
{isStreaming && (
|
|
192
|
+
<div className="flex justify-start">
|
|
193
|
+
<div className="bg-white/10 px-4 py-3 rounded-2xl">
|
|
194
|
+
<div className="flex gap-1">
|
|
195
|
+
<span className="w-2 h-2 bg-white/50 rounded-full animate-bounce" />
|
|
196
|
+
<span className="w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.1s]" />
|
|
197
|
+
<span className="w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.2s]" />
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
<div ref={messagesEndRef} />
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Input - only disabled during active streaming, NOT during background orchestration */}
|
|
207
|
+
<div className="border-t border-white/10 p-4">
|
|
208
|
+
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
209
|
+
<input
|
|
210
|
+
type="text"
|
|
211
|
+
value={input}
|
|
212
|
+
onChange={(e) => setInput(e.target.value)}
|
|
213
|
+
placeholder="Tell me about yourself..."
|
|
214
|
+
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"
|
|
215
|
+
disabled={isStreaming}
|
|
216
|
+
/>
|
|
217
|
+
<button
|
|
218
|
+
type="submit"
|
|
219
|
+
disabled={isStreaming || !input.trim()}
|
|
220
|
+
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"
|
|
221
|
+
>
|
|
222
|
+
Send
|
|
223
|
+
</button>
|
|
224
|
+
</form>
|
|
225
|
+
|
|
226
|
+
{messages.length > 0 && (
|
|
227
|
+
<button
|
|
228
|
+
onClick={onReset}
|
|
229
|
+
className="mt-2 text-sm text-gray-500 hover:text-white transition-colors"
|
|
230
|
+
>
|
|
231
|
+
Clear visualization →
|
|
232
|
+
</button>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
interface DataPreviewProps {
|
|
4
|
+
data: {
|
|
5
|
+
id?: string;
|
|
6
|
+
preview?: string;
|
|
7
|
+
metadata?: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DataPreview({ data }: DataPreviewProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-2 text-sm">
|
|
14
|
+
{/* ID */}
|
|
15
|
+
{data.id && (
|
|
16
|
+
<div className="flex items-center gap-2">
|
|
17
|
+
<span className="text-gray-500">ID:</span>
|
|
18
|
+
<code className="px-1.5 py-0.5 bg-black/30 rounded text-xs font-mono text-cortex-400">
|
|
19
|
+
{data.id}
|
|
20
|
+
</code>
|
|
21
|
+
</div>
|
|
22
|
+
)}
|
|
23
|
+
|
|
24
|
+
{/* Preview */}
|
|
25
|
+
{data.preview && (
|
|
26
|
+
<div className="mt-2">
|
|
27
|
+
<span className="text-gray-500 text-xs">Data:</span>
|
|
28
|
+
<pre className="mt-1 p-2 bg-black/30 rounded text-xs font-mono text-gray-300 overflow-x-auto whitespace-pre-wrap">
|
|
29
|
+
{data.preview}
|
|
30
|
+
</pre>
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
{/* Metadata */}
|
|
35
|
+
{data.metadata && Object.keys(data.metadata).length > 0 && (
|
|
36
|
+
<div className="mt-2">
|
|
37
|
+
<span className="text-gray-500 text-xs">Metadata:</span>
|
|
38
|
+
<div className="mt-1 flex flex-wrap gap-1.5">
|
|
39
|
+
{Object.entries(data.metadata).map(([key, value]) => (
|
|
40
|
+
<span
|
|
41
|
+
key={key}
|
|
42
|
+
className="px-1.5 py-0.5 bg-white/5 rounded text-xs"
|
|
43
|
+
>
|
|
44
|
+
<span className="text-gray-500">{key}:</span>{" "}
|
|
45
|
+
<span className="text-gray-300">
|
|
46
|
+
{typeof value === "object"
|
|
47
|
+
? JSON.stringify(value)
|
|
48
|
+
: String(value)}
|
|
49
|
+
</span>
|
|
50
|
+
</span>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|