All the UI fixes for now
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { PACKAGE_CATALOG, type PackageDef } from "@/lib/packages";
|
||||
|
||||
type Step = "welcome" | "configure" | "billing" | "confirm";
|
||||
|
||||
@@ -19,13 +20,10 @@ You are a helpful AI assistant for {company}. You are professional, concise, and
|
||||
- Respect privacy and confidentiality
|
||||
`;
|
||||
|
||||
const AVAILABLE_PACKAGES = [
|
||||
"telegram",
|
||||
"discord",
|
||||
"email",
|
||||
"web-search",
|
||||
"document-processing",
|
||||
];
|
||||
const CATEGORIES = [
|
||||
{ key: "channel" as const, labelKey: "categories.channels" },
|
||||
{ key: "skill" as const, labelKey: "categories.skills" },
|
||||
] as const;
|
||||
|
||||
interface WizardProps {
|
||||
orgName: string;
|
||||
@@ -34,6 +32,7 @@ interface WizardProps {
|
||||
|
||||
export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
const t = useTranslations("onboarding");
|
||||
const tPkg = useTranslations("packages");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
const [step, setStep] = useState<Step>("welcome");
|
||||
@@ -54,6 +53,15 @@ export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
billingNotes: "",
|
||||
});
|
||||
|
||||
// Per-package collected secrets: { "telegram": { "bot-token": "123:ABC" }, ... }
|
||||
const [packageSecrets, setPackageSecrets] = useState<
|
||||
Record<string, Record<string, string>>
|
||||
>({});
|
||||
// Per-package disclaimer acceptance
|
||||
const [disclaimerAccepted, setDisclaimerAccepted] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
const stepIndex = STEPS.indexOf(step);
|
||||
|
||||
const goNext = () => {
|
||||
@@ -64,13 +72,52 @@ export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
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 togglePackage = useCallback((pkgId: string) => {
|
||||
setConfig((prev) => {
|
||||
const removing = prev.packages.includes(pkgId);
|
||||
if (removing) {
|
||||
setPackageSecrets((s) => {
|
||||
const next = { ...s };
|
||||
delete next[pkgId];
|
||||
return next;
|
||||
});
|
||||
setDisclaimerAccepted((d) => {
|
||||
const next = { ...d };
|
||||
delete next[pkgId];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
packages: removing
|
||||
? prev.packages.filter((p) => p !== pkgId)
|
||||
: [...prev.packages, pkgId],
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateSecret = useCallback(
|
||||
(pkgId: string, key: string, value: string) => {
|
||||
setPackageSecrets((prev) => ({
|
||||
...prev,
|
||||
[pkgId]: { ...(prev[pkgId] || {}), [key]: value },
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Validate that all secret-requiring enabled packages have complete credentials
|
||||
const packageCredentialsValid = (): boolean => {
|
||||
for (const pkgId of config.packages) {
|
||||
const def = PACKAGE_CATALOG.find((p) => p.id === pkgId);
|
||||
if (!def?.requiresSecrets) continue;
|
||||
const secrets = packageSecrets[pkgId] || {};
|
||||
for (const field of def.secrets || []) {
|
||||
if (!secrets[field.key]?.trim()) return false;
|
||||
}
|
||||
if (def.disclaimerKey && !disclaimerAccepted[pkgId]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -78,10 +125,25 @@ export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
setError("");
|
||||
|
||||
try {
|
||||
// Build secrets payload — only for packages that require them
|
||||
const secretsPayload: Record<string, Record<string, string>> = {};
|
||||
for (const pkgId of config.packages) {
|
||||
const def = PACKAGE_CATALOG.find((p) => p.id === pkgId);
|
||||
if (def?.requiresSecrets && packageSecrets[pkgId]) {
|
||||
secretsPayload[pkgId] = packageSecrets[pkgId];
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch("/api/onboarding", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(config),
|
||||
body: JSON.stringify({
|
||||
...config,
|
||||
packageSecrets:
|
||||
Object.keys(secretsPayload).length > 0
|
||||
? secretsPayload
|
||||
: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -212,26 +274,151 @@ export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Packages — grouped by category */}
|
||||
<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>
|
||||
|
||||
{CATEGORIES.map(({ key, labelKey }) => {
|
||||
const packages = PACKAGE_CATALOG.filter(
|
||||
(p) => p.category === key
|
||||
);
|
||||
if (packages.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={key} className="mb-4">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-text-muted/70 mb-1.5">
|
||||
{tPkg(labelKey)}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{packages.map((pkg) => {
|
||||
const isSelected = config.packages.includes(pkg.id);
|
||||
const secrets = packageSecrets[pkg.id] || {};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={pkg.id}
|
||||
className={`border rounded-lg overflow-hidden transition-colors ${
|
||||
isSelected
|
||||
? "border-accent bg-accent/5"
|
||||
: "border-border bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
{/* Toggle row */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePackage(pkg.id)}
|
||||
className="w-full flex items-center justify-between px-3 py-2.5 cursor-pointer hover:bg-surface-3/30 transition-colors"
|
||||
>
|
||||
<div className="text-left">
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
isSelected
|
||||
? "text-accent"
|
||||
: "text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
{pkg.name}
|
||||
</span>
|
||||
{pkg.requiresSecrets && (
|
||||
<span className="ml-1.5 text-[10px] text-text-muted">
|
||||
({tPkg("requiresApiKey")})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`shrink-0 ml-3 h-5 w-9 rounded-full transition-colors ${
|
||||
isSelected ? "bg-accent" : "bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`h-4 w-4 rounded-full bg-white shadow-sm mt-0.5 transition-transform ${
|
||||
isSelected
|
||||
? "translate-x-4"
|
||||
: "translate-x-0.5"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Inline credential inputs — expand when selected + requires secrets */}
|
||||
{isSelected && pkg.requiresSecrets && (
|
||||
<div className="border-t border-border px-3 py-3 space-y-3 bg-surface-1/50">
|
||||
{pkg.instructionsKey && (
|
||||
<div className="bg-surface-2 border border-border rounded-lg p-3 text-xs text-text-secondary leading-relaxed whitespace-pre-line">
|
||||
{tPkg(
|
||||
pkg.instructionsKey.replace(
|
||||
"packages.",
|
||||
""
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(pkg.secrets || []).map((field) => (
|
||||
<label key={field.key} className="block">
|
||||
<span className="text-xs text-text-secondary mb-1 block">
|
||||
{tPkg(
|
||||
field.labelKey.replace("packages.", "")
|
||||
)}
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={tPkg(
|
||||
field.placeholderKey.replace(
|
||||
"packages.",
|
||||
""
|
||||
)
|
||||
)}
|
||||
value={secrets[field.key] || ""}
|
||||
onChange={(e) =>
|
||||
updateSecret(
|
||||
pkg.id,
|
||||
field.key,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
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"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
|
||||
{pkg.disclaimerKey && (
|
||||
<label className="flex items-start gap-2 text-xs text-text-secondary">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
disclaimerAccepted[pkg.id] || false
|
||||
}
|
||||
onChange={(e) =>
|
||||
setDisclaimerAccepted((prev) => ({
|
||||
...prev,
|
||||
[pkg.id]: e.target.checked,
|
||||
}))
|
||||
}
|
||||
className="mt-0.5 accent-accent"
|
||||
/>
|
||||
<span>
|
||||
{tPkg(
|
||||
pkg.disclaimerKey.replace(
|
||||
"packages.",
|
||||
""
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{t("packagesHint")}
|
||||
</p>
|
||||
@@ -247,7 +434,8 @@ export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
</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"
|
||||
disabled={!packageCredentialsValid()}
|
||||
className="py-2 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"
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
@@ -436,9 +624,23 @@ export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{config.packages.some((id) =>
|
||||
PACKAGE_CATALOG.find((p) => p.id === id)?.requiresSecrets
|
||||
) && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-text-muted">
|
||||
{t("credentialsProvided")}
|
||||
</span>
|
||||
<span className="text-emerald-400 text-xs font-medium">
|
||||
✓
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{config.billingAddress.company && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-text-muted">{t("billingCompany")}</span>
|
||||
<span className="text-text-muted">
|
||||
{t("billingCompany")}
|
||||
</span>
|
||||
<span className="text-text-primary">
|
||||
{config.billingAddress.company}
|
||||
</span>
|
||||
@@ -455,9 +657,7 @@ export function OnboardingWizard({ orgName, onComplete }: WizardProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-text-muted">
|
||||
{t("confirmNote")}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">{t("confirmNote")}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user