diff --git a/.gitignore b/.gitignore index 7e261396..db635fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,8 @@ systemprompt-example.txt # Ehnhance Prompt example enhance-prompt.txt + +.env.local + +# clerk configuration (can include secrets) +/.clerk/ diff --git a/app/api/webhooks/clerk/route.ts b/app/api/webhooks/clerk/route.ts new file mode 100644 index 00000000..7be08d52 --- /dev/null +++ b/app/api/webhooks/clerk/route.ts @@ -0,0 +1,128 @@ +import { Webhook } from 'svix'; +import { headers } from 'next/headers'; +import { WebhookEvent } from '@clerk/nextjs/server'; +import { createClient } from '@supabase/supabase-js'; +import { NextResponse } from 'next/server'; + +// Initialize Supabase client +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + +// Helper function to create Supabase admin client with service role key +const getSupabaseAdmin = () => { + if (!supabaseUrl || !supabaseServiceKey) { + console.error('Missing Supabase environment variables for admin operations'); + return null; + } + return createClient(supabaseUrl, supabaseServiceKey); +}; + +export async function POST(req: Request) { + // Get the headers + const headerPayload = headers(); + const svix_id = headerPayload.get('svix-id'); + const svix_timestamp = headerPayload.get('svix-timestamp'); + const svix_signature = headerPayload.get('svix-signature'); + + // If there are no svix headers, error out + if (!svix_id || !svix_timestamp || !svix_signature) { + return new Response('Error: Missing svix headers', { + status: 400, + }); + } + + // Get the body + const payload = await req.json(); + const body = JSON.stringify(payload); + + // Create a new Svix instance with your webhook secret + const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET || ''); + + let evt: WebhookEvent; + + // Verify the payload with the headers + try { + evt = wh.verify(body, { + 'svix-id': svix_id, + 'svix-timestamp': svix_timestamp, + 'svix-signature': svix_signature, + }) as WebhookEvent; + } catch (err) { + console.error('Error verifying webhook:', err); + return new Response('Error verifying webhook', { + status: 400, + }); + } + + // Get the ID and type + const eventType = evt.type; + const supabase = getSupabaseAdmin(); + + if (!supabase) { + return NextResponse.json({ error: 'Could not initialize Supabase client' }, { status: 500 }); + } + + // Handle the event + try { + switch (eventType) { + case 'user.created': { + const { id, email_addresses, first_name, last_name, image_url } = evt.data; + const primaryEmail = email_addresses?.[0]?.email_address; + + // Create user in Supabase + const { error } = await supabase.from('users').insert({ + clerk_user_id: id, + email: primaryEmail, + first_name: first_name || null, + last_name: last_name || null, + avatar_url: image_url || null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }); + + if (error) throw error; + break; + } + case 'user.updated': { + const { id, email_addresses, first_name, last_name, image_url } = evt.data; + const primaryEmail = email_addresses?.[0]?.email_address; + + // Update user in Supabase + const { error } = await supabase + .from('users') + .update({ + email: primaryEmail, + first_name: first_name || null, + last_name: last_name || null, + avatar_url: image_url || null, + updated_at: new Date().toISOString(), + }) + .eq('clerk_user_id', id); + + if (error) throw error; + break; + } + case 'user.deleted': { + const { id } = evt.data; + + // Delete user in Supabase + const { error } = await supabase + .from('users') + .delete() + .eq('clerk_user_id', id); + + if (error) throw error; + break; + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error processing webhook:', error); + return NextResponse.json( + { error: 'Error processing webhook' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index 4cff0b05..8724703d 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -5,6 +5,7 @@ import { motion } from "framer-motion" import { useRouter, useParams } from "next/navigation" import { useUser, UserButton, SignedIn } from "@clerk/nextjs" import { useEffect, useState } from "react" +import { cn } from "@/lib/utils" export default function ChatSessionPage() { const router = useRouter() @@ -12,6 +13,7 @@ export default function ChatSessionPage() { const chatId = params.id as string const { user, isLoaded } = useUser() const [isValidSession, setIsValidSession] = useState(true) + const [isChatStarted, setIsChatStarted] = useState(false) useEffect(() => { // Ensure user and chatId are available @@ -31,77 +33,73 @@ export default function ChatSessionPage() { }, [chatId, user, isLoaded, router]) return ( -
- {/* Auth button */} - - - - - - - {/* ZapDev branding */} - - - ZapDev Studio - - +
+ {/* Header elements */} +
+
+ {/* Back button */} + router.push("/")} + className="p-2 rounded-full bg-white/5 hover:bg-white/10 transition-colors flex items-center justify-center" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + aria-label="Back to Home" + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + transition={{ delay: 0.3 }} + > + + + + + + + {/* ZapDev branding */} + + + ZapDev Studio + + +
- {/* Back button */} - - router.push("/")} - className="px-3 py-1 rounded-lg bg-white/5 hover:bg-white/10 transition-colors text-xs flex items-center gap-1" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} + {/* Auth button */} + - - - - - Back to Home - - + + + + +
- {/* Session ID indicator */} - -
- Chat ID: {chatId} - + {/* Main content with conditional layout */} +
+ {/* Left Card / Full Width Card: Chat Interface */} +
+ setIsChatStarted(true)} />
- - {/* Chat interface */} - + {/* Right Card: Desktop Preview (conditionally rendered) */} + {isChatStarted && ( +
+
+

Desktop Preview

+

The UI preview will appear here.

+
+
+ )} +
) } \ No newline at end of file diff --git a/app/chat/page.tsx b/app/chat/page.tsx index f6a55907..9b3bd954 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -10,7 +10,7 @@ export default function ChatPage() { useEffect(() => { // Generate a new unique chat ID and redirect - const chatId = uuidv4().substring(0, 8) + const chatId = uuidv4() router.push(`/chat/${chatId}`) }, [router]) diff --git a/app/example/page.tsx b/app/example/page.tsx new file mode 100644 index 00000000..51fbe108 --- /dev/null +++ b/app/example/page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { Authenticated, Unauthenticated, AuthLoading } from "convex/react"; +import { SignInButton, UserButton } from "@clerk/nextjs"; +import { useQuery } from "convex/react"; +import { api } from "../../convex/_generated/api"; + +export default function ExamplePage() { + return ( +
+

Convex + Clerk Example

+ + +
Loading authentication state...
+
+ + +
+
+ You are signed in: + +
+ +
+
+ + +
+

You are not signed in.

+ + + +
+
+
+ ); +} + +function Content() { + // This will only be called if the user is authenticated + const messages = useQuery(api.messages.list); + + return ( +
+

Your Messages

+ {messages === undefined ? ( +

Loading messages...

+ ) : messages.length === 0 ? ( +

No messages yet.

+ ) : ( +
    + {messages.map((message) => ( +
  • +

    {message.content}

    +

    + {new Date(message.createdAt).toLocaleString()} +

    +
  • + ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index ec0c903c..771e8e45 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,12 +1,9 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' import { ThemeProvider } from '@/components/theme-provider'; -import { - ClerkProvider, - SignedIn, - UserButton, -} from '@clerk/nextjs'; +import { ClerkProvider } from '@clerk/nextjs'; import { PostHogProvider } from '@/components/PostHogProvider' +import ConvexClientProvider from '@/components/ConvexClientProvider'; import './globals.css' export const metadata: Metadata = { @@ -28,22 +25,21 @@ export default function RootLayout({ }>) { return ( - + - - -
- - - -
- {children} -
-
+ + + +
+ {children} +
+
+
+
diff --git a/app/page.tsx b/app/page.tsx index 9d419ffb..39ed676e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/navigation" import { motion } from "framer-motion"; import dynamic from 'next/dynamic'; import Hero from "@/components/hero"; -import { SignedIn, UserButton } from "@clerk/nextjs"; +import { SignedIn, SignedOut, UserButton, SignInButton, SignUpButton } from "@clerk/nextjs"; import FinalCTA from "@/components/final-cta"; const FeaturesShowcase = dynamic(() => import('@/components/features-showcase'), { loading: () =>
}); @@ -38,6 +38,28 @@ export default function Home() {
+ +
+ + + Sign In + + + + + Sign Up + + +
+
{/* Try it now button that navigates to chat */} @@ -47,18 +69,35 @@ export default function Home() { animate={{ opacity: 1, scale: 1 }} transition={{ delay: 1 }} > - router.push("/chat")} - className="px-6 py-3 rounded-full bg-gradient-to-r from-[#6C52A0] to-[#A0527C] hover:from-[#7C62B0] hover:to-[#B0627C] shadow-lg shadow-purple-900/20 flex items-center gap-2" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > - Try ZapDev Now - - - - - + + router.push("/chat")} + className="px-6 py-3 rounded-full bg-gradient-to-r from-[#6C52A0] to-[#A0527C] hover:from-[#7C62B0] hover:to-[#B0627C] shadow-lg shadow-purple-900/20 flex items-center gap-2" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Try ZapDev Now + + + + + + + + + + Start Weaving the Web + + + + + + + {/* Homepage sections */} diff --git a/bun.lock b/bun.lock index 4dd02ff2..b3f31214 100644 --- a/bun.lock +++ b/bun.lock @@ -45,6 +45,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", + "convex": "^1.24.8", "date-fns": "4.1.0", "embla-carousel-react": "8.5.1", "framer-motion": "latest", @@ -61,6 +62,7 @@ "react-resizable-panels": "^2.1.7", "recharts": "2.15.0", "sonner": "^1.7.1", + "svix": "^1.66.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", @@ -135,6 +137,56 @@ "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.2", "", { "os": "android", "cpu": "arm" }, "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.2", "", { "os": "android", "cpu": "x64" }, "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.2", "", { "os": "linux", "cpu": "arm" }, "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.2", "", { "os": "linux", "cpu": "x64" }, "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.2", "", { "os": "none", "cpu": "arm64" }, "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.2", "", { "os": "none", "cpu": "x64" }, "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], @@ -335,6 +387,8 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="], + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], + "@supabase/auth-js": ["@supabase/auth-js@2.69.1", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ=="], "@supabase/functions-js": ["@supabase/functions-js@2.4.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA=="], @@ -455,6 +509,8 @@ "commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "convex": ["convex@1.24.8", "", { "dependencies": { "esbuild": "0.25.2", "jwt-decode": "^4.0.0", "prettier": "3.5.3" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-WNKLXhOboRthS1cAi8EbBpiEsXJF2UD2H6rEd4f6BjISd+/5P+aDMw5xUpYMsbATdxqSmEoIl5QsciWJcXyW+A=="], + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "core-js": ["core-js@3.42.0", "", {}, "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g=="], @@ -539,6 +595,10 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + + "esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.2", "@esbuild/android-arm": "0.25.2", "@esbuild/android-arm64": "0.25.2", "@esbuild/android-x64": "0.25.2", "@esbuild/darwin-arm64": "0.25.2", "@esbuild/darwin-x64": "0.25.2", "@esbuild/freebsd-arm64": "0.25.2", "@esbuild/freebsd-x64": "0.25.2", "@esbuild/linux-arm": "0.25.2", "@esbuild/linux-arm64": "0.25.2", "@esbuild/linux-ia32": "0.25.2", "@esbuild/linux-loong64": "0.25.2", "@esbuild/linux-mips64el": "0.25.2", "@esbuild/linux-ppc64": "0.25.2", "@esbuild/linux-riscv64": "0.25.2", "@esbuild/linux-s390x": "0.25.2", "@esbuild/linux-x64": "0.25.2", "@esbuild/netbsd-arm64": "0.25.2", "@esbuild/netbsd-x64": "0.25.2", "@esbuild/openbsd-arm64": "0.25.2", "@esbuild/openbsd-x64": "0.25.2", "@esbuild/sunos-x64": "0.25.2", "@esbuild/win32-arm64": "0.25.2", "@esbuild/win32-ia32": "0.25.2", "@esbuild/win32-x64": "0.25.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -549,6 +609,8 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], @@ -631,6 +693,8 @@ "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], + "jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -679,6 +743,8 @@ "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -727,10 +793,14 @@ "preact": ["preact@10.26.8", "", {}, "sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ=="], + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], @@ -763,6 +833,8 @@ "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], @@ -815,6 +887,10 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "svix": ["svix@1.66.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "@types/node": "^22.7.5", "es6-promise": "^4.2.8", "fast-sha256": "^1.3.0", "svix-fetch": "^3.0.0", "url-parse": "^1.5.10" } }, "sha512-tGzdhXHdVebNxcflLGxhKUjbNvYv6oRnbFsQ4IpfpUCliZBb7QXMCf32kB1R6dkTX+1h0cbSn9sCL3d4/Bv7wA=="], + + "svix-fetch": ["svix-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-rcADxEFhSqHbraZIsjyZNh4TF6V+koloX1OzZ+AQuObX9mZ2LIMhm1buZeuc5BIZPftZpJCMBsSiBaeszo9tRw=="], + "swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="], "tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="], @@ -849,6 +925,8 @@ "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], @@ -869,6 +947,8 @@ "webpack-bundle-analyzer": ["webpack-bundle-analyzer@4.10.1", "", { "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", "html-escaper": "^2.0.2", "is-plain-object": "^5.0.0", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ=="], + "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], diff --git a/components/ConvexChatProvider.tsx b/components/ConvexChatProvider.tsx new file mode 100644 index 00000000..91bd2757 --- /dev/null +++ b/components/ConvexChatProvider.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { ReactNode, createContext, useContext, useEffect, useState } from "react"; +import { api } from "@/convex/_generated/api"; +import { useConvexUser } from "@/lib/actions"; +import { useMutation, useQuery } from "convex/react"; +import { Id } from "@/convex/_generated/dataModel"; + +// Define the context shape +type ConvexChatContextType = { + userId: Id<"users"> | null; + chatId: Id<"chats"> | null; + messages: any[] | undefined; + sendMessage: (content: string) => Promise; + isLoading: boolean; +}; + +// Create context with default values +const ConvexChatContext = createContext({ + userId: null, + chatId: null, + messages: undefined, + sendMessage: async () => {}, + isLoading: true, +}); + +// Hook to use the chat context +export const useConvexChat = () => useContext(ConvexChatContext); + +// Provider component +export default function ConvexChatProvider({ + children, + chatId, +}: { + children: ReactNode; + chatId?: string; +}) { + const userId = useConvexUser(); + const [currentChatId, setCurrentChatId] = useState | null>(null); + const [isLoading, setIsLoading] = useState(true); + + // Convex mutations and queries + const createChatMutation = useMutation(api.chats.createChat); + const addMessageMutation = useMutation(api.chats.addMessage); + + // Query for messages if we have a chat ID + const messages = useQuery( + api.chats.getMessagesByChatId, + currentChatId ? { chatId: currentChatId } : "skip" + ); + + // Create a new chat or load existing one + useEffect(() => { + const initializeChat = async () => { + if (!userId) return; + + // If we have a chatId from props, validate and use it + if (chatId) { + // TODO: Validate that the chat exists and belongs to this user + // For now, we'll just trust the chatId + setCurrentChatId(chatId as Id<"chats">); + setIsLoading(false); + return; + } + + // Otherwise, create a new chat + try { + const newChatId = await createChatMutation({ + userId, + title: "New Chat", + }); + setCurrentChatId(newChatId); + } catch (error) { + console.error("Failed to create chat:", error); + } finally { + setIsLoading(false); + } + }; + + if (userId) { + initializeChat(); + } + }, [userId, chatId, createChatMutation]); + + // Function to send a message + const sendMessage = async (content: string) => { + if (!currentChatId || !content.trim()) return; + + try { + await addMessageMutation({ + chatId: currentChatId, + content, + role: "user", + }); + + // Here you would typically trigger an AI response + // For demo, let's just add an echo response after a short delay + setTimeout(async () => { + await addMessageMutation({ + chatId: currentChatId, + content: `Echo: ${content}`, + role: "assistant", + }); + }, 1000); + } catch (error) { + console.error("Failed to send message:", error); + } + }; + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/components/ConvexClientProvider.tsx b/components/ConvexClientProvider.tsx new file mode 100644 index 00000000..de8f4552 --- /dev/null +++ b/components/ConvexClientProvider.tsx @@ -0,0 +1,20 @@ +'use client' + +import { ReactNode } from 'react' +import { ConvexReactClient } from 'convex/react' +import { ConvexProviderWithClerk } from 'convex/react-clerk' +import { useAuth } from '@clerk/nextjs' + +if (!process.env.NEXT_PUBLIC_CONVEX_URL) { + throw new Error('Missing NEXT_PUBLIC_CONVEX_URL in your .env file') +} + +const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL) + +export default function ConvexClientProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/components/PostHogProvider.tsx b/components/PostHogProvider.tsx index 7878da01..fa898b6a 100644 --- a/components/PostHogProvider.tsx +++ b/components/PostHogProvider.tsx @@ -7,16 +7,39 @@ import { usePathname, useSearchParams } from "next/navigation" export function PostHogProvider({ children }: { children: React.ReactNode }) { useEffect(() => { - posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { - api_host: "/ingest", - ui_host: "https://us.posthog.com", - capture_pageview: false, // We capture pageviews manually - capture_pageleave: true, // Enable pageleave capture - capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this - debug: process.env.NODE_ENV === "development", - }) + // Only initialize PostHog if the API key is available + if (process.env.NEXT_PUBLIC_POSTHOG_KEY) { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: "/ingest", + ui_host: "https://us.posthog.com", + capture_pageview: false, // We capture pageviews manually + capture_pageleave: true, // Enable pageleave capture + capture_exceptions: true, // This enables capturing exceptions using Error Tracking + debug: process.env.NODE_ENV === "development", + loaded: (posthog) => { + if (process.env.NODE_ENV === 'development') { + // Log when PostHog is loaded in development + console.log('PostHog loaded'); + } + }, + bootstrap: { + distinctID: undefined, + isIdentifiedID: false, + featureFlags: {}, + featureFlagPayloads: {}, + sessionID: undefined + } + }) + } else if (process.env.NODE_ENV === 'development') { + console.warn('PostHog API key not found. Analytics tracking is disabled.'); + } }, []) + // If PostHog key is missing, just render children without PostHog + if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) { + return <>{children} + } + return ( diff --git a/components/animated-ai-chat.tsx b/components/animated-ai-chat.tsx index 558bbf97..3ef8f16a 100644 --- a/components/animated-ai-chat.tsx +++ b/components/animated-ai-chat.tsx @@ -110,9 +110,10 @@ Textarea.displayName = "Textarea" interface AnimatedAIChatProps { chatId?: string; + onFirstMessageSent?: () => void; } -export function AnimatedAIChat({ chatId = "default" }: AnimatedAIChatProps) { +export function AnimatedAIChat({ chatId = "default", onFirstMessageSent }: AnimatedAIChatProps) { const [value, setValue] = useState("") const [attachments, setAttachments] = useState([]) const [isTyping, setIsTyping] = useState(false) @@ -129,6 +130,7 @@ export function AnimatedAIChat({ chatId = "default" }: AnimatedAIChatProps) { const commandPaletteRef = useRef(null) const [messages, setMessages] = useState([]) const [isSplitScreen, setIsSplitScreen] = useState(false) + const fileInputRef = useRef(null) const commandSuggestions: CommandSuggestion[] = [ { @@ -240,6 +242,10 @@ export function AnimatedAIChat({ chatId = "default" }: AnimatedAIChatProps) { setValue(""); adjustHeight(true); + if (messages.length === 0 && onFirstMessageSent) { + onFirstMessageSent(); + } + // Add user message to chat const newUserMessage: Message = { role: "user", @@ -296,8 +302,24 @@ export function AnimatedAIChat({ chatId = "default" }: AnimatedAIChatProps) { } const handleAttachFile = () => { - const mockFileName = `file-${Math.floor(Math.random() * 1000)}.pdf` - setAttachments((prev) => [...prev, mockFileName]) + // Trigger the hidden file input click event + if (fileInputRef.current) { + fileInputRef.current.click() + } + } + + const handleFileChange = (e: React.ChangeEvent) => { + const files = e.target.files + if (files && files.length > 0) { + // Add each selected file to attachments + Array.from(files).forEach(file => { + setAttachments(prev => [...prev, file.name]) + }) + } + // Reset the file input so the same file can be selected again + if (fileInputRef.current) { + fileInputRef.current.value = '' + } } const removeAttachment = (index: number) => { @@ -315,6 +337,15 @@ export function AnimatedAIChat({ chatId = "default" }: AnimatedAIChatProps) { return (
+ {/* Hidden file input */} + +
diff --git a/convex/README.md b/convex/README.md new file mode 100644 index 00000000..4d82e136 --- /dev/null +++ b/convex/README.md @@ -0,0 +1,90 @@ +# Welcome to your Convex functions directory! + +Write your Convex functions here. +See https://docs.convex.dev/functions for more. + +A query function that takes two arguments looks like: + +```ts +// functions.js +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +export const myQueryFunction = query({ + // Validators for arguments. + args: { + first: v.number(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Read the database as many times as you need here. + // See https://docs.convex.dev/database/reading-data. + const documents = await ctx.db.query("tablename").collect(); + + // Arguments passed from the client are properties of the args object. + console.log(args.first, args.second); + + // Write arbitrary JavaScript here: filter, aggregate, build derived data, + // remove non-public properties, or create new objects. + return documents; + }, +}); +``` + +Using this query function in a React component looks like: + +```ts +const data = useQuery(api.functions.myQueryFunction, { + first: 10, + second: "hello", +}); +``` + +A mutation function looks like: + +```ts +// functions.js +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const myMutationFunction = mutation({ + // Validators for arguments. + args: { + first: v.string(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Insert or modify documents in the database here. + // Mutations can also read from the database like queries. + // See https://docs.convex.dev/database/writing-data. + const message = { body: args.first, author: args.second }; + const id = await ctx.db.insert("messages", message); + + // Optionally, return a value from your mutation. + return await ctx.db.get(id); + }, +}); +``` + +Using this mutation function in a React component looks like: + +```ts +const mutation = useMutation(api.functions.myMutationFunction); +function handleButtonPress() { + // fire and forget, the most common way to use mutations + mutation({ first: "Hello!", second: "me" }); + // OR + // use the result once the mutation has completed + mutation({ first: "Hello!", second: "me" }).then((result) => + console.log(result), + ); +} +``` + +Use the Convex CLI to push your functions to a deployment. See everything +the Convex CLI can do by running `npx convex -h` in your project root +directory. To learn more, launch the docs with `npx convex docs`. diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts new file mode 100644 index 00000000..47aa3189 --- /dev/null +++ b/convex/_generated/api.d.ts @@ -0,0 +1,44 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; +import type * as chats from "../chats.js"; +import type * as clerk from "../clerk.js"; +import type * as http from "../http.js"; +import type * as messages from "../messages.js"; +import type * as users from "../users.js"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +declare const fullApi: ApiFromModules<{ + chats: typeof chats; + clerk: typeof clerk; + http: typeof http; + messages: typeof messages; + users: typeof users; +}>; +export declare const api: FilterApi< + typeof fullApi, + FunctionReference +>; +export declare const internal: FilterApi< + typeof fullApi, + FunctionReference +>; diff --git a/convex/_generated/api.js b/convex/_generated/api.js new file mode 100644 index 00000000..3f9c482d --- /dev/null +++ b/convex/_generated/api.js @@ -0,0 +1,22 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { anyApi } from "convex/server"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api = anyApi; +export const internal = anyApi; diff --git a/convex/_generated/dataModel.d.ts b/convex/_generated/dataModel.d.ts new file mode 100644 index 00000000..8541f319 --- /dev/null +++ b/convex/_generated/dataModel.d.ts @@ -0,0 +1,60 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + DataModelFromSchemaDefinition, + DocumentByName, + TableNamesInDataModel, + SystemTableNames, +} from "convex/server"; +import type { GenericId } from "convex/values"; +import schema from "../schema.js"; + +/** + * The names of all of your Convex tables. + */ +export type TableNames = TableNamesInDataModel; + +/** + * The type of a document stored in Convex. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Doc = DocumentByName< + DataModel, + TableName +>; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = DataModelFromSchemaDefinition; diff --git a/convex/_generated/server.d.ts b/convex/_generated/server.d.ts new file mode 100644 index 00000000..7f337a43 --- /dev/null +++ b/convex/_generated/server.d.ts @@ -0,0 +1,142 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const query: QueryBuilder; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const internalQuery: QueryBuilder; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const mutation: MutationBuilder; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const internalMutation: MutationBuilder; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export declare const action: ActionBuilder; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export declare const internalAction: ActionBuilder; + +/** + * Define an HTTP action. + * + * This function will be used to respond to HTTP requests received by a Convex + * deployment if the requests matches the path and method where this action + * is routed. Be sure to route your action in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export declare const httpAction: HttpActionBuilder; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * This differs from the {@link MutationCtx} because all of the services are + * read-only. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; diff --git a/convex/_generated/server.js b/convex/_generated/server.js new file mode 100644 index 00000000..566d4858 --- /dev/null +++ b/convex/_generated/server.js @@ -0,0 +1,89 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, +} from "convex/server"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery = internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation = internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction = internalActionGeneric; + +/** + * Define a Convex HTTP action. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object + * as its second. + * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. + */ +export const httpAction = httpActionGeneric; diff --git a/convex/auth.config.ts b/convex/auth.config.ts new file mode 100644 index 00000000..efd4db48 --- /dev/null +++ b/convex/auth.config.ts @@ -0,0 +1,8 @@ +export default { + providers: [ + { + domain: "https://funky-humpback-59.clerk.accounts.dev", + applicationID: "convex", + }, + ] +}; \ No newline at end of file diff --git a/convex/chats.ts b/convex/chats.ts new file mode 100644 index 00000000..8b550df8 --- /dev/null +++ b/convex/chats.ts @@ -0,0 +1,115 @@ +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; +import { Doc, Id } from "./_generated/dataModel"; + +// Get all chats for a user +export const getChatsByUser = query({ + args: { userId: v.id("users") }, + handler: async (ctx, args) => { + return await ctx.db + .query("chats") + .withIndex("by_user", (q) => q.eq("userId", args.userId)) + .order("desc") + .collect(); + }, +}); + +// Get a specific chat by ID +export const getChatById = query({ + args: { chatId: v.id("chats") }, + handler: async (ctx, args) => { + return await ctx.db.get(args.chatId); + }, +}); + +// Get messages for a chat +export const getMessagesByChatId = query({ + args: { chatId: v.id("chats") }, + handler: async (ctx, args) => { + return await ctx.db + .query("messages") + .withIndex("by_chat", (q) => q.eq("chatId", args.chatId)) + .order("asc") + .collect(); + }, +}); + +// Create a new chat +export const createChat = mutation({ + args: { + userId: v.id("users"), + title: v.string(), + }, + handler: async (ctx, args) => { + const { userId, title } = args; + const now = Date.now(); + + return await ctx.db.insert("chats", { + userId, + title, + createdAt: now, + updatedAt: now, + }); + }, +}); + +// Update a chat +export const updateChat = mutation({ + args: { + chatId: v.id("chats"), + title: v.string(), + }, + handler: async (ctx, args) => { + const { chatId, title } = args; + const now = Date.now(); + + return await ctx.db.patch(chatId, { + title, + updatedAt: now, + }); + }, +}); + +// Delete a chat and its messages +export const deleteChat = mutation({ + args: { + chatId: v.id("chats"), + }, + handler: async (ctx, args) => { + const { chatId } = args; + + // Get all messages for this chat + const messages = await ctx.db + .query("messages") + .withIndex("by_chat", (q) => q.eq("chatId", chatId)) + .collect(); + + // Delete all messages + for (const message of messages) { + await ctx.db.delete(message._id); + } + + // Delete the chat + return await ctx.db.delete(chatId); + }, +}); + +// Add a message to a chat +export const addMessage = mutation({ + args: { + chatId: v.id("chats"), + content: v.string(), + role: v.string(), + }, + handler: async (ctx, args) => { + const { chatId, content, role } = args; + const now = Date.now(); + + return await ctx.db.insert("messages", { + chatId, + content, + role, + createdAt: now, + }); + }, +}); \ No newline at end of file diff --git a/convex/clerk.ts b/convex/clerk.ts new file mode 100644 index 00000000..50b897c1 --- /dev/null +++ b/convex/clerk.ts @@ -0,0 +1,88 @@ +import { v } from "convex/values"; +import { internalMutation } from "./_generated/server"; + +// This internal mutation will be called to sync user data +export const syncUser = internalMutation({ + args: { + clerkId: v.string(), + email: v.optional(v.string()), + firstName: v.optional(v.string()), + lastName: v.optional(v.string()), + avatarUrl: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const { clerkId, email, firstName, lastName, avatarUrl } = args; + + // Check if user already exists + const existingUser = await ctx.db + .query("users") + .withIndex("by_clerk_id", (q) => q.eq("clerkId", clerkId)) + .first(); + + const now = Date.now(); + + if (existingUser) { + // Update existing user + return await ctx.db.patch(existingUser._id, { + email, + firstName, + lastName, + avatarUrl, + updatedAt: now, + }); + } else { + // Create new user + return await ctx.db.insert("users", { + clerkId, + email, + firstName, + lastName, + avatarUrl, + createdAt: now, + updatedAt: now, + }); + } + }, +}); + +// This internal mutation will delete a user +export const deleteUser = internalMutation({ + args: { clerkId: v.string() }, + handler: async (ctx, args) => { + const { clerkId } = args; + + // Find user by clerkId + const user = await ctx.db + .query("users") + .withIndex("by_clerk_id", (q) => q.eq("clerkId", clerkId)) + .first(); + + if (!user) { + return null; + } + + // Find all chats for this user + const chats = await ctx.db + .query("chats") + .withIndex("by_user", (q) => q.eq("userId", user._id)) + .collect(); + + // Delete all messages in each chat + for (const chat of chats) { + const messages = await ctx.db + .query("messages") + .withIndex("by_chat", (q) => q.eq("chatId", chat._id)) + .collect(); + + for (const message of messages) { + await ctx.db.delete(message._id); + } + + // Delete the chat + await ctx.db.delete(chat._id); + } + + // Delete the user + return await ctx.db.delete(user._id); + }, +}); \ No newline at end of file diff --git a/convex/http.ts b/convex/http.ts new file mode 100644 index 00000000..31daabcf --- /dev/null +++ b/convex/http.ts @@ -0,0 +1,19 @@ +import { httpRouter } from "convex/server"; +import { httpAction } from "./_generated/server"; + +const http = httpRouter(); + +// Add HTTP routes here as needed +// Example: health check endpoint +http.route({ + path: "/health", + method: "GET", + handler: httpAction(async () => { + return new Response(JSON.stringify({ status: "ok" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }), +}); + +export default http; \ No newline at end of file diff --git a/convex/messages.ts b/convex/messages.ts new file mode 100644 index 00000000..0effbf83 --- /dev/null +++ b/convex/messages.ts @@ -0,0 +1,47 @@ +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +// List messages for the current authenticated user +export const list = query({ + args: {}, + handler: async (ctx) => { + const identity = await ctx.auth.getUserIdentity(); + if (identity === null) { + throw new Error("Not authenticated"); + } + + // Use the user's tokenIdentifier to filter messages + // This is typically in the format of "clerk:user_id" + const tokenIdentifier = identity.tokenIdentifier; + + return await ctx.db + .query("authMessages") + .filter((q) => q.eq(q.field("author"), tokenIdentifier)) + .order("desc") + .collect(); + }, +}); + +// Create a new message +export const create = mutation({ + args: { + content: v.string(), + }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (identity === null) { + throw new Error("Not authenticated"); + } + + const { content } = args; + const tokenIdentifier = identity.tokenIdentifier; + + return await ctx.db.insert("authMessages", { + content, + author: tokenIdentifier, + authorName: identity.name || "Anonymous", + authorEmail: identity.email || undefined, + createdAt: Date.now(), + }); + }, +}); \ No newline at end of file diff --git a/convex/schema.ts b/convex/schema.ts new file mode 100644 index 00000000..9ba6edd7 --- /dev/null +++ b/convex/schema.ts @@ -0,0 +1,40 @@ +import { v } from "convex/values"; +import { defineSchema, defineTable } from "convex/server"; + +export default defineSchema({ + // Users table to store user information synced from Clerk + users: defineTable({ + clerkId: v.string(), + email: v.optional(v.string()), + firstName: v.optional(v.string()), + lastName: v.optional(v.string()), + avatarUrl: v.optional(v.string()), + createdAt: v.number(), // Unix timestamp + updatedAt: v.number(), // Unix timestamp + }).index("by_clerk_id", ["clerkId"]), + + // Chats table to store chat information + chats: defineTable({ + userId: v.id("users"), // Reference to users table + title: v.string(), + createdAt: v.number(), // Unix timestamp + updatedAt: v.number(), // Unix timestamp + }).index("by_user", ["userId"]), + + // Messages table to store chat messages + messages: defineTable({ + chatId: v.id("chats"), // Reference to chats table + content: v.string(), + role: v.string(), // "user" or "assistant" + createdAt: v.number(), // Unix timestamp + }).index("by_chat", ["chatId"]), + + // Auth example messages table + authMessages: defineTable({ + content: v.string(), + author: v.string(), // Store the tokenIdentifier from auth + authorName: v.optional(v.string()), + authorEmail: v.optional(v.string()), + createdAt: v.number(), // Unix timestamp + }).index("by_author", ["author"]), +}); \ No newline at end of file diff --git a/convex/tsconfig.json b/convex/tsconfig.json new file mode 100644 index 00000000..73741270 --- /dev/null +++ b/convex/tsconfig.json @@ -0,0 +1,25 @@ +{ + /* This TypeScript project config describes the environment that + * Convex functions run in and is used to typecheck them. + * You can modify it, but some settings are required to use Convex. + */ + "compilerOptions": { + /* These settings are not required by Convex and can be modified. */ + "allowJs": true, + "strict": true, + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + + /* These compiler options are required by Convex */ + "target": "ESNext", + "lib": ["ES2021", "dom"], + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "isolatedModules": true, + "noEmit": true + }, + "include": ["./**/*"], + "exclude": ["./_generated"] +} diff --git a/convex/users.ts b/convex/users.ts new file mode 100644 index 00000000..e6183741 --- /dev/null +++ b/convex/users.ts @@ -0,0 +1,65 @@ +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; +import { Doc, Id } from "./_generated/dataModel"; + +// Get user by Clerk ID +export const getUserByClerkId = query({ + args: { clerkId: v.string() }, + handler: async (ctx, args) => { + return await ctx.db + .query("users") + .withIndex("by_clerk_id", (q) => q.eq("clerkId", args.clerkId)) + .first(); + }, +}); + +// Get all users (for admin purposes) +export const getAllUsers = query({ + handler: async (ctx) => { + return await ctx.db.query("users").collect(); + }, +}); + +// Sync a Clerk user with Convex +export const syncClerkUser = mutation({ + args: { + clerkId: v.string(), + email: v.optional(v.string()), + firstName: v.optional(v.string()), + lastName: v.optional(v.string()), + avatarUrl: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const { clerkId, email, firstName, lastName, avatarUrl } = args; + + // Check if user already exists + const existingUser = await ctx.db + .query("users") + .withIndex("by_clerk_id", (q) => q.eq("clerkId", clerkId)) + .first(); + + const now = Date.now(); + + if (existingUser) { + // Update existing user + return await ctx.db.patch(existingUser._id, { + email, + firstName, + lastName, + avatarUrl, + updatedAt: now, + }); + } else { + // Create new user + return await ctx.db.insert("users", { + clerkId, + email, + firstName, + lastName, + avatarUrl, + createdAt: now, + updatedAt: now, + }); + } + }, +}); \ No newline at end of file diff --git a/lib/actions.ts b/lib/actions.ts new file mode 100644 index 00000000..1a24b498 --- /dev/null +++ b/lib/actions.ts @@ -0,0 +1,49 @@ +import { api } from "../convex/_generated/api"; +import { Id } from "../convex/_generated/dataModel"; +import { useUser } from "@clerk/nextjs"; +import { useMutation, useQuery } from "convex/react"; +import { useEffect } from "react"; + +/** + * Hook to sync the current Clerk user with Convex. + * Call this in components where you need the user's Convex ID. + * + * @returns The user's Convex ID or null if not found/authenticated + */ +export function useConvexUser() { + const { user, isSignedIn } = useUser(); + const syncUser = useMutation(api.users.syncClerkUser); + const convexUser = useQuery( + api.users.getUserByClerkId, + isSignedIn ? { clerkId: user?.id || "" } : "skip" + ); + + useEffect(() => { + // If user is signed in and we have their data, sync with Convex + if (isSignedIn && user?.id) { + const primaryEmail = user.emailAddresses[0]?.emailAddress; + + syncUser({ + clerkId: user.id, + email: primaryEmail, + firstName: user.firstName || undefined, + lastName: user.lastName || undefined, + avatarUrl: user.imageUrl || undefined, + }); + } + }, [isSignedIn, user?.id, syncUser, user]); + + return convexUser ? convexUser._id : null; +} + +/** + * Function to get the current user in an API route + */ +export async function getCurrentUser(clerkUserId: string, db: any) { + if (!clerkUserId) return null; + + return await db + .query("users") + .withIndex("by_clerk_id", (q: any) => q.eq("clerkId", clerkUserId)) + .first(); +} \ No newline at end of file diff --git a/lib/convex.ts b/lib/convex.ts new file mode 100644 index 00000000..deb3ef1e --- /dev/null +++ b/lib/convex.ts @@ -0,0 +1,11 @@ +import { ConvexReactClient } from "convex/react"; +import { ConvexProviderWithClerk } from "convex/react-clerk"; +import { ClerkProvider, useAuth } from "@clerk/nextjs"; + +// Create a Convex client +export const convex = new ConvexReactClient( + process.env.NEXT_PUBLIC_CONVEX_URL as string +); + +// Convenience exports +export { ConvexProviderWithClerk, useAuth }; \ No newline at end of file diff --git a/lib/posthog.ts b/lib/posthog.ts index 083c056f..58142708 100644 --- a/lib/posthog.ts +++ b/lib/posthog.ts @@ -5,8 +5,19 @@ import { PostHog } from "posthog-node" * You can call this in server-side code to capture events. */ export default function PostHogClient() { - const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { - host: process.env.NEXT_PUBLIC_POSTHOG_HOST, + // Return null if required environment variables are missing + if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) { + if (process.env.NODE_ENV === 'development') { + console.warn('PostHog API key not found. Server-side analytics tracking is disabled.'); + } + return null; + } + + // Default host if not provided + const host = process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com'; + + const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + host, // Adjust flush settings as needed flushAt: 1, flushInterval: 0, diff --git a/middleware.ts b/middleware.ts index befe7e50..2c548177 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,31 +1,12 @@ -import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; -import { NextResponse } from 'next/server'; +import { clerkMiddleware } from '@clerk/nextjs/server'; -// Define routes that require authentication -const isProtectedRoute = createRouteMatcher([ - '/chat(.*)', // Protect the /chat route and its sub-paths - // Add any other routes you want to protect here -]); - -const publicRoutes = [ - '/', - '/sign-in(.*)', - '/sign-up(.*)', -]; - -export default clerkMiddleware(async (auth, req) => { - // For routes that require authentication, protect them - if (isProtectedRoute(req)) { - await auth.protect(); - } - - // No automatic redirection from home page to chat -}); +// Using basic clerkMiddleware with no custom configuration +export default clerkMiddleware(); export const config = { matcher: [ // Skip Next.js internals and all static files, unless found in search params - '/((?!_next|[^?]*\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', + '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', // Always run for API routes '/(api|trpc)(.*)', ], diff --git a/next.config.mjs b/next.config.mjs index fab1d0e5..ff94392c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -18,14 +18,26 @@ const nextConfig = { source: '/ingest/static/:path*', destination: 'https://us-assets.i.posthog.com/static/:path*', }, - { - source: '/ingest/:path*', - destination: 'https://us.i.posthog.com/:path*', - }, { source: '/ingest/decide', destination: 'https://us.i.posthog.com/decide', }, + { + source: '/ingest/decide/', + destination: 'https://us.i.posthog.com/decide/', + }, + { + source: '/ingest/e', + destination: 'https://us.i.posthog.com/e', + }, + { + source: '/ingest/e/', + destination: 'https://us.i.posthog.com/e/', + }, + { + source: '/ingest/:path*', + destination: 'https://us.i.posthog.com/:path*', + }, ]; }, // This is required to support PostHog trailing slash API requests diff --git a/package.json b/package.json index 4bcb0a55..8f8f5a3f 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", + "convex": "^1.24.8", "date-fns": "4.1.0", "embla-carousel-react": "8.5.1", "framer-motion": "latest", @@ -67,6 +68,7 @@ "react-resizable-panels": "^2.1.7", "recharts": "2.15.0", "sonner": "^1.7.1", + "svix": "^1.66.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0",