Session 6.3
This commit is contained in:
488
src/components/onboarding/wizard.tsx
Normal file
488
src/components/onboarding/wizard.tsx
Normal file
@@ -0,0 +1,488 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
type Step = "welcome" | "configure" | "billing" | "confirm";
|
||||
|
||||
const STEPS: Step[] = ["welcome", "configure", "billing", "confirm"];
|
||||
|
||||
const DEFAULT_SOUL = `# AI Assistant
|
||||
|
||||
You are a helpful AI assistant for {company}. You are professional, concise, and friendly.
|
||||
|
||||
## Guidelines
|
||||
- Answer questions accurately and helpfully
|
||||
- If you don't know something, say so
|
||||
- Keep responses clear and to the point
|
||||
- Respect privacy and confidentiality
|
||||
`;
|
||||
|
||||
const AVAILABLE_PACKAGES = [
|
||||
"telegram",
|
||||
"discord",
|
||||
"email",
|
||||
"web-search",
|
||||
"document-processing",
|
||||
];
|
||||
|
||||
interface WizardProps {
|
||||
orgName: string;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
const t = useTranslations("onboarding");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
const [step, setStep] = useState<Step>("welcome");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [config, setConfig] = useState({
|
||||
agentName: "Assistant",
|
||||
soulMd: DEFAULT_SOUL.replace("{company}", orgName),
|
||||
packages: [] as string[],
|
||||
billingAddress: {
|
||||
company: orgName,
|
||||
street: "",
|
||||
city: "",
|
||||
postalCode: "",
|
||||
country: "CH",
|
||||
},
|
||||
billingNotes: "",
|
||||
});
|
||||
|
||||
const stepIndex = STEPS.indexOf(step);
|
||||
|
||||
const goNext = () => {
|
||||
if (stepIndex < STEPS.length - 1) setStep(STEPS[stepIndex + 1]);
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (stepIndex > 0) setStep(STEPS[stepIndex - 1]);
|
||||
};
|
||||
|
||||
const togglePackage = (pkg: string) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packages: prev.packages.includes(pkg)
|
||||
? prev.packages.filter((p) => p !== pkg)
|
||||
: [...prev.packages, pkg],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSubmitting(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/onboarding", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Submission failed");
|
||||
}
|
||||
|
||||
onComplete();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Step indicator
|
||||
const StepIndicator = () => (
|
||||
<div className="flex items-center justify-center gap-2 mb-8">
|
||||
{STEPS.map((s, i) => (
|
||||
<div key={s} className="flex items-center gap-2">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full transition-colors ${
|
||||
i <= stepIndex ? "bg-accent" : "bg-border"
|
||||
}`}
|
||||
/>
|
||||
{i < STEPS.length - 1 && (
|
||||
<div
|
||||
className={`h-px w-8 transition-colors ${
|
||||
i < stepIndex ? "bg-accent" : "bg-border"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-xl mx-auto">
|
||||
<StepIndicator />
|
||||
|
||||
{/* Step: Welcome */}
|
||||
{step === "welcome" && (
|
||||
<Card className="animate-in">
|
||||
<div className="text-center py-4">
|
||||
<div className="h-14 w-14 rounded-xl bg-accent/15 flex items-center justify-center mx-auto mb-4">
|
||||
<svg
|
||||
className="h-7 w-7 text-accent"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="font-display text-xl font-semibold text-text-primary mb-2">
|
||||
{t("welcomeTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary max-w-sm mx-auto mb-6">
|
||||
{t("welcomeDescription")}
|
||||
</p>
|
||||
<div className="space-y-2 text-left max-w-sm mx-auto mb-6">
|
||||
{["swissHosted", "privacy", "customizable"].map((key) => (
|
||||
<div key={key} className="flex items-start gap-2">
|
||||
<span className="text-accent mt-0.5 text-sm">✓</span>
|
||||
<span className="text-sm text-text-secondary">
|
||||
{t(`welcomeFeature_${key}`)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={goNext}
|
||||
className="py-2 px-6 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-dim transition-colors"
|
||||
>
|
||||
{t("getStarted")}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step: Configure */}
|
||||
{step === "configure" && (
|
||||
<Card className="animate-in">
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-1">
|
||||
{t("configureTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary mb-6">
|
||||
{t("configureDescription")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
|
||||
{t("agentName")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.agentName}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({ ...prev, agentName: e.target.value }))
|
||||
}
|
||||
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
|
||||
{t("soulMd")}
|
||||
</label>
|
||||
<textarea
|
||||
value={config.soulMd}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({ ...prev, soulMd: e.target.value }))
|
||||
}
|
||||
rows={8}
|
||||
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary font-mono text-xs focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors resize-y"
|
||||
/>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{t("soulMdHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-2">
|
||||
{t("packages")}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{AVAILABLE_PACKAGES.map((pkg) => (
|
||||
<button
|
||||
key={pkg}
|
||||
type="button"
|
||||
onClick={() => togglePackage(pkg)}
|
||||
className={`text-left px-3 py-2 border rounded-lg text-xs transition-colors ${
|
||||
config.packages.includes(pkg)
|
||||
? "border-accent bg-accent/10 text-accent"
|
||||
: "border-border bg-surface-2 text-text-secondary hover:border-accent/40"
|
||||
}`}
|
||||
>
|
||||
{pkg}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{t("packagesHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<button
|
||||
onClick={goBack}
|
||||
className="py-2 px-4 text-sm text-text-secondary hover:text-text-primary transition-colors"
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
<button
|
||||
onClick={goNext}
|
||||
className="py-2 px-6 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-dim transition-colors"
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step: Billing */}
|
||||
{step === "billing" && (
|
||||
<Card className="animate-in">
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-1">
|
||||
{t("billingTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary mb-6">
|
||||
{t("billingDescription")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
|
||||
{t("billingCompany")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.billingAddress.company}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
billingAddress: {
|
||||
...prev.billingAddress,
|
||||
company: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
|
||||
{t("billingStreet")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.billingAddress.street}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
billingAddress: {
|
||||
...prev.billingAddress,
|
||||
street: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
|
||||
{t("billingPostalCode")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.billingAddress.postalCode}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
billingAddress: {
|
||||
...prev.billingAddress,
|
||||
postalCode: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
|
||||
{t("billingCity")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.billingAddress.city}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
billingAddress: {
|
||||
...prev.billingAddress,
|
||||
city: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
|
||||
{t("billingCountry")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.billingAddress.country}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
billingAddress: {
|
||||
...prev.billingAddress,
|
||||
country: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
|
||||
{t("billingNotes")}
|
||||
</label>
|
||||
<textarea
|
||||
value={config.billingNotes}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
billingNotes: e.target.value,
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
placeholder={t("billingNotesPlaceholder")}
|
||||
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors resize-y"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<button
|
||||
onClick={goBack}
|
||||
className="py-2 px-4 text-sm text-text-secondary hover:text-text-primary transition-colors"
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
<button
|
||||
onClick={goNext}
|
||||
className="py-2 px-6 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-dim transition-colors"
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step: Confirm */}
|
||||
{step === "confirm" && (
|
||||
<Card className="animate-in">
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-1">
|
||||
{t("confirmTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary mb-6">
|
||||
{t("confirmDescription")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-surface-2 border border-border rounded-lg p-4 space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-text-muted">{t("agentName")}</span>
|
||||
<span className="text-text-primary font-mono">
|
||||
{config.agentName}
|
||||
</span>
|
||||
</div>
|
||||
{config.packages.length > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-text-muted">{t("packages")}</span>
|
||||
<div className="flex flex-wrap gap-1 justify-end">
|
||||
{config.packages.map((pkg) => (
|
||||
<span
|
||||
key={pkg}
|
||||
className="text-xs font-mono bg-accent/10 text-accent border border-accent/20 rounded-full px-2 py-0.5"
|
||||
>
|
||||
{pkg}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{config.billingAddress.company && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-text-muted">{t("billingCompany")}</span>
|
||||
<span className="text-text-primary">
|
||||
{config.billingAddress.company}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{config.billingAddress.city && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-text-muted">{t("billingCity")}</span>
|
||||
<span className="text-text-primary">
|
||||
{config.billingAddress.postalCode}{" "}
|
||||
{config.billingAddress.city}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-text-muted">
|
||||
{t("confirmNote")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-xs text-red-400 bg-red-400/10 border border-red-400/20 rounded-lg px-3 py-2 mt-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<button
|
||||
onClick={goBack}
|
||||
className="py-2 px-4 text-sm text-text-secondary hover:text-text-primary transition-colors"
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
className="py-2.5 px-6 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-dim transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{submitting ? tCommon("loading") : t("submitRequest")}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user