mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
Add .env.development configuration for Hexclave API and update setup-page with new documentation links and environment variable handling. Introduce codePanelShellClasses for consistent styling in code blocks.
This commit is contained in:
parent
f6fcead1c5
commit
a8732b0c4a
@ -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
|
||||
|
||||
|
||||
@ -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<SetupFramework>('nextjs');
|
||||
const [setupMode, setSetupMode] = useState<SetupMode>("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: <>
|
||||
<Typography>
|
||||
In a new or existing Next.js project, install Hexclave as a dependency into your project:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npx @hexclave/cli@latest init`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>pnpx</span> <span className={nameClasses}>@hexclave/cli@latest</span> init
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <>
|
||||
<Typography>
|
||||
Put these keys in the <InlineCode>.env.local</InlineCode> file.
|
||||
</Typography>
|
||||
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="next" />
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Done",
|
||||
content: <>
|
||||
<Typography>
|
||||
If you start your Next.js app with npm run dev and navigate to <StyledLink href="http://localhost:3000/handler/signup">http://localhost:3000/handler/signup</StyledLink>, you will see the sign-up page.
|
||||
</Typography>
|
||||
</>
|
||||
},
|
||||
];
|
||||
|
||||
const reactSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install Hexclave",
|
||||
content: <>
|
||||
<Typography>
|
||||
In a new or existing React project, install Hexclave's dependencies:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npm install @hexclave/react`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/react</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Create hexclave/client.ts file",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a new file called <InlineCode>hexclave/client.ts</InlineCode> and add the following code. Here we use react-router-dom as an example.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
content={deindent`
|
||||
import { HexclaveClientApp } from "@hexclave/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const hexclaveClientApp = new HexclaveClientApp({
|
||||
// You should store these in environment variables
|
||||
projectId: "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyProp(" ")}
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: {
|
||||
useNavigate,
|
||||
}
|
||||
});
|
||||
`}
|
||||
title="hexclave/client.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: "Update App.tsx",
|
||||
content: <>
|
||||
<Typography>
|
||||
Update your App.tsx file to wrap the entire app with a <InlineCode>HexclaveProvider</InlineCode> and <InlineCode>HexclaveTheme</InlineCode> and add a <InlineCode>HexclaveHandler</InlineCode> component to handle the authentication flow.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
maxHeight={300}
|
||||
content={deindent`
|
||||
import { HexclaveHandler, HexclaveProvider, HexclaveTheme } from "@hexclave/react";
|
||||
import { Suspense } from "react";
|
||||
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { hexclaveClientApp } from "./hexclave/client";
|
||||
|
||||
function HandlerRoutes() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<HexclaveHandler app={hexclaveClientApp} location={location.pathname} fullPage />
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={"Loading..."}>
|
||||
<BrowserRouter>
|
||||
<HexclaveProvider app={hexclaveClientApp}>
|
||||
<HexclaveTheme>
|
||||
<Routes>
|
||||
<Route path="/handler/*" element={<HandlerRoutes />} />
|
||||
<Route path="/" element={<div>hello world</div>} />
|
||||
</Routes>
|
||||
</HexclaveTheme>
|
||||
</HexclaveProvider>
|
||||
</BrowserRouter>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
`}
|
||||
title="App.tsx"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
title: "Done",
|
||||
content: <>
|
||||
<Typography>
|
||||
If you start your React app with npm run dev and navigate to <StyledLink href="http://localhost:5173/handler/signup">http://localhost:5173/handler/signup</StyledLink>, you will see the sign-up page.
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
];
|
||||
|
||||
const tanstackStartSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install Hexclave",
|
||||
content: <>
|
||||
<Typography>
|
||||
In a new or existing TanStack Start project, install the alpha Hexclave package:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npm install @hexclave/tanstack-start`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/tanstack-start</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <>
|
||||
<Typography>
|
||||
Put these keys in your TanStack Start environment file.
|
||||
</Typography>
|
||||
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="vite" />
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Create hexclave/client.ts file",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a new file called <InlineCode>src/hexclave/client.ts</InlineCode> and initialize Hexclave with cookie storage.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
content={deindent`
|
||||
import { HexclaveClientApp } from "@hexclave/tanstack-start";
|
||||
|
||||
export const hexclaveClientApp = new HexclaveClientApp({
|
||||
projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID,
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: "window",
|
||||
});
|
||||
`}
|
||||
title="src/hexclave/client.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: "Update the root route",
|
||||
content: <>
|
||||
<Typography>
|
||||
Wrap your TanStack Start root route with <InlineCode>HexclaveProvider</InlineCode> and <InlineCode>HexclaveTheme</InlineCode>.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
maxHeight={300}
|
||||
content={deindent`
|
||||
import { HexclaveProvider, HexclaveTheme } from "@hexclave/tanstack-start";
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
|
||||
import { hexclaveClientApp } from "../hexclave/client";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
shellComponent: RootDocument,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<HexclaveProvider app={hexclaveClientApp}>
|
||||
<HexclaveTheme>
|
||||
<Outlet />
|
||||
</HexclaveTheme>
|
||||
</HexclaveProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
`}
|
||||
title="src/routes/__root.tsx"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
title: "Add the handler route",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a splat route for Hexclave's built-in auth pages.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
content={deindent`
|
||||
import { HexclaveHandler } from "@hexclave/tanstack-start";
|
||||
import { createFileRoute, useLocation } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/handler/$")({
|
||||
ssr: false,
|
||||
component: HandlerPage,
|
||||
});
|
||||
|
||||
function HandlerPage() {
|
||||
const { pathname } = useLocation();
|
||||
return <HexclaveHandler fullPage location={pathname} />;
|
||||
}
|
||||
`}
|
||||
title="src/routes/handler/$.tsx"
|
||||
icon="code"
|
||||
/>
|
||||
<Typography>
|
||||
If you start your TanStack Start app and navigate to <StyledLink href="http://localhost:3000/handler/sign-up">http://localhost:3000/handler/sign-up</StyledLink>, you will see the sign-up page.
|
||||
</Typography>
|
||||
</>
|
||||
},
|
||||
];
|
||||
|
||||
const javascriptSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install Hexclave",
|
||||
content: <>
|
||||
<Typography>
|
||||
Install Hexclave using npm:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npm install @hexclave/js`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/js</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Initialize the app",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a new file for your Hexclave app initialization:
|
||||
</Typography>
|
||||
<Tabs defaultValue="server">
|
||||
<TabsList>
|
||||
<TabsTrigger value="server">Server</TabsTrigger>
|
||||
<TabsTrigger value="client">Client</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="server">
|
||||
<CodeBlock
|
||||
language="typescript"
|
||||
content={deindent`
|
||||
import { HexclaveServerApp } from "@hexclave/js";
|
||||
|
||||
const hexclaveServerApp = new HexclaveServerApp({
|
||||
// You should store these in environment variables based on your project setup
|
||||
projectId: "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyProp(" ")}
|
||||
secretServerKey: "${keys?.secretServerKey ?? "..."}",
|
||||
tokenStore: "memory",
|
||||
});
|
||||
`}
|
||||
title="hexclave/server.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="client">
|
||||
<CodeBlock
|
||||
language="typescript"
|
||||
content={deindent`
|
||||
import { HexclaveClientApp } from "@hexclave/js";
|
||||
|
||||
const hexclaveClientApp = new HexclaveClientApp({
|
||||
// You should store these in environment variables
|
||||
projectId: "your-project-id",${optionalPublishableClientKeyProp(" ")}
|
||||
tokenStore: "cookie",
|
||||
});
|
||||
`}
|
||||
title="hexclave/client.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: "Example usage",
|
||||
content: <>
|
||||
<Tabs defaultValue="server">
|
||||
<TabsList>
|
||||
<TabsTrigger value="server">Server</TabsTrigger>
|
||||
<TabsTrigger value="client">Client</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="server">
|
||||
<CodeBlock
|
||||
language="typescript"
|
||||
content={deindent`
|
||||
import { hexclaveServerApp } from "@/hexclave/server";
|
||||
|
||||
const user = await hexclaveServerApp.getUser("user_id");
|
||||
|
||||
await user.update({
|
||||
displayName: "New Display Name",
|
||||
});
|
||||
|
||||
const team = await hexclaveServerApp.createTeam({
|
||||
name: "New Team",
|
||||
});
|
||||
|
||||
await team.addUser(user.id);
|
||||
`}
|
||||
title="Example server usage"
|
||||
icon="code"
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="client">
|
||||
<CodeBlock
|
||||
language="typescript"
|
||||
content={deindent`
|
||||
import { hexclaveClientApp } from "@/hexclave/client";
|
||||
|
||||
await hexclaveClientApp.signInWithCredential({
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const user = await hexclaveClientApp.getUser();
|
||||
|
||||
await user.update({
|
||||
displayName: "New Display Name",
|
||||
});
|
||||
|
||||
await user.signOut();
|
||||
`}
|
||||
title="Example client usage"
|
||||
icon="code"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
}
|
||||
];
|
||||
|
||||
const pythonSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install requests",
|
||||
content: <>
|
||||
<Typography>
|
||||
Install the requests library to make HTTP requests to the Hexclave API:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`pip install requests`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>pip install</span> <span className={nameClasses}>requests</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Create helper function",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a helper function to make requests to the Hexclave API:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="python"
|
||||
content={deindent`
|
||||
import requests
|
||||
|
||||
def stack_auth_request(method, endpoint, **kwargs):
|
||||
res = requests.request(
|
||||
method,
|
||||
f'https://api.hexclave.com/{endpoint}',
|
||||
headers={
|
||||
'x-hexclave-access-type': 'server',
|
||||
# You should store these in environment variables
|
||||
'x-hexclave-project-id': "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyHeader(" ")}
|
||||
'x-hexclave-secret-server-key': "${keys?.secretServerKey ?? "..."}",
|
||||
**kwargs.pop('headers', {}),
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
if res.status_code >= 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: <>
|
||||
<Typography>
|
||||
You can now make requests to the Hexclave API:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="python"
|
||||
content={deindent`
|
||||
# Get current project info
|
||||
print(stack_auth_request('GET', '/api/v1/projects/current'))
|
||||
|
||||
# Get user info with access token
|
||||
print(stack_auth_request('GET', '/api/v1/users/me', headers={
|
||||
'x-hexclave-access-token': access_token,
|
||||
}))
|
||||
`}
|
||||
title="example.py"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
];
|
||||
|
||||
const selectedKeyType = selectedFramework === 'nextjs' ? 'next' : selectedFramework === 'tanstackStart' ? 'vite' : 'raw';
|
||||
const selectedInstallPrompt = buildCloudSetupPrompt({
|
||||
docsBaseUrl: getSetupDocsBaseUrl(),
|
||||
projectId: adminApp.projectId,
|
||||
apiBaseUrl: getSetupApiBaseUrl(),
|
||||
});
|
||||
const manualSetupDocsUrl = getManualSetupDocsUrl();
|
||||
|
||||
return (
|
||||
<PageLayout width={1000}>
|
||||
@ -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');
|
||||
}}
|
||||
>
|
||||
<BookIcon className="w-4 h-4 mr-2" />
|
||||
@ -617,13 +141,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-8 mx-4">
|
||||
<Tabs value={setupMode} onValueChange={(value) => {
|
||||
if (value === "manual" || value === "recommended") {
|
||||
setSetupMode(value);
|
||||
return;
|
||||
}
|
||||
throw new Error(`Unexpected setup mode: ${value}`);
|
||||
}}>
|
||||
<Tabs value={setupMode} onValueChange={(value) => setSetupMode(value === "manual" ? "manual" : "recommended")}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="recommended">Recommended</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual setup</TabsTrigger>
|
||||
@ -631,122 +149,73 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-4 mx-4">
|
||||
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
|
||||
{(setupMode === "recommended" ? [
|
||||
{
|
||||
step: 1,
|
||||
title: "Copy Setup Prompt",
|
||||
content: <div className="flex min-w-0 flex-col gap-4">
|
||||
<CodeBlock
|
||||
language="text"
|
||||
content={SETUP_PROMPT}
|
||||
title="Prompt for your AI agent"
|
||||
icon="code"
|
||||
maxHeight={260}
|
||||
/>
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Create Keys",
|
||||
content: <>
|
||||
<Typography>
|
||||
Add these to your project's <InlineCode>.env.local</InlineCode> file.
|
||||
</Typography>
|
||||
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="next" />
|
||||
</>,
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Done",
|
||||
content: <>
|
||||
<Typography>
|
||||
After starting your dev server, navigate to <StyledLink href="http://localhost:3000/handler/signup">http://localhost:3000/handler/signup</StyledLink>, you will see the sign-up page.
|
||||
</Typography>
|
||||
</>,
|
||||
},
|
||||
] : [
|
||||
{
|
||||
step: 1,
|
||||
title: "Select your framework",
|
||||
content: <div>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{([{
|
||||
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 }) => (
|
||||
<DesignButton
|
||||
key={id}
|
||||
variant="plain"
|
||||
className={cn(
|
||||
"h-24 w-24 flex flex-col items-center justify-center gap-2 rounded-2xl border transition-all duration-150 hover:transition-none shadow-sm",
|
||||
id === selectedFramework
|
||||
? "bg-white/95 dark:bg-background/90 ring-1 ring-black/[0.08] dark:ring-white/[0.08] border-black/[0.1] dark:border-white/[0.1] shadow"
|
||||
: "bg-white/40 dark:bg-background/20 ring-1 ring-black/[0.04] dark:ring-white/[0.04] border-transparent hover:bg-white/70 dark:hover:bg-background/40 hover:ring-black/[0.06] dark:hover:ring-white/[0.06]"
|
||||
)}
|
||||
onClick={() => setSelectedFramework(id)}
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={name}
|
||||
|
||||
className={reverseIfDark ? "dark:invert" : undefined}
|
||||
width="0"
|
||||
height="0"
|
||||
sizes="100vw"
|
||||
style={{ width: '30px', height: 'auto' }}
|
||||
/>
|
||||
<span className="max-w-full px-1 text-center text-xs font-medium leading-tight text-foreground/90 whitespace-normal">
|
||||
{name}
|
||||
</span>
|
||||
</DesignButton>
|
||||
))}
|
||||
{setupMode === "recommended" ? (
|
||||
<div className="flex flex-col mt-4 mx-4">
|
||||
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
|
||||
{[
|
||||
{
|
||||
step: 1,
|
||||
title: "Copy Setup Prompt",
|
||||
content: <div className="flex min-w-0 flex-col gap-4">
|
||||
<CodeBlock
|
||||
language="text"
|
||||
content={selectedInstallPrompt}
|
||||
customRender={
|
||||
<pre className="max-h-[480px] overflow-y-auto whitespace-pre-wrap break-words p-4 text-sm leading-6 text-foreground">
|
||||
{selectedInstallPrompt}
|
||||
</pre>
|
||||
}
|
||||
title="Prompt for your AI agent"
|
||||
icon="code"
|
||||
maxHeight={480}
|
||||
/>
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Create Keys",
|
||||
content: <>
|
||||
<Typography>
|
||||
Add this server-only key to your project's <InlineCode>.env.local</InlineCode> file.
|
||||
</Typography>
|
||||
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} />
|
||||
</>,
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Done",
|
||||
content: <SetupRecommendedDoneStep onExploreDashboard={props.toMetrics} />,
|
||||
},
|
||||
].map((item) => (
|
||||
<li key={item.step} className={cn("ms-6 flex flex-col lg:flex-row gap-10 mb-20")}>
|
||||
<div className="flex flex-col justify-center gap-2 max-w-[180px] min-w-[180px]">
|
||||
<span className={`absolute flex items-center justify-center w-8 h-8 bg-zinc-100 dark:bg-zinc-800 rounded-full -start-4 ring-4 ring-white dark:ring-zinc-900`}>
|
||||
<span className={`text-zinc-500 dark:text-zinc-400 font-semibold`}>{item.step}</span>
|
||||
</span>
|
||||
<h3 className="font-medium leading-tight">{item.title}</h3>
|
||||
</div>
|
||||
</div>,
|
||||
},
|
||||
...(selectedFramework === 'nextjs' ? nextJsSteps : []),
|
||||
...(selectedFramework === 'tanstackStart' ? tanstackStartSteps : []),
|
||||
...(selectedFramework === 'react' ? reactSteps : []),
|
||||
...(selectedFramework === 'javascript' ? javascriptSteps : []),
|
||||
...(selectedFramework === 'python' ? pythonSteps : []),
|
||||
]).map((item) => (
|
||||
<li key={item.step} className={cn("ms-6 flex flex-col lg:flex-row gap-10 mb-20")}>
|
||||
<div className="flex flex-col justify-center gap-2 max-w-[180px] min-w-[180px]">
|
||||
<span className={`absolute flex items-center justify-center w-8 h-8 bg-zinc-100 dark:bg-zinc-800 rounded-full -start-4 ring-4 ring-white dark:ring-zinc-900`}>
|
||||
<span className={`text-zinc-500 dark:text-zinc-400 font-semibold`}>{item.step}</span>
|
||||
</span>
|
||||
<h3 className="font-medium leading-tight">{item.title}</h3>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-grow flex-col gap-4">
|
||||
{item.content}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-grow flex-col gap-4">
|
||||
{item.content}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-4 mt-12 flex flex-col items-center gap-4 py-16 text-center">
|
||||
<Typography>
|
||||
Manual setup steps live in the documentation so they stay up to date with every framework and SDK change.
|
||||
</Typography>
|
||||
<DesignButton
|
||||
onClick={() => {
|
||||
window.open(manualSetupDocsUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<BookIcon className="w-4 h-4 mr-2" />
|
||||
Open manual setup docs
|
||||
</DesignButton>
|
||||
</div>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@ -826,10 +295,24 @@ function GlobeIllustrationInner() {
|
||||
);
|
||||
}
|
||||
|
||||
function SetupRecommendedDoneStep(props: { onExploreDashboard: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Typography>
|
||||
Hooray! Setup completed.
|
||||
</Typography>
|
||||
<div>
|
||||
<DesignButton onClick={props.onExploreDashboard}>
|
||||
Explore Dashboard
|
||||
</DesignButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HexclaveKeys(props: {
|
||||
keys: { projectId: string, publishableClientKey?: string, secretServerKey: string } | null,
|
||||
onGenerateKeys: () => Promise<void>,
|
||||
type: 'next' | 'vite' | 'raw',
|
||||
}) {
|
||||
if (!props.keys) {
|
||||
return (
|
||||
@ -845,24 +328,7 @@ function HexclaveKeys(props: {
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-3">
|
||||
{props.type === 'next' ? (
|
||||
<NextJsEnvKeys
|
||||
projectId={props.keys.projectId}
|
||||
publishableClientKey={props.keys.publishableClientKey}
|
||||
secretServerKey={props.keys.secretServerKey}
|
||||
/>
|
||||
) : props.type === 'vite' ? (
|
||||
<ViteEnvKeys
|
||||
projectId={props.keys.projectId}
|
||||
secretServerKey={props.keys.secretServerKey}
|
||||
/>
|
||||
) : (
|
||||
<APIEnvKeys
|
||||
projectId={props.keys.projectId}
|
||||
publishableClientKey={props.keys.publishableClientKey}
|
||||
secretServerKey={props.keys.secretServerKey}
|
||||
/>
|
||||
)}
|
||||
<EnvFileViewer filename=".env.local" value={`HEXCLAVE_SECRET_SERVER_KEY=${props.keys.secretServerKey}`} />
|
||||
|
||||
<Typography type="label" variant="secondary">
|
||||
{`Save these keys securely - they won't be shown again after leaving this page.`}
|
||||
|
||||
@ -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<string, CSSProperties> = {
|
||||
'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<string, CSSProperties> = {
|
||||
...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<string, CSSProperties> = {
|
||||
...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 (
|
||||
<div className={cn(
|
||||
"overflow-hidden transition-all duration-150 hover:transition-none",
|
||||
!props.fullWidth && "rounded-xl",
|
||||
props.neutralBackground
|
||||
? "bg-background border border-black/[0.08] dark:border-white/[0.06] shadow-sm"
|
||||
: "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]"
|
||||
? "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-background border border-black/[0.08] dark:border-white/[0.06] shadow-sm"
|
||||
: !props.fullWidth
|
||||
? codePanelShellClasses
|
||||
: "overflow-hidden transition-all duration-150 hover:transition-none 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]"
|
||||
)}>
|
||||
<div className={cn(
|
||||
"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]",
|
||||
@ -143,7 +79,7 @@ export function CodeBlock(props: CodeBlockProps) {
|
||||
<div className="overflow-x-auto">
|
||||
{props.customRender ?? <SyntaxHighlighter
|
||||
language={props.language}
|
||||
style={theme === 'dark' ? darkCodeTheme : lightCodeTheme}
|
||||
style={theme === 'dark' ? dark : prism}
|
||||
customStyle={{
|
||||
background: 'transparent',
|
||||
padding: '1em',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
@ -11,10 +12,6 @@ type EnvFileViewerProps = {
|
||||
value: string;
|
||||
}
|
||||
|
||||
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 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]";
|
||||
|
||||
export function EnvFileViewer({ filename, value }: EnvFileViewerProps) {
|
||||
const [revealAll, setRevealAll] = useState(false);
|
||||
|
||||
@ -83,7 +80,7 @@ export function EnvFileViewer({ filename, value }: EnvFileViewerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function getEnvFileContent(props: {
|
||||
export function getEnvFileContent(props: {
|
||||
projectId: string,
|
||||
publishableClientKey?: string,
|
||||
secretServerKey?: string,
|
||||
|
||||
0
gist-id.txt
Normal file
0
gist-id.txt
Normal file
Loading…
Reference in New Issue
Block a user