@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.
Files changed (58) hide show
  1. package/dist/commands/dev.d.ts.map +1 -1
  2. package/dist/commands/dev.js +121 -10
  3. package/dist/commands/dev.js.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +273 -43
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/setup.d.ts.map +1 -1
  8. package/dist/commands/setup.js +102 -46
  9. package/dist/commands/setup.js.map +1 -1
  10. package/dist/commands/status.d.ts.map +1 -1
  11. package/dist/commands/status.js +94 -7
  12. package/dist/commands/status.js.map +1 -1
  13. package/dist/types.d.ts +23 -0
  14. package/dist/types.d.ts.map +1 -1
  15. package/dist/utils/config.d.ts +11 -0
  16. package/dist/utils/config.d.ts.map +1 -1
  17. package/dist/utils/config.js +20 -0
  18. package/dist/utils/config.js.map +1 -1
  19. package/dist/utils/init/graph-setup.d.ts.map +1 -1
  20. package/dist/utils/init/graph-setup.js +12 -0
  21. package/dist/utils/init/graph-setup.js.map +1 -1
  22. package/dist/utils/init/quickstart-setup.d.ts +87 -0
  23. package/dist/utils/init/quickstart-setup.d.ts.map +1 -0
  24. package/dist/utils/init/quickstart-setup.js +462 -0
  25. package/dist/utils/init/quickstart-setup.js.map +1 -0
  26. package/dist/utils/schema-sync.d.ts.map +1 -1
  27. package/dist/utils/schema-sync.js +27 -21
  28. package/dist/utils/schema-sync.js.map +1 -1
  29. package/package.json +3 -2
  30. package/templates/vercel-ai-quickstart/.env.local.example +45 -0
  31. package/templates/vercel-ai-quickstart/README.md +280 -0
  32. package/templates/vercel-ai-quickstart/app/api/chat/route.ts +196 -0
  33. package/templates/vercel-ai-quickstart/app/api/facts/route.ts +39 -0
  34. package/templates/vercel-ai-quickstart/app/api/health/route.ts +99 -0
  35. package/templates/vercel-ai-quickstart/app/api/memories/route.ts +37 -0
  36. package/templates/vercel-ai-quickstart/app/globals.css +114 -0
  37. package/templates/vercel-ai-quickstart/app/layout.tsx +19 -0
  38. package/templates/vercel-ai-quickstart/app/page.tsx +131 -0
  39. package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +237 -0
  40. package/templates/vercel-ai-quickstart/components/ConvexClientProvider.tsx +21 -0
  41. package/templates/vercel-ai-quickstart/components/DataPreview.tsx +57 -0
  42. package/templates/vercel-ai-quickstart/components/HealthStatus.tsx +214 -0
  43. package/templates/vercel-ai-quickstart/components/LayerCard.tsx +263 -0
  44. package/templates/vercel-ai-quickstart/components/LayerFlowDiagram.tsx +195 -0
  45. package/templates/vercel-ai-quickstart/components/MemorySpaceSwitcher.tsx +93 -0
  46. package/templates/vercel-ai-quickstart/convex/conversations.ts +67 -0
  47. package/templates/vercel-ai-quickstart/convex/facts.ts +131 -0
  48. package/templates/vercel-ai-quickstart/convex/health.ts +15 -0
  49. package/templates/vercel-ai-quickstart/convex/memories.ts +104 -0
  50. package/templates/vercel-ai-quickstart/convex/schema.ts +20 -0
  51. package/templates/vercel-ai-quickstart/convex/users.ts +105 -0
  52. package/templates/vercel-ai-quickstart/lib/animations.ts +146 -0
  53. package/templates/vercel-ai-quickstart/lib/layer-tracking.ts +214 -0
  54. package/templates/vercel-ai-quickstart/next.config.js +7 -0
  55. package/templates/vercel-ai-quickstart/package.json +41 -0
  56. package/templates/vercel-ai-quickstart/postcss.config.js +5 -0
  57. package/templates/vercel-ai-quickstart/tailwind.config.js +37 -0
  58. 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
+ }