Pass 1 of the config instructions

This commit is contained in:
Owen
2026-08-18 13:47:08 -04:00
parent 6c6e5c0fdf
commit 2a3c00045f
9 changed files with 667 additions and 7 deletions
+18
View File
@@ -5,6 +5,7 @@ import moment from "moment";
import { Button } from "@app/components/ui/button";
import CopyTextBox from "@app/components/CopyTextBox";
import CopyToClipboard from "@app/components/CopyToClipboard";
import { AiClientConfigSection } from "@app/components/ai-client-config/AiClientConfigSection";
import {
SettingsContainer,
SettingsFormCell,
@@ -166,6 +167,14 @@ export default function UserVirtualApiKeys({
}: UserVirtualApiKeysProps) {
const t = useTranslations();
const resourceName = initialData.resourceName;
const { getCopyText: getKeyCopyText } = useMyVirtualApiKeySecret(
orgId,
initialData.userKey.virtualApiKeyId
);
const keyPreview = formatVirtualApiKeyPreview(
initialData.userKey.virtualApiKeyId,
initialData.userKey.lastChars
);
return (
<>
@@ -177,6 +186,15 @@ export default function UserVirtualApiKeys({
resourceName={resourceName}
/>
<AiClientConfigSection
endpoint={t("aiClientConfigEndpointPlaceholder")}
auth={{
mode: "keyed",
keyDisplay: keyPreview,
getKeyText: getKeyCopyText
}}
/>
{initialData.manualKeys.length > 0 ? (
<SettingsSection>
<SettingsSectionHeader>
@@ -0,0 +1,138 @@
"use client";
import { AiConfigCodeBlock } from "@app/components/ai-client-config/AiConfigCodeBlock";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from "@app/components/ui/collapsible";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@app/components/ui/select";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger
} from "@app/components/ui/tabs";
import type { AiClientGuide, AiClientPresetId } from "@app/lib/aiClientConfig";
import { cn } from "@app/lib/cn";
import { ChevronDown, type LucideIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
type AiClientConfigCardProps = {
guide: AiClientGuide;
description: string;
icon: LucideIcon;
defaultOpen?: boolean;
};
export function AiClientConfigCard({
guide,
description,
icon: Icon,
defaultOpen = false
}: AiClientConfigCardProps) {
const t = useTranslations();
const [open, setOpen] = useState(defaultOpen);
const [presetId, setPresetId] = useState<AiClientPresetId>(
guide.presets[0]?.id ?? "default"
);
const preset =
guide.presets.find((p) => p.id === presetId) ?? guide.presets[0];
const manualContent = (
<div className="space-y-4">
{guide.presets.length > 1 ? (
<Select
value={presetId}
onValueChange={(value) =>
setPresetId(value as AiClientPresetId)
}
>
<SelectTrigger size="sm" className="max-w-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{guide.presets.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : null}
<div className="grid gap-4 @lg:grid-cols-2">
{preset?.blocks.map((block) => (
<AiConfigCodeBlock key={block.id} block={block} />
))}
</div>
</div>
);
return (
<Collapsible
open={open}
onOpenChange={setOpen}
className="rounded-md border bg-card"
>
<CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left cursor-pointer">
<Icon className="size-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="font-medium truncate">{guide.name}</p>
<p className="text-xs text-muted-foreground truncate">
{description}
</p>
</div>
<ChevronDown
className={cn(
"size-4 shrink-0 text-muted-foreground transition-transform",
open && "rotate-180"
)}
/>
</CollapsibleTrigger>
<CollapsibleContent className="border-t px-4 py-4">
{guide.cli ? (
<Tabs defaultValue="cli">
<TabsList>
<TabsTrigger value="cli">
{t("aiClientConfigTabCli")}
</TabsTrigger>
<TabsTrigger value="manual">
{t("aiClientConfigTabManual")}
</TabsTrigger>
</TabsList>
<TabsContent
value="cli"
className="grid gap-4 @lg:grid-cols-2 mt-4"
>
<AiConfigCodeBlock block={guide.cli.configure} />
<AiConfigCodeBlock block={guide.cli.run} />
{guide.cli.configureWithKey ? (
<AiConfigCodeBlock
block={guide.cli.configureWithKey}
/>
) : null}
{guide.cli.runWithKey ? (
<AiConfigCodeBlock
block={guide.cli.runWithKey}
/>
) : null}
</TabsContent>
<TabsContent value="manual" className="mt-4">
{manualContent}
</TabsContent>
</Tabs>
) : (
manualContent
)}
</CollapsibleContent>
</Collapsible>
);
}
@@ -0,0 +1,77 @@
"use client";
import { AiClientConfigCard } from "@app/components/ai-client-config/AiClientConfigCard";
import {
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import type { AiClientAuth } from "@app/lib/aiClientConfig";
import { buildAiClientGuides } from "@app/lib/aiClientConfig";
import { cn } from "@app/lib/cn";
import { MousePointerClick, Sparkles, SquareTerminal, TerminalSquare } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
type AiClientConfigSectionProps = {
endpoint: string;
auth: AiClientAuth;
className?: string;
};
export function AiClientConfigSection({
endpoint,
auth,
className
}: AiClientConfigSectionProps) {
const t = useTranslations();
const guides = useMemo(
() => buildAiClientGuides(endpoint, auth),
[endpoint, auth]
);
const icons = {
claude: Sparkles,
codex: TerminalSquare,
opencode: SquareTerminal,
cursor: MousePointerClick
} as const;
const descriptions: Record<string, string> = {
claude: t("aiClientConfigDescriptionClaude"),
codex: t("aiClientConfigDescriptionCodex"),
opencode: t("aiClientConfigDescriptionOpencode"),
cursor: t("aiClientConfigDescriptionCursor")
};
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("aiClientConfigTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("aiClientConfigDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div
className={cn("@container space-y-3", className)}
>
{guides.map((guide, index) => (
<AiClientConfigCard
key={guide.id}
guide={guide}
description={descriptions[guide.id]}
icon={icons[guide.id]}
defaultOpen={index === 0}
/>
))}
</div>
</SettingsSectionBody>
</SettingsSection>
);
}
@@ -0,0 +1,27 @@
"use client";
import CopyTextBox from "@app/components/CopyTextBox";
import type { AiConfigBlock } from "@app/lib/aiClientConfig";
export function AiConfigCodeBlock({ block }: { block: AiConfigBlock }) {
return (
<div className="space-y-1.5">
<p className="font-mono text-xs text-muted-foreground">
{block.label}
</p>
<div
className={
block.kind === "steps"
? "[&_pre]:text-sm"
: "[&_pre]:text-xs [&_code]:font-mono"
}
>
<CopyTextBox
text={block.displayText}
getCopyText={block.getCopyText}
wrapText={block.kind === "steps"}
/>
</div>
</div>
);
}
@@ -1,5 +1,6 @@
"use client";
import { AiClientConfigSection } from "@app/components/ai-client-config/AiClientConfigSection";
import CopyToClipboard from "@app/components/CopyToClipboard";
import {
InfoSection,
@@ -27,6 +28,7 @@ import {
} from "@app/components/SidePanel";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { Button } from "@app/components/ui/button";
import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
import {
derivePublicAuthState,
formatPublicResourceType
@@ -34,6 +36,7 @@ import {
import { getLauncherResourceAdminHref } from "@app/lib/launcherResourceAdminHref";
import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess";
import { launcherQueries } from "@app/lib/queries";
import { formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat";
import type { LauncherResource } from "@server/routers/launcher/types";
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
import type { GetResourceResponse } from "@server/routers/resource/getResource";
@@ -249,6 +252,15 @@ function PublicResourceDetails({
const authState = derivePublicAuthState(resource.mode, authInfo);
const infoSectionCount = 2 + (showAuthBadge ? 1 : 0) + (showHealth ? 1 : 0);
const { data: aiKeysData } = useQuery({
...launcherQueries.myVirtualApiKeys(orgId, resource.resourceGuid),
enabled: isInference
});
const { getCopyText: getAiKeyCopyText } = useMyVirtualApiKeySecret(
orgId,
aiKeysData?.userKey.virtualApiKeyId ?? ""
);
return (
<div className="space-y-4">
<SettingsSection>
@@ -333,6 +345,19 @@ function PublicResourceDetails({
orgId={orgId}
resourceGuid={resource.resourceGuid}
/>
{aiKeysData ? (
<AiClientConfigSection
endpoint={launcherResource.accessUrl ?? ""}
auth={{
mode: "keyed",
keyDisplay: formatVirtualApiKeyPreview(
aiKeysData.userKey.virtualApiKeyId,
aiKeysData.userKey.lastChars
),
getKeyText: getAiKeyCopyText
}}
/>
) : null}
</>
) : null}
</div>
@@ -397,13 +422,19 @@ function PrivateResourceDetails({
</SettingsSectionBody>
</SettingsSection>
{isInference ? (
<LauncherInferenceModelsSection
orgId={orgId}
params={{
resourceType: "site",
siteResourceId: resource.siteResourceId
}}
/>
<>
<LauncherInferenceModelsSection
orgId={orgId}
params={{
resourceType: "site",
siteResourceId: resource.siteResourceId
}}
/>
<AiClientConfigSection
endpoint={launcherResource.accessUrl ?? ""}
auth={{ mode: "keyless" }}
/>
</>
) : null}
</div>
);