mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-20 22:13:33 +00:00
Guide onboarding through granular provider setup steps
This commit is contained in:
parent
084b660cd4
commit
9f704fc911
6 changed files with 212 additions and 139 deletions
|
|
@ -1259,8 +1259,7 @@ function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const providerFormVisible = providerAddOpen || (activeView === "onboarding" && onboardingStep === "provider");
|
if (!window.ccr || !providerAddOpen) {
|
||||||
if (!window.ccr || !providerFormVisible) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (providerDraft.protocolDetectionMode === "manual") {
|
if (providerDraft.protocolDetectionMode === "manual") {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import {
|
import {
|
||||||
AddProfileDraft, AddProviderDraft, AppConfig, Button, Check, ChevronLeft,
|
AddProfileDraft, AddProviderDraft, AppConfig, Button, Check, ChevronLeft,
|
||||||
ChevronRight, CircleAlert, cn, GatewayProviderProbeResult, GatewayStatus, Gauge, getNextOnboardingStep,
|
ChevronRight, cn, findProviderPreset, GatewayProviderProbeResult, GatewayStatus, Gauge, getNextOnboardingStep,
|
||||||
isOnboardingProfileReady, isOnboardingProviderReady, Layers3, LucideIcon, motion, motionEase,
|
isOnboardingProfileReady, isOnboardingProviderReady, Layers3, LucideIcon, mergeProviderModelLists, motion, motionEase,
|
||||||
LoaderCircle, onboardingMascotSpriteUrl, OnboardingReadinessOptions, OnboardingStepId, onboardingStepOrder, ProviderConnectivityCheckReport, reducedMotionTransition, useAppText, useReducedMotion,
|
LoaderCircle, onboardingMascotSpriteUrl, OnboardingReadinessOptions, OnboardingStepId, onboardingStepOrder, providerDraftHasReadyCredentialPool, ProviderConnectivityCheckReport, reducedMotionTransition, splitLines, useAppText, useEffect, useReducedMotion,
|
||||||
useState,
|
useState,
|
||||||
UserRound, X
|
UserRound, X
|
||||||
} from "../shared/index";
|
} from "../shared/index";
|
||||||
import { AddProviderForm } from "./providers";
|
import { AddProviderForm, providerSetupStepIds, type ProviderSetupStepId } from "./providers";
|
||||||
import { AddProfileForm } from "./profiles";
|
import { AddProfileForm } from "./profiles";
|
||||||
|
|
||||||
type OnboardingMascotTone = "cyan" | "orange" | "violet";
|
type OnboardingMascotTone = "cyan" | "orange" | "violet";
|
||||||
|
|
@ -108,6 +108,7 @@ export function OnboardingView({
|
||||||
const t = useAppText();
|
const t = useAppText();
|
||||||
const shouldReduceMotion = useReducedMotion();
|
const shouldReduceMotion = useReducedMotion();
|
||||||
const [providerIconDetecting, setProviderIconDetecting] = useState(false);
|
const [providerIconDetecting, setProviderIconDetecting] = useState(false);
|
||||||
|
const [providerSetupStep, setProviderSetupStep] = useState<ProviderSetupStepId>("provider");
|
||||||
const providerReady = isOnboardingProviderReady(config);
|
const providerReady = isOnboardingProviderReady(config);
|
||||||
const profileReady = isOnboardingProfileReady(config, readiness);
|
const profileReady = isOnboardingProfileReady(config, readiness);
|
||||||
const serviceReady = gatewayStatus.state === "running";
|
const serviceReady = gatewayStatus.state === "running";
|
||||||
|
|
@ -116,25 +117,72 @@ export function OnboardingView({
|
||||||
const activeDetails = onboardingStepDetails[activeStep];
|
const activeDetails = onboardingStepDetails[activeStep];
|
||||||
const previousStep = onboardingStepOrder[activeIndex - 1];
|
const previousStep = onboardingStepOrder[activeIndex - 1];
|
||||||
const nextStep = getNextOnboardingStep(activeStep, config, readiness);
|
const nextStep = getNextOnboardingStep(activeStep, config, readiness);
|
||||||
|
const localAgentProviderImport = providerDraft.providerPlugins.length > 0;
|
||||||
|
const providerIdentityReady = Boolean(findProviderPreset(providerDraft.presetId) || providerDraft.baseUrl.trim());
|
||||||
|
const providerCredentialReady = localAgentProviderImport || Boolean(
|
||||||
|
providerDraft.credentialMode === "pool"
|
||||||
|
? providerDraftHasReadyCredentialPool(providerDraft)
|
||||||
|
: providerDraft.apiKey.trim()
|
||||||
|
);
|
||||||
|
const providerModelsReady = mergeProviderModelLists(providerDraft.selectedModels, splitLines(providerDraft.modelsText)).length > 0;
|
||||||
|
const providerSetupIndex = Math.max(0, providerSetupStepIds.indexOf(providerSetupStep));
|
||||||
|
const previousProviderSetupStep = activeStep === "provider" ? providerSetupStepIds[providerSetupIndex - 1] : undefined;
|
||||||
|
const nextProviderSetupStep = activeStep === "provider" ? providerSetupStepIds[providerSetupIndex + 1] : undefined;
|
||||||
const providerSubmitLoading = activeStep === "provider" && (providerProbeLoading || providerConnectivityLoading || providerIconDetecting);
|
const providerSubmitLoading = activeStep === "provider" && (providerProbeLoading || providerConnectivityLoading || providerIconDetecting);
|
||||||
const nextDisabled = activeStep === "provider"
|
const nextDisabled = activeStep === "provider"
|
||||||
? providerSubmitLoading || !(providerReady || canSubmitProvider)
|
? providerSubmitLoading || (nextProviderSetupStep
|
||||||
|
? !isProviderSetupStepReady(providerSetupStep)
|
||||||
|
: !(providerReady || canSubmitProvider))
|
||||||
: activeStep === "profile"
|
: activeStep === "profile"
|
||||||
? !(profileReady || (providerReady && canSubmitProfile))
|
? !(profileReady || (providerReady && canSubmitProfile))
|
||||||
: !routeReady;
|
: !routeReady;
|
||||||
const nextGuidance = onboardingNextGuidance({
|
|
||||||
activeStep,
|
useEffect(() => {
|
||||||
canSubmitProfile,
|
if (activeStep !== "provider" || isProviderSetupStepUnlocked(providerSetupStep)) {
|
||||||
canSubmitProvider,
|
return;
|
||||||
profileReady,
|
}
|
||||||
providerReady,
|
setProviderSetupStep(getLatestUnlockedProviderSetupStep());
|
||||||
providerSubmitLoading,
|
}, [activeStep, providerCredentialReady, providerIdentityReady, providerModelsReady, providerSetupStep]);
|
||||||
routeReady,
|
|
||||||
serviceReady
|
function isProviderSetupStepReady(step: ProviderSetupStepId): boolean {
|
||||||
});
|
switch (step) {
|
||||||
|
case "provider":
|
||||||
|
return providerIdentityReady;
|
||||||
|
case "credentials":
|
||||||
|
return providerCredentialReady;
|
||||||
|
case "models":
|
||||||
|
return providerModelsReady;
|
||||||
|
case "verify":
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProviderSetupStepUnlocked(step: ProviderSetupStepId): boolean {
|
||||||
|
switch (step) {
|
||||||
|
case "provider":
|
||||||
|
return true;
|
||||||
|
case "credentials":
|
||||||
|
return providerIdentityReady;
|
||||||
|
case "models":
|
||||||
|
return providerIdentityReady && providerCredentialReady;
|
||||||
|
case "verify":
|
||||||
|
return providerIdentityReady && providerCredentialReady && providerModelsReady;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestUnlockedProviderSetupStep(): ProviderSetupStepId {
|
||||||
|
return [...providerSetupStepIds].reverse().find(isProviderSetupStepUnlocked) ?? "provider";
|
||||||
|
}
|
||||||
|
|
||||||
function goToPreviousStep() {
|
function goToPreviousStep() {
|
||||||
|
if (activeStep === "provider" && previousProviderSetupStep) {
|
||||||
|
setProviderSetupStep(previousProviderSetupStep);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (previousStep) {
|
if (previousStep) {
|
||||||
|
if (previousStep === "provider") {
|
||||||
|
setProviderSetupStep(getLatestUnlockedProviderSetupStep());
|
||||||
|
}
|
||||||
onSelectStep(previousStep);
|
onSelectStep(previousStep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +199,12 @@ export function OnboardingView({
|
||||||
if (providerSubmitLoading) {
|
if (providerSubmitLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (nextProviderSetupStep) {
|
||||||
|
if (isProviderSetupStepReady(providerSetupStep)) {
|
||||||
|
setProviderSetupStep(nextProviderSetupStep);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (canSubmitProvider) {
|
if (canSubmitProvider) {
|
||||||
const saved = await onSubmitProvider();
|
const saved = await onSubmitProvider();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
|
|
@ -188,10 +242,10 @@ export function OnboardingView({
|
||||||
style={{ transformPerspective: 900 }}
|
style={{ transformPerspective: 900 }}
|
||||||
transition={shouldReduceMotion ? reducedMotionTransition : { duration: 0.28, ease: motionEase }}
|
transition={shouldReduceMotion ? reducedMotionTransition : { duration: 0.28, ease: motionEase }}
|
||||||
>
|
>
|
||||||
<OnboardingProgress activeIndex={activeIndex} />
|
<OnboardingProgress activeStep={activeStep} providerSetupStep={providerSetupStep} />
|
||||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col p-4 sm:p-5">
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col p-4 sm:p-5">
|
||||||
<div className="flex h-8 shrink-0 items-center">
|
<div className="flex h-8 shrink-0 items-center">
|
||||||
{previousStep ? (
|
{previousStep || previousProviderSetupStep ? (
|
||||||
<Button
|
<Button
|
||||||
className="inline-flex h-8 items-center gap-1.5 rounded-md px-1 text-[13px] font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/25"
|
className="inline-flex h-8 items-center gap-1.5 rounded-md px-1 text-[13px] font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/25"
|
||||||
onClick={goToPreviousStep}
|
onClick={goToPreviousStep}
|
||||||
|
|
@ -210,13 +264,6 @@ export function OnboardingView({
|
||||||
<h2 className="text-[20px] font-semibold tracking-normal">{t(activeDetails.title)}</h2>
|
<h2 className="text-[20px] font-semibold tracking-normal">{t(activeDetails.title)}</h2>
|
||||||
<p className="mt-1 text-[12px] leading-5 text-muted-foreground">{t(activeDetails.description)}</p>
|
<p className="mt-1 text-[12px] leading-5 text-muted-foreground">{t(activeDetails.description)}</p>
|
||||||
</div>
|
</div>
|
||||||
<OnboardingReadinessChecklist
|
|
||||||
activeStep={activeStep}
|
|
||||||
profileReady={profileReady}
|
|
||||||
providerReady={providerReady}
|
|
||||||
routeReady={routeReady}
|
|
||||||
serviceReady={serviceReady}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="onboarding-step-panels mt-5 min-h-0 flex-1 overflow-hidden">
|
<div className="onboarding-step-panels mt-5 min-h-0 flex-1 overflow-hidden">
|
||||||
|
|
@ -230,10 +277,16 @@ export function OnboardingView({
|
||||||
connectivityProbe={providerConnectivityProbe}
|
connectivityProbe={providerConnectivityProbe}
|
||||||
draft={providerDraft}
|
draft={providerDraft}
|
||||||
error={providerError}
|
error={providerError}
|
||||||
|
activeStep={providerSetupStep}
|
||||||
mode={providerReady ? "edit" : "add"}
|
mode={providerReady ? "edit" : "add"}
|
||||||
onCheck={onCheckProvider}
|
onCheck={onCheckProvider}
|
||||||
onChange={onChangeProvider}
|
onChange={onChangeProvider}
|
||||||
onIconDetectingChange={setProviderIconDetecting}
|
onIconDetectingChange={setProviderIconDetecting}
|
||||||
|
onSelectStep={(step) => {
|
||||||
|
if (isProviderSetupStepUnlocked(step)) {
|
||||||
|
setProviderSetupStep(step);
|
||||||
|
}
|
||||||
|
}}
|
||||||
probe={providerProbe}
|
probe={providerProbe}
|
||||||
probeLoading={providerProbeLoading}
|
probeLoading={providerProbeLoading}
|
||||||
providerPlugins={config.providerPlugins ?? []}
|
providerPlugins={config.providerPlugins ?? []}
|
||||||
|
|
@ -284,15 +337,7 @@ export function OnboardingView({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex shrink-0 items-center justify-between gap-3 border-t border-border/60 pt-4 max-[640px]:flex-col max-[640px]:items-stretch">
|
<div className="mt-5 flex shrink-0 items-center justify-end gap-3 border-t border-border/60 pt-4 max-[640px]:items-stretch">
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
{nextGuidance ? (
|
|
||||||
<div className="flex min-w-0 items-start gap-2 rounded-md border border-border bg-muted/25 px-3 py-2 text-[12px] leading-5 text-muted-foreground">
|
|
||||||
<CircleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
||||||
<span className="min-w-0">{t(nextGuidance)}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<Button disabled={nextDisabled} onClick={() => void goToNextStep()} type="button">
|
<Button disabled={nextDisabled} onClick={() => void goToNextStep()} type="button">
|
||||||
{providerSubmitLoading ? <LoaderCircle className="h-4 w-4 animate-spin" /> : activeStep === "enter" ? <Check className="h-4 w-4" /> : null}
|
{providerSubmitLoading ? <LoaderCircle className="h-4 w-4 animate-spin" /> : activeStep === "enter" ? <Check className="h-4 w-4" /> : null}
|
||||||
{providerSubmitLoading ? t("Loading") : activeStep === "enter" ? t("Let's start") : t("Next step")}
|
{providerSubmitLoading ? t("Loading") : activeStep === "enter" ? t("Let's start") : t("Next step")}
|
||||||
|
|
@ -307,121 +352,42 @@ export function OnboardingView({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onboardingNextGuidance({
|
const onboardingProgressItems: Array<{ key: ProviderSetupStepId | "profile" | "enter"; label: string }> = [
|
||||||
activeStep,
|
{ key: "provider", label: "Choose provider" },
|
||||||
canSubmitProfile,
|
{ key: "credentials", label: "Add credentials" },
|
||||||
canSubmitProvider,
|
{ key: "models", label: "Pick models" },
|
||||||
profileReady,
|
{ key: "verify", label: "Verify connection" },
|
||||||
providerReady,
|
{ key: "profile", label: "Connect agent" },
|
||||||
providerSubmitLoading,
|
{ key: "enter", label: "Let's start" }
|
||||||
routeReady,
|
];
|
||||||
serviceReady
|
|
||||||
}: {
|
|
||||||
activeStep: OnboardingStepId;
|
|
||||||
canSubmitProfile: boolean;
|
|
||||||
canSubmitProvider: boolean;
|
|
||||||
profileReady: boolean;
|
|
||||||
providerReady: boolean;
|
|
||||||
providerSubmitLoading: boolean;
|
|
||||||
routeReady: boolean;
|
|
||||||
serviceReady: boolean;
|
|
||||||
}): string {
|
|
||||||
if (providerSubmitLoading) {
|
|
||||||
return "CCR is checking this provider. Wait for the check to finish before continuing.";
|
|
||||||
}
|
|
||||||
if (activeStep === "provider") {
|
|
||||||
if (canSubmitProvider) {
|
|
||||||
return "Save this provider to continue.";
|
|
||||||
}
|
|
||||||
if (!providerReady) {
|
|
||||||
return "Choose a provider preset or endpoint, enter an API key, and add at least one model.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activeStep === "profile") {
|
|
||||||
if (!providerReady) {
|
|
||||||
return "Configure a provider before creating an agent profile.";
|
|
||||||
}
|
|
||||||
if (canSubmitProfile) {
|
|
||||||
return "Save this agent profile to continue.";
|
|
||||||
}
|
|
||||||
if (!profileReady) {
|
|
||||||
return "Choose an agent, model, and required profile settings.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activeStep === "enter") {
|
|
||||||
if (!routeReady) {
|
|
||||||
return "Finish the provider and agent profile steps before entering the app.";
|
|
||||||
}
|
|
||||||
if (!serviceReady) {
|
|
||||||
return "The gateway is configured. Start the service from the toolbar when you are ready to test traffic.";
|
|
||||||
}
|
|
||||||
return "Send a request from your agent, then confirm it appears in Logs.";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function OnboardingReadinessChecklist({
|
function OnboardingProgress({
|
||||||
activeStep,
|
activeStep,
|
||||||
profileReady,
|
providerSetupStep
|
||||||
providerReady,
|
|
||||||
routeReady,
|
|
||||||
serviceReady
|
|
||||||
}: {
|
}: {
|
||||||
activeStep: OnboardingStepId;
|
activeStep: OnboardingStepId;
|
||||||
profileReady: boolean;
|
providerSetupStep: ProviderSetupStepId;
|
||||||
providerReady: boolean;
|
|
||||||
routeReady: boolean;
|
|
||||||
serviceReady: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const t = useAppText();
|
const t = useAppText();
|
||||||
const items = [
|
const activeKey = activeStep === "provider" ? providerSetupStep : activeStep;
|
||||||
{ active: activeStep === "provider", label: "Provider", ready: providerReady },
|
const activeIndex = Math.max(0, onboardingProgressItems.findIndex((item) => item.key === activeKey));
|
||||||
{ active: activeStep === "profile", label: "Agent profile", ready: profileReady },
|
const stepCount = onboardingProgressItems.length;
|
||||||
{ active: activeStep === "enter", label: "Gateway service", ready: serviceReady },
|
|
||||||
{ active: activeStep === "enter" && routeReady, label: "Ready to test", ready: routeReady }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-2 flex max-w-full flex-wrap items-center justify-center gap-1.5" aria-label={t("Setup readiness")}>
|
|
||||||
{items.map((item) => (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-6 max-w-full items-center gap-1.5 rounded-full border px-2 text-[11px] font-medium",
|
|
||||||
item.ready
|
|
||||||
? "border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-200"
|
|
||||||
: item.active
|
|
||||||
? "border-primary/25 bg-primary/10 text-primary"
|
|
||||||
: "border-border bg-muted/30 text-muted-foreground"
|
|
||||||
)}
|
|
||||||
key={item.label}
|
|
||||||
>
|
|
||||||
{item.ready ? <Check className="h-3 w-3 shrink-0" /> : <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-current opacity-70" />}
|
|
||||||
<span className="truncate">{t(item.label)}</span>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function OnboardingProgress({ activeIndex }: { activeIndex: number }) {
|
|
||||||
const t = useAppText();
|
|
||||||
const stepCount = onboardingStepOrder.length;
|
|
||||||
const progressWidth = `${((activeIndex + 1) / stepCount) * 100}%`;
|
const progressWidth = `${((activeIndex + 1) / stepCount) * 100}%`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative shrink-0 border-b border-border/60 bg-card/95" aria-label={`${t("Step")} ${activeIndex + 1} / ${stepCount}`}>
|
<div className="relative shrink-0 border-b border-border/60 bg-card/95" aria-label={`${t("Step")} ${activeIndex + 1} / ${stepCount}`}>
|
||||||
<div className="mx-auto flex h-11 max-w-[520px] items-center justify-center px-3 text-[13px] font-medium">
|
<div className="mx-auto flex h-11 max-w-[920px] items-center justify-start overflow-x-auto px-3 text-[12px] font-medium sm:justify-center">
|
||||||
{onboardingStepOrder.map((step, index) => (
|
{onboardingProgressItems.map((item, index) => (
|
||||||
<div className="flex min-w-0 items-center" key={step}>
|
<div className="flex shrink-0 items-center" key={item.key}>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-w-[136px] truncate",
|
"max-w-[128px] truncate max-[560px]:max-w-[96px]",
|
||||||
index === activeIndex ? "text-foreground" : "text-muted-foreground"
|
index === activeIndex ? "text-foreground" : "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t(onboardingStepDetails[step].title)}
|
{t(item.label)}
|
||||||
</span>
|
</span>
|
||||||
{index < stepCount - 1 ? <ChevronRight className="mx-5 h-4 w-4 shrink-0 text-muted-foreground/70 max-[560px]:mx-2" /> : null}
|
{index < stepCount - 1 ? <ChevronRight className="mx-2.5 h-4 w-4 shrink-0 text-muted-foreground/70 max-[560px]:mx-1.5" /> : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -759,7 +759,7 @@ export function AddProfileForm({
|
||||||
].filter(Boolean).length;
|
].filter(Boolean).length;
|
||||||
const advancedSummary = advancedIssueCount > 0
|
const advancedSummary = advancedIssueCount > 0
|
||||||
? t("Advanced settings need attention")
|
? t("Advanced settings need attention")
|
||||||
: t("Paths, provider identity, bot, compact, and env");
|
: t("Paths, bot, compact, and env");
|
||||||
const handleAppPathDrop = useCallback((event: ReactDragEvent<HTMLElement>) => {
|
const handleAppPathDrop = useCallback((event: ReactDragEvent<HTMLElement>) => {
|
||||||
if (!showAppPathField) {
|
if (!showAppPathField) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1593,9 +1593,9 @@ function localAgentProviderAlreadyImported(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderSetupStepId = "provider" | "credentials" | "models" | "verify";
|
export type ProviderSetupStepId = "provider" | "credentials" | "models" | "verify";
|
||||||
|
|
||||||
const providerSetupStepIds: ProviderSetupStepId[] = ["provider", "credentials", "models", "verify"];
|
export const providerSetupStepIds: ProviderSetupStepId[] = ["provider", "credentials", "models", "verify"];
|
||||||
|
|
||||||
function ProviderSetupProgress({
|
function ProviderSetupProgress({
|
||||||
activeStep,
|
activeStep,
|
||||||
|
|
|
||||||
|
|
@ -431,7 +431,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
||||||
"Idle seconds must be between 30 and 86400.": "Idle seconds must be between 30 and 86400.",
|
"Idle seconds must be between 30 and 86400.": "Idle seconds must be between 30 and 86400.",
|
||||||
"No request logs match the current filters.": "No request logs match the current filters.",
|
"No request logs match the current filters.": "No request logs match the current filters.",
|
||||||
"No request logs yet.": "No request logs yet.",
|
"No request logs yet.": "No request logs yet.",
|
||||||
"Paths, provider identity, bot, compact, and env": "Paths, provider identity, bot, compact, and env",
|
"Paths, bot, compact, and env": "Paths, bot, compact, and env",
|
||||||
"Request logs are off": "Request logs are off",
|
"Request logs are off": "Request logs are off",
|
||||||
"Request logs record gateway requests and make payload inspection available.": "Request logs record gateway requests and make payload inspection available.",
|
"Request logs record gateway requests and make payload inspection available.": "Request logs record gateway requests and make payload inspection available.",
|
||||||
"Select an existing bot or turn Bot off.": "Select an existing bot or turn Bot off.",
|
"Select an existing bot or turn Bot off.": "Select an existing bot or turn Bot off.",
|
||||||
|
|
@ -1314,7 +1314,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
||||||
"Retry": "继续重试",
|
"Retry": "继续重试",
|
||||||
"Retry attempts": "重试尝试",
|
"Retry attempts": "重试尝试",
|
||||||
"Weighted order": "按权重排序",
|
"Weighted order": "按权重排序",
|
||||||
"Paths, provider identity, bot, compact, and env": "路径、供应商身份、Bot、压缩和环境变量",
|
"Paths, bot, compact, and env": "路径、Bot、压缩和环境变量",
|
||||||
"Ready to route": "可以开始路由",
|
"Ready to route": "可以开始路由",
|
||||||
"Restart proxy": "重启代理",
|
"Restart proxy": "重启代理",
|
||||||
"Route": "路由",
|
"Route": "路由",
|
||||||
|
|
|
||||||
108
packages/ui/test/component/onboarding.test.tsx
Normal file
108
packages/ui/test/component/onboarding.test.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import * as React from "react";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { OnboardingView } from "@ccr/ui/pages/home/components/onboarding.tsx";
|
||||||
|
import { AppI18nContext, appCopy } from "@ccr/ui/pages/home/shared/i18n.tsx";
|
||||||
|
import {
|
||||||
|
createProfileDraft,
|
||||||
|
createProviderDraft,
|
||||||
|
fallbackGatewayStatus
|
||||||
|
} from "@ccr/ui/pages/home/shared/index.tsx";
|
||||||
|
import { appConfigFixture } from "../fixtures/index.ts";
|
||||||
|
|
||||||
|
test("Onboarding provider step renders only the active provider setup section", () => {
|
||||||
|
const config = appConfigFixture();
|
||||||
|
const providerDraft = {
|
||||||
|
...createProviderDraft([]),
|
||||||
|
baseUrl: "https://gateway.example/v1",
|
||||||
|
modelsText: "example-model",
|
||||||
|
name: "Example Provider"
|
||||||
|
};
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<AppI18nContext.Provider value={appCopy.en}>
|
||||||
|
<OnboardingView
|
||||||
|
activeStep="provider"
|
||||||
|
canSubmitProfile={false}
|
||||||
|
canSubmitProvider={false}
|
||||||
|
config={config}
|
||||||
|
endpoint="http://127.0.0.1:3456"
|
||||||
|
gatewayStatus={fallbackGatewayStatus}
|
||||||
|
onCheckProvider={async () => ({ failed: [], passed: [], results: [] })}
|
||||||
|
onChangeProfile={() => undefined}
|
||||||
|
onChangeProvider={() => undefined}
|
||||||
|
onComplete={() => undefined}
|
||||||
|
onSelectStep={() => undefined}
|
||||||
|
onSubmitProfile={async () => false}
|
||||||
|
onSubmitProvider={async () => false}
|
||||||
|
profileDraft={createProfileDraft()}
|
||||||
|
profileError=""
|
||||||
|
providerConnectivityLoading={false}
|
||||||
|
providerDraft={providerDraft}
|
||||||
|
providerError=""
|
||||||
|
providerProbeLoading={false}
|
||||||
|
/>
|
||||||
|
</AppI18nContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /Choose provider/);
|
||||||
|
assert.match(html, /Add credentials/);
|
||||||
|
assert.match(html, /Pick models/);
|
||||||
|
assert.match(html, /Verify connection/);
|
||||||
|
assert.match(html, /Connect agent/);
|
||||||
|
assert.match(html, /Let's start/);
|
||||||
|
assert.match(html, /aria-label="Step 1 \/ 6"/);
|
||||||
|
assert.match(html, /1 \/ 4/);
|
||||||
|
assert.doesNotMatch(html, /Setup readiness/);
|
||||||
|
assert.doesNotMatch(html, /Choose a provider preset or endpoint, enter an API key, and add at least one model/);
|
||||||
|
assert.doesNotMatch(html, /Choose how this provider authenticates model requests/);
|
||||||
|
assert.doesNotMatch(html, /Choose the models that should be available through this provider/);
|
||||||
|
assert.doesNotMatch(html, /Run a real model request before relying on this provider/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Onboarding profile step does not prompt to save before continuing", () => {
|
||||||
|
const config = appConfigFixture();
|
||||||
|
config.Providers = [{
|
||||||
|
api_base_url: "https://gateway.example/v1",
|
||||||
|
api_key: "sk-test",
|
||||||
|
models: ["example-model"],
|
||||||
|
name: "Example Provider",
|
||||||
|
type: "openai_chat_completions"
|
||||||
|
}];
|
||||||
|
const providerDraft = {
|
||||||
|
...createProviderDraft([]),
|
||||||
|
baseUrl: "https://gateway.example/v1",
|
||||||
|
modelsText: "example-model",
|
||||||
|
name: "Example Provider"
|
||||||
|
};
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<AppI18nContext.Provider value={appCopy.en}>
|
||||||
|
<OnboardingView
|
||||||
|
activeStep="profile"
|
||||||
|
canSubmitProfile
|
||||||
|
canSubmitProvider={false}
|
||||||
|
config={config}
|
||||||
|
endpoint="http://127.0.0.1:3456"
|
||||||
|
gatewayStatus={fallbackGatewayStatus}
|
||||||
|
onCheckProvider={async () => ({ failed: [], passed: [], results: [] })}
|
||||||
|
onChangeProfile={() => undefined}
|
||||||
|
onChangeProvider={() => undefined}
|
||||||
|
onComplete={() => undefined}
|
||||||
|
onSelectStep={() => undefined}
|
||||||
|
onSubmitProfile={async () => false}
|
||||||
|
onSubmitProvider={async () => false}
|
||||||
|
profileDraft={createProfileDraft()}
|
||||||
|
profileError=""
|
||||||
|
providerConnectivityLoading={false}
|
||||||
|
providerDraft={providerDraft}
|
||||||
|
providerError=""
|
||||||
|
providerProbeLoading={false}
|
||||||
|
/>
|
||||||
|
</AppI18nContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.doesNotMatch(html, /Save this agent profile to continue/);
|
||||||
|
assert.doesNotMatch(html, /Choose an agent, model, and required profile settings/);
|
||||||
|
assert.doesNotMatch(html, /provider identity/);
|
||||||
|
assert.match(html, /Paths, bot, compact, and env/);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue