ui(onboarding): flat tab navigation + fix vertical-stacked button text

Onboarding was a 7-step linear wizard, same information hierarchy as
Settings but behaving like a modal. Replace with the SettingsShell tab
pattern: one row of six tabs (nodecheck / providers / company /
modelpool / executionmode / mission), free navigation, per-tab
completion ticks derived from the wizard's own validation rules.

Sub-fixes on the same pass:
- ActionButton: add whitespace-nowrap + shrink-0 so "选择" never
  stacks vertically on narrow containers. Global — every caller gets
  the correct behavior
- CompanyStep: CEO Runtime Provider/Model go vertical (space-y-3
  instead of grid-cols-2) so long names like "Fireworks AI" /
  "Kimi K2.5 Turbo Preview 0911" stay readable; workspace-path Input
  wrapped in min-w-0 flex-1 container so it yields, not the button
- MarkdownBody: render react-markdown in chat bubbles (was plain text)

i18n: added onboarding.stepsAria for the tablist label.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-18 21:38:11 -04:00
1 parent ff5e22092a
commit c83d670050
16 files changed
+352 -60

No files matched your search

@@ -16,6 +16,7 @@ export function ModelPoolStep({ onBack, onContinue }: ModelPoolStepProps) {
const {
connectorModelPools,
onToggleConnectorModel,
onSetAllConnectorModels,
selectedConnectorId,
selectedConnectorEnabledModels,
totalEnabledModels,
@@ -44,31 +45,54 @@ export function ModelPoolStep({ onBack, onContinue }: ModelPoolStepProps) {
<div className="space-y-3">
{connectorModelPools.map(({ connector, models }) => {
const enabledCount = models.filter((model) => model.enabled).length;
const allEnabled = models.length > 0 && enabledCount === models.length;
// One action, state-aware: when everything is on the next useful
// move is "disable all"; otherwise the user almost always wants
// "enable all". Two buttons would compete for the user's decision
// for no real benefit.
const bulkAction = allEnabled
? { label: t("models.disableAll"), target: false }
: { label: t("models.enableAll"), target: true };
return (
<div key={connector.id} className="rounded-lg border border-border bg-background/50 p-4">
<div className="mb-3 flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-semibold text-foreground">
{connector.id.startsWith("claude_compat") && connector.notes ? connector.notes : connector.label}
{/* Row 1 — identity + health. Title has room to breathe. */}
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-[13px] font-semibold text-foreground">
{connector.id.startsWith("claude_compat") && connector.notes ? connector.notes : connector.label}
</span>
{connector.id === selectedConnectorId ? (
<span className="shrink-0 whitespace-nowrap rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.06em] text-primary">
{t("models.ceoRuntime")}
</span>
{connector.id === selectedConnectorId ? (
<span className="rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.06em] text-primary">
{t("models.ceoRuntime")}
</span>
) : null}
</div>
<div className="mt-1 text-[11px] text-muted-foreground">
{t("models.poolSummary").replace("{enabled}", String(enabledCount)).replace("{total}", String(models.length || 0))}
</div>
) : null}
</div>
<StatusBadge status={connector.status} />
</div>
<ModelToggleList
models={models}
onToggle={(modelId) => void onToggleConnectorModel(connector.id, modelId)}
canRemove={false}
/>
{/* Row 2 — stats + bulk action. Only rendered when there is */}
{/* more than one model (single-model providers have nothing */}
{/* meaningful to bulk-toggle). */}
<div className="mt-2 flex items-center justify-between gap-3 text-[11px] text-muted-foreground">
<span>
{t("models.poolSummary").replace("{enabled}", String(enabledCount)).replace("{total}", String(models.length || 0))}
</span>
{models.length > 1 ? (
<button
type="button"
onClick={() => onSetAllConnectorModels(connector.id, bulkAction.target)}
className="whitespace-nowrap rounded-md border border-border bg-background/60 px-2 py-1 text-[11px] text-muted-foreground transition hover:border-foreground/40 hover:text-foreground"
>
{bulkAction.label}
</button>
) : null}
</div>
<div className="mt-3">
<ModelToggleList
models={models}
onToggle={(modelId) => void onToggleConnectorModel(connector.id, modelId)}
canRemove={false}
/>
</div>
</div>
);
})}