mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 09:48:27 +00:00
258 lines
8.1 KiB
TypeScript
258 lines
8.1 KiB
TypeScript
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<typeof action>();
|
|
|
|
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";
|
|
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 (
|
|
<LoginPageLayout>
|
|
<Card className="min-w-[400px] rounded-lg bg-transparent p-3 pt-1">
|
|
<CardHeader className="flex flex-col items-start px-0">
|
|
<CardTitle className="px-0 text-xl">Your Memory Link</CardTitle>
|
|
<CardDescription className="text-md">
|
|
Here's your personal memory API endpoint. Copy this URL to connect
|
|
with external tools (Claude, Cursor etc).
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent className="pt-2 text-base">
|
|
<div className="space-y-4">
|
|
<div className="space-y-3">
|
|
<div className="bg-grayAlpha-100 flex space-x-1 rounded-lg p-1">
|
|
{(["Claude", "Cursor", "Other"] as const).map((source) => (
|
|
<Button
|
|
key={source}
|
|
onClick={() => setSelectedSource(source)}
|
|
variant="ghost"
|
|
className={cn(
|
|
"flex-1 rounded-md px-3 py-1.5 transition-all",
|
|
selectedSource === source
|
|
? "bg-accent text-accent-foreground shadow-sm"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)}
|
|
>
|
|
{source}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="bg-background-3 flex items-center rounded">
|
|
<Input
|
|
type="text"
|
|
id="memoryUrl"
|
|
value={memoryUrl}
|
|
readOnly
|
|
className="bg-background-3 block w-full text-base"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="link"
|
|
size="sm"
|
|
onClick={copyToClipboard}
|
|
className="px-3"
|
|
>
|
|
{copied ? (
|
|
<Check className="h-4 w-4" />
|
|
) : (
|
|
<Copy className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
size="xl"
|
|
className="w-full rounded-lg px-4 py-2"
|
|
onClick={() => navigate("/")}
|
|
>
|
|
Continue to Dashboard
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</LoginPageLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<LoginPageLayout>
|
|
<Card className="bg-background-2 w-full max-w-[400px] rounded-lg p-3 pt-1">
|
|
<CardHeader className="flex flex-col items-start px-0"></CardHeader>
|
|
|
|
<CardContent className="text-base">
|
|
<form method="post" {...form.props}>
|
|
<div className="space-y-4 pl-1">
|
|
<CardTitle className="text-md mb-0 -ml-1 px-0 text-xl">
|
|
Tell me about you
|
|
</CardTitle>
|
|
<div>
|
|
<Textarea
|
|
id="aboutUser"
|
|
placeholder="I'm Steve Jobs, co-founder of Apple. I helped create the iPhone, iPad, and Mac. I'm passionate about design, technology, and making products that change the world. I spent much of my life in California, working on innovative devices and inspiring creativity. I enjoy simplicity, calligraphy, and thinking differently..."
|
|
name={fields.aboutUser.name}
|
|
className="block min-h-[120px] w-full bg-transparent px-0 text-base"
|
|
rows={10}
|
|
/>
|
|
{fields.aboutUser.error && (
|
|
<div className="text-sm text-red-500">
|
|
{fields.aboutUser.error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="submit"
|
|
variant="secondary"
|
|
size="xl"
|
|
className="rounded-lg px-4 py-2"
|
|
>
|
|
Continue
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
</LoginPageLayout>
|
|
);
|
|
}
|