Skip to content

Commit 81be0fd

Browse files
committed
onboarding
1 parent 350aef4 commit 81be0fd

File tree

22 files changed

+597
-89
lines changed

22 files changed

+597
-89
lines changed

CLAUDE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ Core models: `agents`, `tasks`, `messages` (see `convex/schema.ts`)
5252

5353
## Environment Variables
5454

55-
- `NEXT_PUBLIC_CONVEX_URL`: Convex deployment URL (required for web)
55+
- `OPENCLAW_TOKEN`: Authentication token for OpenClaw (required)
56+
- `OPENCLAW_URL`: OpenClaw gateway URL (set in `.env.development` / `.env.production`)
5657
- `NODE_ENV`: local (`development`) vs deployed (`production`) — controls dev tooling
5758
- `ENVIRONMENT`: deployment target (`dev` / `prod`) — controls feature flags
5859

60+
Note: Convex URL is configured at runtime via the `/setup/convex` onboarding page.
61+
5962
## Code Style
6063

6164
- Write clean, readable code that humans can understand
@@ -150,6 +153,6 @@ pnpm test:coverage # Coverage report
150153

151154
## Design
152155

153-
- Accent: `yellow-600` (light), `amber-400` (dark)
156+
- Accent: `pink-600` (light), `pink-400` (dark)
154157
- Clean, minimal, dashboard aesthetic for agent monitoring
155158
- Mobile-first responsive

apps/web/CLAUDE.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ import type { Agent, Task, Message } from "@clawe/shared";
6969

7070
**Environment variables:**
7171

72-
- `NEXT_PUBLIC_CONVEX_URL` → Convex deployment URL
72+
- `OPENCLAW_URL` → OpenClaw gateway URL
73+
- `OPENCLAW_TOKEN` → OpenClaw authentication token (from root `.env`)
74+
75+
Note: Convex URL is configured at runtime via `/setup/convex`.
7376

7477
## Adding Routes
7578

@@ -92,6 +95,7 @@ import type { Agent, Task, Message } from "@clawe/shared";
9295
- **Use hooks for side effects, not components** - never create components that return `null` just to run an effect; use a custom hook instead
9396
- **Convex data** - no manual cache invalidation needed, data syncs automatically
9497
- **React Query cache** - use `invalidateQueries` after mutations for API calls
98+
- **Use React Query + axios** - for API calls, use `useQuery`/`useMutation` with `axios`; place API functions in `lib/api/`
9599
- **Button loading states** - replace icon with `<Spinner />` from `@clawe/ui/components/spinner`, update text (e.g., "Creating..."), and disable
96100
- **Conditional classNames** - always use `cn()` for merging classes: `cn(baseStyles, { "conditional-class": condition })`
97101
- **Verify library APIs are current** - check official docs for deprecated/legacy patterns before implementing
@@ -152,8 +156,8 @@ import type { Agent, Task, Message } from "@clawe/shared";
152156
## Active Nav Styling
153157

154158
```
155-
Light: text-yellow-600, hover bg-yellow-600/5
156-
Dark: text-amber-400, hover bg-amber-400/5
159+
Light: text-pink-600, hover bg-pink-600/5
160+
Dark: text-pink-400, hover bg-pink-400/5
157161
```
158162

159163
## Testing

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@tiptap/starter-kit": "^3.15.3",
3030
"@tiptap/suggestion": "^3.15.3",
3131
"@xyflow/react": "^12.10.0",
32+
"axios": "^1.13.4",
3233
"convex": "^1.21.0",
3334
"framer-motion": "^12.29.0",
3435
"lucide-react": "^0.562.0",
1.48 MB
Loading

apps/web/src/app/(dashboard)/_components/nav-main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { cn } from "@clawe/ui/lib/utils";
1515

1616
export const sidebarMenuButtonActiveStyles =
17-
"font-normal data-[active=true]:bg-transparent data-[active=true]:font-normal data-[active=true]:text-yellow-600 data-[active=true]:hover:bg-yellow-600/5 dark:data-[active=true]:bg-transparent dark:data-[active=true]:text-yellow-400 dark:data-[active=true]:hover:bg-yellow-400/5";
17+
"font-normal data-[active=true]:bg-transparent data-[active=true]:font-normal data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-600/5 dark:data-[active=true]:bg-transparent dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-400/5";
1818

1919
export interface NavItem {
2020
title: string;

apps/web/src/app/api/config/route.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@ import { z } from "zod";
33
import { readConfig, writeConfig } from "@/lib/config/server";
44
import type { ConfigResponse } from "@/lib/config/types";
55

6+
const isDev = process.env.ENVIRONMENT === "dev";
7+
8+
const isValidConvexUrl = (hostname: string): boolean => {
9+
// Allow Convex cloud URLs
10+
if (hostname.endsWith(".convex.cloud")) return true;
11+
// Allow local development URLs only in dev environment
12+
if (isDev && (hostname === "localhost" || hostname === "127.0.0.1"))
13+
return true;
14+
return false;
15+
};
16+
617
const configSchema = z.object({
718
convexUrl: z.string().superRefine((val, ctx) => {
819
try {
920
const url = new URL(val);
10-
if (!url.hostname.endsWith(".convex.cloud")) {
21+
if (!isValidConvexUrl(url.hostname)) {
1122
ctx.addIssue({
1223
code: "custom",
13-
message: "Must be a .convex.cloud URL",
24+
message: isDev
25+
? "Must be a Convex URL (.convex.cloud or localhost)"
26+
: "Must be a .convex.cloud URL",
1427
});
1528
}
1629
} catch {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use client";
2+
3+
import { Moon, Sun } from "lucide-react";
4+
import { useTheme } from "next-themes";
5+
6+
import { Avatar, AvatarFallback } from "@clawe/ui/components/avatar";
7+
import {
8+
DropdownMenu,
9+
DropdownMenuContent,
10+
DropdownMenuItem,
11+
DropdownMenuTrigger,
12+
} from "@clawe/ui/components/dropdown-menu";
13+
14+
export const SetupUserMenu = () => {
15+
const { theme, setTheme } = useTheme();
16+
17+
const toggleTheme = () => {
18+
setTheme(theme === "dark" ? "light" : "dark");
19+
};
20+
21+
return (
22+
<DropdownMenu>
23+
<DropdownMenuTrigger asChild>
24+
<button className="focus-visible:ring-ring rounded-full transition-colors focus:outline-none focus-visible:ring-2">
25+
<Avatar className="h-9 w-9">
26+
<AvatarFallback className="bg-muted text-muted-foreground text-sm font-medium">
27+
U
28+
</AvatarFallback>
29+
</Avatar>
30+
</button>
31+
</DropdownMenuTrigger>
32+
<DropdownMenuContent align="end" sideOffset={8}>
33+
<DropdownMenuItem onClick={toggleTheme}>
34+
<Sun className="dark:hidden" />
35+
<Moon className="hidden dark:block" />
36+
Toggle theme
37+
</DropdownMenuItem>
38+
</DropdownMenuContent>
39+
</DropdownMenu>
40+
);
41+
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useMutation } from "@tanstack/react-query";
5+
import { Button } from "@clawe/ui/components/button";
6+
import { Input } from "@clawe/ui/components/input";
7+
import { Label } from "@clawe/ui/components/label";
8+
import { Progress } from "@clawe/ui/components/progress";
9+
import { Spinner } from "@clawe/ui/components/spinner";
10+
import { saveConfig } from "@/lib/api/config";
11+
12+
const TOTAL_STEPS = 5;
13+
const CURRENT_STEP = 2;
14+
15+
export default function ConvexSetupPage() {
16+
const [convexUrl, setConvexUrl] = useState("");
17+
18+
const mutation = useMutation({
19+
mutationFn: saveConfig,
20+
onSuccess: (data) => {
21+
if (data.ok) {
22+
// Hard redirect to reinitialize ConvexProvider with new URL
23+
window.location.href = "/setup/provider";
24+
}
25+
},
26+
});
27+
28+
const handleSubmit = (e: React.FormEvent) => {
29+
e.preventDefault();
30+
mutation.mutate(convexUrl);
31+
};
32+
33+
const error =
34+
mutation.error?.message ??
35+
(mutation.data && !mutation.data.ok ? mutation.data.error : null);
36+
37+
return (
38+
<div className="flex flex-col">
39+
{/* Progress indicator */}
40+
<div className="mb-12">
41+
<Progress
42+
value={(CURRENT_STEP / TOTAL_STEPS) * 100}
43+
className="h-1 w-64"
44+
indicatorClassName="bg-brand"
45+
/>
46+
</div>
47+
48+
{/* Header */}
49+
<h1 className="mb-3 text-3xl font-semibold tracking-tight">
50+
Connect to Convex
51+
</h1>
52+
<p className="text-muted-foreground mb-8">
53+
Enter your Convex deployment URL to store your data.
54+
</p>
55+
56+
{/* Form */}
57+
<form onSubmit={handleSubmit} className="space-y-6">
58+
<div className="space-y-2">
59+
<Label htmlFor="convex-url">Deployment URL</Label>
60+
<Input
61+
id="convex-url"
62+
type="url"
63+
placeholder="https://your-project.convex.cloud"
64+
value={convexUrl}
65+
onChange={(e) => setConvexUrl(e.target.value)}
66+
required
67+
/>
68+
<p className="text-muted-foreground text-sm">
69+
Find this in your{" "}
70+
<a
71+
href="https://dashboard.convex.dev"
72+
target="_blank"
73+
rel="noopener noreferrer"
74+
className="underline"
75+
>
76+
Convex dashboard
77+
</a>{" "}
78+
under Settings → URL & Deploy Key
79+
</p>
80+
</div>
81+
82+
{error && <p className="text-destructive text-sm">{error}</p>}
83+
84+
<Button
85+
type="submit"
86+
size="lg"
87+
variant="brand"
88+
className="w-full sm:w-auto"
89+
disabled={!convexUrl || mutation.isPending}
90+
>
91+
{mutation.isPending ? (
92+
<>
93+
<Spinner />
94+
Connecting...
95+
</>
96+
) : (
97+
"Continue"
98+
)}
99+
</Button>
100+
</form>
101+
</div>
102+
);
103+
}

apps/web/src/app/setup/layout.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,44 @@
1+
import Image from "next/image";
12
import type { ReactNode } from "react";
23

4+
import { SetupUserMenu } from "./_components/setup-user-menu";
5+
36
export default function SetupLayout({ children }: { children: ReactNode }) {
47
return (
5-
<div className="from-background to-muted flex min-h-svh items-center justify-center bg-gradient-to-b p-4">
6-
<div className="w-full max-w-md">{children}</div>
8+
<div className="relative flex min-h-svh">
9+
{/* User menu - top right (on illustration side) */}
10+
<div className="absolute top-4 right-4 z-10 hidden lg:block">
11+
<SetupUserMenu />
12+
</div>
13+
14+
{/* Left side - Form content */}
15+
<div className="flex w-full flex-col px-6 py-8 lg:w-1/2 lg:px-16 xl:px-24">
16+
{/* Logo */}
17+
<div className="mb-12 flex items-center justify-between">
18+
<span className="text-xl font-semibold">Clawe</span>
19+
{/* User menu on mobile */}
20+
<div className="lg:hidden">
21+
<SetupUserMenu />
22+
</div>
23+
</div>
24+
25+
{/* Content */}
26+
<div className="w-full max-w-md">{children}</div>
27+
</div>
28+
29+
{/* Right side - Illustration (hidden on mobile) */}
30+
<div className="bg-muted relative hidden lg:block lg:w-1/2">
31+
<div className="absolute inset-0 flex items-end justify-center p-12">
32+
<Image
33+
src="/onboarding-hero.png"
34+
alt="Onboarding illustration"
35+
width={450}
36+
height={450}
37+
className="h-auto max-h-full w-auto max-w-full object-contain"
38+
priority
39+
/>
40+
</div>
41+
</div>
742
</div>
843
);
944
}

apps/web/src/app/setup/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { redirect } from "next/navigation";
2+
3+
export default function SetupPage() {
4+
redirect("/setup/welcome");
5+
}

0 commit comments

Comments
 (0)