diff --git a/apps/dashboard/.env.development b/apps/dashboard/.env.development index 3464e3957..0b8dd416e 100644 --- a/apps/dashboard/.env.development +++ b/apps/dashboard/.env.development @@ -1,5 +1,5 @@ NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02 -NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}26 +NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04 NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09 NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR=false diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx index 2b23ab5fe..b618fc3e9 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx @@ -1,17 +1,17 @@ 'use client'; -import { CodeBlock } from '@/components/code-block'; +import { CodeBlock, codePanelShellClasses } from '@/components/code-block'; import { DesignButton } from "@/components/design-components"; -import { APIEnvKeys, NextJsEnvKeys, ViteEnvKeys } from '@/components/env-keys'; +import { EnvFileViewer } from '@/components/env-keys'; import { InlineCode } from '@/components/inline-code'; -import { StyledLink } from '@/components/link'; -import { CopyPromptButton, Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui"; +import { Tabs, TabsList, TabsTrigger, Typography, cn } from "@/components/ui"; +import { getPublicEnvVar } from '@/lib/env'; import { useThemeWatcher } from '@/lib/theme'; -import { BookIcon, SparkleIcon, XIcon } from "@phosphor-icons/react"; +import { BookIcon, XIcon } from "@phosphor-icons/react"; +import { remindersPrompt } from '@hexclave/shared/dist/ai/unified-prompts/reminders'; import { use } from "@hexclave/shared/dist/utils/react"; import { deindent } from '@hexclave/shared/dist/utils/strings'; import dynamic from "next/dynamic"; -import Image from 'next/image'; import { Suspense, useRef, useState } from "react"; import type { GlobeMethods } from 'react-globe.gl'; import { PageLayout } from "../page-layout"; @@ -22,71 +22,63 @@ import styles from './setup-page.module.css'; const countriesPromise = import('./country-data.geo.json'); const Globe = dynamic(() => import('react-globe.gl').then((mod) => mod.default), { ssr: false }); -const commandClasses = "text-red-600 dark:text-red-400"; -const nameClasses = "text-green-600 dark:text-green-500"; +type SetupMode = "recommended" | "manual"; -const INSTALL_COMMAND_BY_FRAMEWORK = { - nextjs: 'npx @hexclave/cli@latest init', - tanstackStart: 'npm install @hexclave/tanstack-start', - react: 'npm install @hexclave/react', - javascript: 'npm install @hexclave/js', - python: 'pip install requests', -} as const; +const PROD_DOCS_BASE_URL = 'https://docs.hexclave.com'; +const PROD_API_BASE_URL = 'https://api.hexclave.com'; -type SetupFramework = keyof typeof INSTALL_COMMAND_BY_FRAMEWORK; +function getSetupDocsBaseUrl() { + return getPublicEnvVar('NEXT_PUBLIC_STACK_DOCS_BASE_URL') ?? PROD_DOCS_BASE_URL; +} -const TANSTACK_START_SETUP_PROMPT = deindent` - Please set up Hexclave in my TanStack Start app. +function getManualSetupDocsUrl() { + const docsBaseUrl = getSetupDocsBaseUrl().replace(/\/$/, ''); + return `${docsBaseUrl}/guides/getting-started/setup`; +} - 1. Install the alpha TanStack Start package: +function getSetupApiBaseUrl() { + return getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') ?? PROD_API_BASE_URL; +} - npm install @hexclave/tanstack-start +function buildCloudSetupPrompt(options: { + docsBaseUrl: string, + projectId: string, + apiBaseUrl: string, +}) { + const { docsBaseUrl, projectId, apiBaseUrl } = options; + const normalizedDocsBaseUrl = docsBaseUrl.replace(/\/$/, ''); + const reminders = remindersPrompt.replaceAll(PROD_DOCS_BASE_URL, normalizedDocsBaseUrl); - 2. Configure the app with these environment variables: - - VITE_HEXCLAVE_PROJECT_ID= - HEXCLAVE_SECRET_SERVER_KEY= - - 3. Create a HexclaveClientApp using @hexclave/tanstack-start with: - - projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID - - tokenStore: "cookie" - - redirectMethod: "window" - - 4. Wrap the TanStack Start root route with HexclaveProvider and HexclaveTheme. - - 5. Add a /handler/$ route using HexclaveHandler. The handler route must set ssr: false and pass location={pathname} from useLocation(). - - Use only the environment variables listed above. - - After it finishes, verify that the Hexclave MCP server is registered in your AI client config — name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/\`. If it is not registered, please add it manually so you have live access to Hexclave docs and APIs. -`; - -const buildInstallPrompt = (framework: SetupFramework) => { - if (framework === "tanstackStart") { - return TANSTACK_START_SETUP_PROMPT; - } - - const command = INSTALL_COMMAND_BY_FRAMEWORK[framework]; return deindent` - Please run the following command in my project's terminal: + Install and set up Hexclave in this project by following these instructions: - ${command} + Read https://skill.hexclave.com and follow the setup instructions it gives for this project's specific framework and language. - After it finishes, verify that the Hexclave MCP server is registered in your AI client config — name: \`stack-auth\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. The command above should handle this automatically; if for any reason it didn't, please add the MCP server manually so you have live access to Hexclave docs and APIs. -`; -}; + Follow skill.hexclave.com as written, but make sure to use the cloud setup, not the local dashboard setup. + + Do not change the dev script in package.json. In cloud setup, there's no need for that. + + Use these Hexclave project values when creating environment variables: + + - Hexclave API URL: ${apiBaseUrl} + - Hexclave project ID: ${projectId} + + Create the framework-specific public environment variables for the Hexclave API URL and project ID. For example, Next.js uses NEXT_PUBLIC_HEXCLAVE_API_URL and NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, while Vite-based frameworks use VITE_HEXCLAVE_API_URL and VITE_HEXCLAVE_PROJECT_ID. If the Hexclave docs for this framework specify different environment variable names, use the docs' framework-specific names with the values above. + + After setup finishes, verify that the Hexclave MCP server is registered in your AI client config — name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. If it is not registered, add it manually so future agents have live access to Hexclave docs and APIs. + + Once setup is done, tell me to add the Hexclave secret server key from the dashboard to my environment file. After that, setup is complete. + + ${reminders} + `; +} export default function SetupPage(props: { toMetrics: () => void }) { const adminApp = useAdminApp(); - const [selectedFramework, setSelectedFramework] = useState<'nextjs' | 'tanstackStart' | 'react' | 'javascript' | 'python'>('nextjs'); + const [setupMode, setSetupMode] = useState("recommended"); const [keys, setKeys] = useState<{ projectId: string, publishableClientKey?: string, secretServerKey: string } | null>(null); const projectConfig = adminApp.useProject().useConfig(); const requirePublishableClientKey = projectConfig.project.requirePublishableClientKey; - const publishableClientKeyValue = keys?.publishableClientKey ?? "..."; - const optionalPublishableClientKeyProp = (indent: string) => - requirePublishableClientKey ? `\n${indent}publishableClientKey: "${publishableClientKeyValue}",` : ""; - const optionalPublishableClientKeyHeader = (indent: string) => - requirePublishableClientKey ? `\n${indent}'x-hexclave-publishable-client-key': "${publishableClientKeyValue}",` : ""; const onGenerateKeys = async () => { const newKey = await adminApp.createInternalApiKey({ @@ -104,512 +96,12 @@ export default function SetupPage(props: { toMetrics: () => void }) { }); }; - const nextJsSteps = [ - { - step: 2, - title: "Install Hexclave", - content: <> - - In a new or existing Next.js project, install Hexclave as a dependency into your project: - - - pnpx @hexclave/cli@latest init - - } - title="Terminal" - icon="terminal" - /> - - }, - { - step: 3, - title: "Create Keys", - content: <> - - Put these keys in the .env.local file. - - - - }, - { - step: 4, - title: "Done", - content: <> - - If you start your Next.js app with npm run dev and navigate to http://localhost:3000/handler/signup, you will see the sign-up page. - - - }, - ]; - - const reactSteps = [ - { - step: 2, - title: "Install Hexclave", - content: <> - - In a new or existing React project, install Hexclave's dependencies: - - - npm install @hexclave/react - - } - title="Terminal" - icon="terminal" - /> - - }, - { - step: 3, - title: "Create Keys", - content: - }, - { - step: 4, - title: "Create hexclave/client.ts file", - content: <> - - Create a new file called hexclave/client.ts and add the following code. Here we use react-router-dom as an example. - - - - }, - { - step: 5, - title: "Update App.tsx", - content: <> - - Update your App.tsx file to wrap the entire app with a HexclaveProvider and HexclaveTheme and add a HexclaveHandler component to handle the authentication flow. - - - ); - } - - export default function App() { - return ( - - - - - - } /> - hello world} /> - - - - - - ); - } - `} - title="App.tsx" - icon="code" - /> - - }, - { - step: 6, - title: "Done", - content: <> - - If you start your React app with npm run dev and navigate to http://localhost:5173/handler/signup, you will see the sign-up page. - - - } - ]; - - const tanstackStartSteps = [ - { - step: 2, - title: "Install Hexclave", - content: <> - - In a new or existing TanStack Start project, install the alpha Hexclave package: - - - npm install @hexclave/tanstack-start - - } - title="Terminal" - icon="terminal" - /> - - }, - { - step: 3, - title: "Create Keys", - content: <> - - Put these keys in your TanStack Start environment file. - - - - }, - { - step: 4, - title: "Create hexclave/client.ts file", - content: <> - - Create a new file called src/hexclave/client.ts and initialize Hexclave with cookie storage. - - - - }, - { - step: 5, - title: "Update the root route", - content: <> - - Wrap your TanStack Start root route with HexclaveProvider and HexclaveTheme. - - - - - - - ); - } - - function RootDocument({ children }: { children: React.ReactNode }) { - return ( - - - - - - {children} - - - - ); - } - `} - title="src/routes/__root.tsx" - icon="code" - /> - - }, - { - step: 6, - title: "Add the handler route", - content: <> - - Create a splat route for Hexclave's built-in auth pages. - - ; - } - `} - title="src/routes/handler/$.tsx" - icon="code" - /> - - If you start your TanStack Start app and navigate to http://localhost:3000/handler/sign-up, you will see the sign-up page. - - - }, - ]; - - const javascriptSteps = [ - { - step: 2, - title: "Install Hexclave", - content: <> - - Install Hexclave using npm: - - - npm install @hexclave/js - - } - title="Terminal" - icon="terminal" - /> - - }, - { - step: 3, - title: "Create Keys", - content: - }, - { - step: 4, - title: "Initialize the app", - content: <> - - Create a new file for your Hexclave app initialization: - - - - Server - Client - - - - - - - - - - }, - { - step: 5, - title: "Example usage", - content: <> - - - Server - Client - - - - - - - - - - } - ]; - - const pythonSteps = [ - { - step: 2, - title: "Install requests", - content: <> - - Install the requests library to make HTTP requests to the Hexclave API: - - - pip install requests - - } - title="Terminal" - icon="terminal" - /> - - }, - { - step: 3, - title: "Create Keys", - content: - }, - { - step: 4, - title: "Create helper function", - content: <> - - Create a helper function to make requests to the Hexclave API: - - = 400: - raise Exception(f"Hexclave API request failed with {res.status_code}: {res.text}") - return res.json() - `} - title="stack_auth.py" - icon="code" - /> - - }, - { - step: 5, - title: "Make requests", - content: <> - - You can now make requests to the Hexclave API: - - - - } - ]; - + const selectedInstallPrompt = buildCloudSetupPrompt({ + docsBaseUrl: getSetupDocsBaseUrl(), + projectId: adminApp.projectId, + apiBaseUrl: getSetupApiBaseUrl(), + }); + const manualSetupDocsUrl = getManualSetupDocsUrl(); return ( @@ -619,7 +111,7 @@ export default function SetupPage(props: { toMetrics: () => void }) { -
+
@@ -638,7 +130,7 @@ export default function SetupPage(props: { toMetrics: () => void }) { variant='outline' size='sm' onClick={() => { - window.open('https://docs.hexclave.com/', '_blank'); + window.open(getSetupDocsBaseUrl(), '_blank'); }} > @@ -649,91 +141,81 @@ export default function SetupPage(props: { toMetrics: () => void }) {
- - - Copy prompt - + setSetupMode(value === "manual" ? "manual" : "recommended")}> + + Recommended + Manual setup + +
-
-
    - {[ - { - step: 1, - title: "Select your framework", - content:
    -
    - {([{ - id: 'nextjs', - name: 'Next.js', - reverseIfDark: true, - imgSrc: '/next-logo.svg', - }, { - id: 'tanstackStart', - name: 'TanStack Start', - reverseIfDark: false, - imgSrc: '/tanstack-start-logo.png', - }, { - id: 'react', - name: 'React', - reverseIfDark: false, - imgSrc: '/react-logo.svg', - }, { - id: 'javascript', - name: 'JavaScript', - reverseIfDark: false, - imgSrc: '/javascript-logo.svg', - }, { - id: 'python', - name: 'Python', - reverseIfDark: false, - imgSrc: '/python-logo.svg', - }] as const).map(({ name, imgSrc: src, reverseIfDark, id }) => ( - setSelectedFramework(id)} - > - {name} - {name} - - ))} + {setupMode === "recommended" ? ( +
    +
      + {[ + { + step: 1, + title: "Copy Setup Prompt", + content:
      + + {selectedInstallPrompt} + + } + title="Prompt for your AI agent" + icon="code" + maxHeight={480} + /> +
      , + }, + { + step: 2, + title: "Create Keys", + content: <> + + Add this server-only key to your project's .env.local file. + + + , + }, + { + step: 3, + title: "Done", + content: , + }, + ].map((item) => ( +
    1. +
      + + {item.step} + +

      {item.title}

      -
    , - }, - ...(selectedFramework === 'nextjs' ? nextJsSteps : []), - ...(selectedFramework === 'tanstackStart' ? tanstackStartSteps : []), - ...(selectedFramework === 'react' ? reactSteps : []), - ...(selectedFramework === 'javascript' ? javascriptSteps : []), - ...(selectedFramework === 'python' ? pythonSteps : []), - ].map((item, index) => ( -
  1. -
    - - {item.step} - -

    {item.title}

    -
    -
    - {item.content} -
    -
  2. - ))} -
-
+
+ {item.content} +
+ + ))} + +
+ ) : ( +
+ + Manual setup steps live in the documentation so they stay up to date with every framework and SDK change. + + { + window.open(manualSetupDocsUrl, '_blank'); + }} + > + + Open manual setup docs + +
+ )} ); } @@ -813,45 +295,44 @@ function GlobeIllustrationInner() { ); } +function SetupRecommendedDoneStep(props: { onExploreDashboard: () => void }) { + return ( +
+ + Hooray! Setup completed. + +
+ + Explore Dashboard + +
+
+ ); +} + function HexclaveKeys(props: { keys: { projectId: string, publishableClientKey?: string, secretServerKey: string } | null, onGenerateKeys: () => Promise, - type: 'next' | 'vite' | 'raw', }) { - return ( -
- {props.keys ? ( - <> - {props.type === 'next' ? ( - - ) : props.type === 'vite' ? ( - - ) : ( - - )} - - - {`Save these keys securely - they won't be shown again after leaving this page.`} - - - ) : ( + if (!props.keys) { + return ( +
Generate Keys
- )} +
+ ); + } + + return ( +
+ + + + {`Save these keys securely - they won't be shown again after leaving this page.`} +
); } diff --git a/apps/dashboard/src/components/code-block.tsx b/apps/dashboard/src/components/code-block.tsx index 8cff7dea4..ccf012bad 100644 --- a/apps/dashboard/src/components/code-block.tsx +++ b/apps/dashboard/src/components/code-block.tsx @@ -17,6 +17,10 @@ Object.entries({ tsx, bash, typescript, python, sql }).forEach(([key, value]) => SyntaxHighlighter.registerLanguage(key, value); }); +export const codePanelShellClasses = "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]"; + +export const codePanelHeaderClasses = "text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center bg-black/[0.015] dark:bg-white/[0.015] py-2.5 border-b border-black/[0.06] dark:border-white/[0.06]"; + type CodeBlockProps = { language: string, content: string, @@ -32,7 +36,7 @@ type CodeBlockProps = { }; export function CodeBlock(props: CodeBlockProps) { - const { theme, mounted } = useThemeWatcher(); + const { theme } = useThemeWatcher(); let icon = null; switch (props.icon) { @@ -47,8 +51,20 @@ export function CodeBlock(props: CodeBlockProps) { } return ( -
-
+
+
{icon} {props.title} diff --git a/apps/dashboard/src/components/env-keys.tsx b/apps/dashboard/src/components/env-keys.tsx index 2a2c3931f..37b0b34e4 100644 --- a/apps/dashboard/src/components/env-keys.tsx +++ b/apps/dashboard/src/components/env-keys.tsx @@ -1,7 +1,86 @@ -import { getPublicEnvVar } from '@/lib/env'; -import { Button, CopyField, Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui"; +"use client"; -function getEnvFileContent(props: { +import { codePanelHeaderClasses, codePanelShellClasses } from '@/components/code-block'; +import { getPublicEnvVar } from '@/lib/env'; +import { Button, CopyButton, CopyField, Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui"; +import React, { useState } from "react"; +import { EyeIcon, EyeSlashIcon, FileTextIcon } from "@phosphor-icons/react"; +import { cn } from "@/lib/utils"; + +type EnvFileViewerProps = { + filename: string; + value: string; +} + +export function EnvFileViewer({ filename, value }: EnvFileViewerProps) { + const [revealAll, setRevealAll] = useState(false); + + const lines = value.split("\n").map((line, idx) => { + const eqIndex = line.indexOf("="); + if (eqIndex === -1) return { key: `comment_${idx}`, val: line, isComment: true }; + const key = line.substring(0, eqIndex); + const val = line.substring(eqIndex + 1); + return { key, val, isComment: false }; + }); + + return ( +
+
+
+ + {filename} +
+
+ + +
+
+ +
+ + + {lines.map((line, idx) => { + return ( + + + + ); + })} + +
+
+
+ {line.isComment ? ( + {line.val} + ) : ( + <> + {line.key} + = + {revealAll ? ( + {line.val} + ) : ( + •••••••••••••••••••• + )} + + )} +
+
+
+
+
+ ); +} + +export function getEnvFileContent(props: { projectId: string, publishableClientKey?: string, secretServerKey?: string, @@ -91,6 +170,7 @@ export function APIEnvKeys(props: { + ); } @@ -142,12 +217,6 @@ export function ViteEnvKeys(props: { .join("\n"); return ( - + ); } diff --git a/apps/dashboard/src/components/ui/copy-field.tsx b/apps/dashboard/src/components/ui/copy-field.tsx index a8945be55..a3458c14b 100644 --- a/apps/dashboard/src/components/ui/copy-field.tsx +++ b/apps/dashboard/src/components/ui/copy-field.tsx @@ -1,8 +1,12 @@ +"use client"; + +import React, { useState } from "react"; import { Input } from "./input"; import { Label } from "./label"; import { SimpleTooltip } from "./simple-tooltip"; import { Textarea } from "./textarea"; import { CopyButton } from "./copy-button"; +import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react"; export function CopyField(props: { value: string, @@ -11,12 +15,15 @@ export function CopyField(props: { monospace?: boolean, fixedSize?: boolean, initialCopied?: boolean, + isSecret?: boolean, } & ({ type: "textarea", height?: number, } | { type: "input", })) { + const [isRevealed, setIsRevealed] = useState(false); + return (
{props.label && ( @@ -43,12 +50,24 @@ export function CopyField(props: {
- + {props.isSecret && ( + + )} +
)}
diff --git a/apps/dashboard/src/components/ui/textarea.tsx b/apps/dashboard/src/components/ui/textarea.tsx index 60af5cd78..867b81262 100644 --- a/apps/dashboard/src/components/ui/textarea.tsx +++ b/apps/dashboard/src/components/ui/textarea.tsx @@ -10,7 +10,7 @@ const Textarea = forwardRefIfNeeded( return (