diff --git a/.env.example b/.env.example index 20ae561..c66b5a5 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -VERSION=0.1.11 +VERSION=0.1.12 diff --git a/apps/webapp/app/components/layout/login-page-layout.tsx b/apps/webapp/app/components/layout/login-page-layout.tsx index 7c87ff4..22a1b36 100644 --- a/apps/webapp/app/components/layout/login-page-layout.tsx +++ b/apps/webapp/app/components/layout/login-page-layout.tsx @@ -1,18 +1,30 @@ import { Button } from "../ui"; import Logo from "../logo/logo"; import { Theme, useTheme } from "remix-themes"; +import { GalleryVerticalEnd } from "lucide-react"; export function LoginPageLayout({ children }: { children: React.ReactNode }) { return ( -
-
-
- +
+
+ - -
C.O.R.E.
-
- {children} +
+
{children}
+
+
+
+ Image
); diff --git a/apps/webapp/app/components/layout/onboarding-layout.tsx b/apps/webapp/app/components/layout/onboarding-layout.tsx new file mode 100644 index 0000000..92dd7eb --- /dev/null +++ b/apps/webapp/app/components/layout/onboarding-layout.tsx @@ -0,0 +1,26 @@ +import { Button } from "../ui"; +import Logo from "../logo/logo"; +import { Theme, useTheme } from "remix-themes"; + +export function LoginPageLayout({ children }: { children: React.ReactNode }) { + return ( +
+
+
+ +
+ +
C.O.R.E.
+
+ {children} +
+
+ ); +} diff --git a/apps/webapp/app/components/sidebar/nav-user.tsx b/apps/webapp/app/components/sidebar/nav-user.tsx index 3898d80..cb162f9 100644 --- a/apps/webapp/app/components/sidebar/nav-user.tsx +++ b/apps/webapp/app/components/sidebar/nav-user.tsx @@ -39,7 +39,7 @@ export function NavUser({ user }: { user: ExtendedUser }) {
- Harshith Mullapudi + {user.displayName} {user.email} diff --git a/apps/webapp/app/models/user.server.ts b/apps/webapp/app/models/user.server.ts index c5fa807..8fe6093 100644 --- a/apps/webapp/app/models/user.server.ts +++ b/apps/webapp/app/models/user.server.ts @@ -200,22 +200,20 @@ export async function getUserByEmail(email: User["email"]) { export function updateUser({ id, - name, - email, marketingEmails, referralSource, -}: Pick & { + onboardingComplete, +}: Pick & { marketingEmails?: boolean; referralSource?: string; }) { return prisma.user.update({ where: { id }, data: { - name, - email, marketingEmails, referralSource, confirmedBasicDetails: true, + onboardingComplete, }, }); } diff --git a/apps/webapp/app/routes/_index.tsx b/apps/webapp/app/routes/_index.tsx index 08c715a..5455396 100644 --- a/apps/webapp/app/routes/_index.tsx +++ b/apps/webapp/app/routes/_index.tsx @@ -2,7 +2,11 @@ import { redirect, type MetaFunction } from "@remix-run/node"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { requireUser } from "~/services/session.server"; -import { confirmBasicDetailsPath, dashboardPath } from "~/utils/pathBuilder"; +import { + confirmBasicDetailsPath, + dashboardPath, + onboardingPath, +} from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { return [ @@ -17,6 +21,8 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { //you have to confirm basic details before you can do anything if (!user.confirmedBasicDetails) { return redirect(confirmBasicDetailsPath()); + } else if (!user.onboardingComplete) { + return redirect(onboardingPath()); } else { return redirect(dashboardPath()); } diff --git a/apps/webapp/app/routes/api.v1.add.tsx b/apps/webapp/app/routes/api.v1.add.tsx index 3c27617..2fcc5b1 100644 --- a/apps/webapp/app/routes/api.v1.add.tsx +++ b/apps/webapp/app/routes/api.v1.add.tsx @@ -1,10 +1,10 @@ import { json } from "@remix-run/node"; -import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; +import { createHybridActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { addToQueue } from "~/lib/ingest.server"; import { IngestBodyRequest } from "~/trigger/ingest/ingest"; -const { action, loader } = createActionApiRoute( +const { action, loader } = createHybridActionApiRoute( { body: IngestBodyRequest, allowJWT: true, diff --git a/apps/webapp/app/routes/api.v1.integration_account.tsx b/apps/webapp/app/routes/api.v1.integration_account.tsx index 529f856..396cc93 100644 --- a/apps/webapp/app/routes/api.v1.integration_account.tsx +++ b/apps/webapp/app/routes/api.v1.integration_account.tsx @@ -56,7 +56,7 @@ const { action, loader } = createHybridActionApiRoute( workspace?.id, ); - if (!setupResult || !setupResult.accountId) { + if (!setupResult.account || !setupResult.account.id) { return json( { error: "Failed to setup integration with the provided API key" }, { status: 400 }, @@ -64,7 +64,7 @@ const { action, loader } = createHybridActionApiRoute( } await tasks.trigger("scheduler", { - integrationAccountId: setupResult?.id, + integrationAccountId: setupResult?.account?.id, }); return json({ success: true, setupResult }); diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index 56815ea..70b567a 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -1,6 +1,10 @@ import { z } from "zod"; import { useActionData } from "@remix-run/react"; -import { type ActionFunctionArgs, json } from "@remix-run/node"; +import { + type ActionFunctionArgs, + json, + type LoaderFunctionArgs, +} from "@remix-run/node"; import { useForm } from "@conform-to/react"; import { getFieldsetConstraint, parse } from "@conform-to/zod"; import { LoginPageLayout } from "~/components/layout/login-page-layout"; @@ -14,10 +18,11 @@ import { import { Button } from "~/components/ui"; import { Input } from "~/components/ui/input"; import { useState } from "react"; -import { requireUserId } from "~/services/session.server"; +import { requireUser, requireUserId } from "~/services/session.server"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { rootPath } from "~/utils/pathBuilder"; import { createWorkspace } from "~/models/workspace.server"; +import { typedjson } from "remix-typedjson"; const schema = z.object({ workspaceName: z @@ -55,6 +60,14 @@ export async function action({ request }: ActionFunctionArgs) { } } +export const loader = async ({ request }: LoaderFunctionArgs) => { + const user = await requireUser(request); + + return typedjson({ + user, + }); +}; + export default function ConfirmBasicDetails() { const lastSubmission = useActionData(); diff --git a/apps/webapp/app/routes/home.integration.$slug.tsx b/apps/webapp/app/routes/home.integration.$slug.tsx index 1e422e4..75aae0b 100644 --- a/apps/webapp/app/routes/home.integration.$slug.tsx +++ b/apps/webapp/app/routes/home.integration.$slug.tsx @@ -164,7 +164,7 @@ export default function IntegrationDetail() { ]} />
-
+
{ const user = await requireUser(request); const workspace = await requireWorkpace(request); - return typedjson( - { - user, - workspace, - }, - { - headers: { - "Set-Cookie": await commitSession(await clearRedirectTo(request)), + //you have to confirm basic details before you can do anything + if (!user.confirmedBasicDetails) { + return redirect(confirmBasicDetailsPath()); + } else if (!user.onboardingComplete) { + return redirect(onboardingPath()); + } else { + return typedjson( + { + user, + workspace, }, - }, - ); + { + headers: { + "Set-Cookie": await commitSession(await clearRedirectTo(request)), + }, + }, + ); + } }; export default function Home() { diff --git a/apps/webapp/app/routes/login._index.tsx b/apps/webapp/app/routes/login._index.tsx index 2d8a300..ac1c992 100644 --- a/apps/webapp/app/routes/login._index.tsx +++ b/apps/webapp/app/routes/login._index.tsx @@ -57,10 +57,12 @@ export default function LoginPage() { return ( - + - Login to your account - Create an account or login + Welcome back + + Create an account or login + @@ -69,7 +71,7 @@ export default function LoginPage() { {data.showGoogleAuth && ( +
+ +
{data.magicLinkError && <>{data.magicLinkError}}
diff --git a/apps/webapp/app/routes/onboarding.tsx b/apps/webapp/app/routes/onboarding.tsx new file mode 100644 index 0000000..ab92979 --- /dev/null +++ b/apps/webapp/app/routes/onboarding.tsx @@ -0,0 +1,230 @@ +import { z } from "zod"; +import { useLoaderData, useActionData, useNavigate } from "@remix-run/react"; +import { + type ActionFunctionArgs, + json, + type LoaderFunctionArgs, + redirect, + createCookie, +} from "@remix-run/node"; +import { useForm } from "@conform-to/react"; +import { getFieldsetConstraint, parse } from "@conform-to/zod"; +import { LoginPageLayout } from "~/components/layout/login-page-layout"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Button } from "~/components/ui"; +import { Textarea } from "~/components/ui/textarea"; +import { Input } from "~/components/ui/input"; +import { useState } from "react"; +import { requireUserId } from "~/services/session.server"; +import { updateUser } from "~/models/user.server"; +import { Copy, Check } from "lucide-react"; +import { addToQueue } from "~/lib/ingest.server"; + +const ONBOARDING_STEP_COOKIE = "onboardingStep"; +const onboardingStepCookie = createCookie(ONBOARDING_STEP_COOKIE, { + path: "/", + httpOnly: true, + sameSite: "lax", + maxAge: 60 * 60 * 24 * 7, // 1 week +}); + +const schema = z.object({ + aboutUser: z + .string() + .min( + 10, + "Please tell us a bit more about yourself (at least 10 characters)", + ) + .max(1000, "Please keep it under 1000 characters"), +}); + +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request); + + // Read step from cookie + const cookieHeader = request.headers.get("Cookie"); + const cookie = (await onboardingStepCookie.parse(cookieHeader)) || {}; + const step = cookie.step || null; + + return json({ step }); +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request); + + const formData = await request.formData(); + const submission = parse(formData, { schema }); + + if (!submission.value || submission.intent !== "submit") { + return json(submission); + } + + const { aboutUser } = submission.value; + + try { + // Ingest memory via API call + const memoryResponse = await addToQueue( + { + source: "Core", + episodeBody: aboutUser, + referenceTime: new Date().toISOString(), + }, + userId, + ); + + if (!memoryResponse.id) { + throw new Error("Failed to save memory"); + } + + // Update user's onboarding status + await updateUser({ + id: userId, + onboardingComplete: true, + }); + + // Set step in cookie and redirect to GET (PRG pattern) + const cookie = await onboardingStepCookie.serialize({ + step: "memory-link", + }); + return redirect("/onboarding", { + headers: { + "Set-Cookie": cookie, + }, + }); + } catch (e: any) { + return json({ errors: { body: e.message } }, { status: 400 }); + } +} + +export default function Onboarding() { + const loaderData = useLoaderData<{ step: string | null }>(); + const lastSubmission = useActionData(); + + const navigate = useNavigate(); + const [copied, setCopied] = useState(false); + + const [form, fields] = useForm({ + lastSubmission: lastSubmission as any, + constraint: getFieldsetConstraint(schema), + onValidate({ formData }) { + return parse(formData, { schema }); + }, + }); + + const memoryUrl = "https://core.heysol.ai/api/v1/mcp/memory"; + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(memoryUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + // Show memory link step after successful submission (step persisted in cookie) + if (loaderData.step === "memory-link") { + return ( + + + + Your Memory Link + + Here's your personal memory API endpoint. Copy this URL to connect + with external tools (Claude, Cursor etc). + + + + +
+
+
+ + +
+
+ + +
+
+
+
+ ); + } + + return ( + + + + + + +
+ + Tell me about you + +
+