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 fb84e00c7..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, codePanelShellClasses } from '@/components/env-keys'; +import { EnvFileViewer } from '@/components/env-keys'; import { InlineCode } from '@/components/inline-code'; -import { StyledLink } from '@/components/link'; -import { 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, 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,38 +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 SetupFramework = 'nextjs' | 'tanstackStart' | 'react' | 'javascript' | 'python'; type SetupMode = "recommended" | "manual"; -const SETUP_PROMPT = deindent` - Install and set up Hexclave in this project by following these instructions: +const PROD_DOCS_BASE_URL = 'https://docs.hexclave.com'; +const PROD_API_BASE_URL = 'https://api.hexclave.com'; - Read https://skill.hexclave.com and follow the setup instructions it gives for this project's specific framework and language. +function getSetupDocsBaseUrl() { + return getPublicEnvVar('NEXT_PUBLIC_STACK_DOCS_BASE_URL') ?? PROD_DOCS_BASE_URL; +} - Follow skill.hexclave.com as written, but make sure to use the cloud setup, not the local dashboard setup. +function getManualSetupDocsUrl() { + const docsBaseUrl = getSetupDocsBaseUrl().replace(/\/$/, ''); + return `${docsBaseUrl}/guides/getting-started/setup`; +} - Do not change the dev script in package.json, even if the skill mentions doing that. +function getSetupApiBaseUrl() { + return getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') ?? PROD_API_BASE_URL; +} - 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. +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); - Once setup is done, tell me to add the Hexclave environment variables to .env.local. After that, setup is complete. -`; + return deindent` + Install and set up Hexclave in this project by following these instructions: + + Read https://skill.hexclave.com and follow the setup instructions it gives for this project's specific framework and language. + + 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'); 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({ @@ -71,513 +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 selectedKeyType = selectedFramework === 'nextjs' ? 'next' : selectedFramework === 'tanstackStart' ? 'vite' : 'raw'; + const selectedInstallPrompt = buildCloudSetupPrompt({ + docsBaseUrl: getSetupDocsBaseUrl(), + projectId: adminApp.projectId, + apiBaseUrl: getSetupApiBaseUrl(), + }); + const manualSetupDocsUrl = getManualSetupDocsUrl(); return ( @@ -606,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'); }} > @@ -617,13 +141,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
- { - if (value === "manual" || value === "recommended") { - setSetupMode(value); - return; - } - throw new Error(`Unexpected setup mode: ${value}`); - }}> + setSetupMode(value === "manual" ? "manual" : "recommended")}> Recommended Manual setup @@ -631,122 +149,73 @@ export default function SetupPage(props: { toMetrics: () => void }) {
-
-
    - {(setupMode === "recommended" ? [ - { - step: 1, - title: "Copy Setup Prompt", - content:
    - -
    , - }, - { - step: 2, - title: "Create Keys", - content: <> - - Add these to your project's .env.local file. - - - , - }, - { - step: 3, - title: "Done", - content: <> - - After starting your dev server, navigate to http://localhost:3000/handler/signup, you will see the sign-up page. - - , - }, - ] : [ - { - 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) => ( -
  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 + +
+ )}
); } @@ -826,10 +295,24 @@ 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', }) { if (!props.keys) { return ( @@ -845,24 +328,7 @@ function HexclaveKeys(props: { return (
- {props.type === 'next' ? ( - - ) : props.type === 'vite' ? ( - - ) : ( - - )} + {`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 950b6ed02..ccf012bad 100644 --- a/apps/dashboard/src/components/code-block.tsx +++ b/apps/dashboard/src/components/code-block.tsx @@ -4,86 +4,22 @@ import { CopyButton, SimpleTooltip } from "@/components/ui"; import { useThemeWatcher } from '@/lib/theme'; import { cn } from '@/lib/utils'; import { CodeIcon, TerminalWindowIcon } from "@phosphor-icons/react"; -import type { CSSProperties, ReactNode } from 'react'; +import type { ReactNode } from 'react'; import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'; import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash'; import python from 'react-syntax-highlighter/dist/esm/languages/prism/python'; import sql from 'react-syntax-highlighter/dist/esm/languages/prism/sql'; import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx'; import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript'; +import { dark, prism } from 'react-syntax-highlighter/dist/esm/styles/prism'; Object.entries({ tsx, bash, typescript, python, sql }).forEach(([key, value]) => { SyntaxHighlighter.registerLanguage(key, value); }); -const codeThemeBase: Record = { - 'code[class*="language-"]': { - background: 'transparent', - fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace', - textShadow: 'none', - }, - 'pre[class*="language-"]': { - background: 'transparent', - fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace', - textShadow: 'none', - }, -}; +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]"; -const lightCodeTheme: Record = { - ...codeThemeBase, - 'code[class*="language-"]': { - ...codeThemeBase['code[class*="language-"]'], - color: '#334155', - }, - 'pre[class*="language-"]': { - ...codeThemeBase['pre[class*="language-"]'], - color: '#334155', - }, - comment: { color: '#94a3b8', fontStyle: 'italic' }, - punctuation: { color: '#475569' }, - property: { color: '#075985' }, - tag: { color: '#1d4ed8' }, - boolean: { color: '#b45309', fontWeight: 600 }, - number: { color: '#b45309', fontWeight: 600 }, - constant: { color: '#6d28d9', fontWeight: 600 }, - selector: { color: '#047857' }, - string: { color: '#047857' }, - builtin: { color: '#0e7490', fontWeight: 600 }, - operator: { color: '#0f766e', fontWeight: 600 }, - variable: { color: '#334155' }, - atrule: { color: '#6d28d9', fontWeight: 600 }, - function: { color: '#1d4ed8', fontWeight: 600 }, - 'class-name': { color: '#a16207', fontWeight: 600 }, - keyword: { color: '#6d28d9', fontWeight: 600 }, -}; - -const darkCodeTheme: Record = { - ...codeThemeBase, - 'code[class*="language-"]': { - ...codeThemeBase['code[class*="language-"]'], - color: '#eef6ff', - }, - 'pre[class*="language-"]': { - ...codeThemeBase['pre[class*="language-"]'], - color: '#eef6ff', - }, - comment: { color: '#7c8798', fontStyle: 'italic' }, - punctuation: { color: '#c6d3e1' }, - property: { color: '#38bdf8' }, - tag: { color: '#60a5fa' }, - boolean: { color: '#fbbf24', fontWeight: 600 }, - number: { color: '#fbbf24', fontWeight: 600 }, - constant: { color: '#d8b4fe', fontWeight: 600 }, - selector: { color: '#4ade80' }, - string: { color: '#4ade80' }, - builtin: { color: '#22d3ee', fontWeight: 600 }, - operator: { color: '#22d3ee', fontWeight: 600 }, - variable: { color: '#eef6ff' }, - atrule: { color: '#d8b4fe', fontWeight: 600 }, - function: { color: '#60a5fa', fontWeight: 600 }, - 'class-name': { color: '#fde047', fontWeight: 600 }, - keyword: { color: '#d8b4fe', fontWeight: 600 }, -}; +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, @@ -116,11 +52,11 @@ export function CodeBlock(props: CodeBlockProps) { return (
{props.customRender ??