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"; import { cn } from "~/lib/utils"; 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 [selectedSource, setSelectedSource] = useState< "Claude" | "Cursor" | "Other" >("Claude"); const [form, fields] = useForm({ lastSubmission: lastSubmission as any, constraint: getFieldsetConstraint(schema), onValidate({ formData }) { return parse(formData, { schema }); }, }); const getMemoryUrl = (source: "Claude" | "Cursor" | "Other") => { const baseUrl = "https://core.heysol.ai/api/v1/mcp/memory"; return `${baseUrl}?Source=${source}`; }; const memoryUrl = getMemoryUrl(selectedSource); 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).
{(["Claude", "Cursor", "Other"] as const).map((source) => ( ))}
); } return (
Tell me about you