dashboard-nanobot/frontend/src/modules/onboarding/components/BotWizardBaseStep.tsx

256 lines
10 KiB
TypeScript

import { Settings2 } from 'lucide-react';
import type { Dispatch, SetStateAction } from 'react';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import { LucentSelect } from '../../../components/lucent/LucentSelect';
import { PasswordInput } from '../../../components/PasswordInput';
import { buildLlmProviderOptions } from '../../../utils/llmProviders';
import type { OnboardingChannelLabels, WizardLabels } from '../localeTypes';
import type { BotWizardForm } from '../types';
interface BotWizardBaseStepProps {
isZh: boolean;
ui: WizardLabels;
lc: OnboardingChannelLabels;
passwordToggleLabels: { show: string; hide: string };
form: BotWizardForm;
setForm: Dispatch<SetStateAction<BotWizardForm>>;
botIdStatus: 'idle' | 'checking' | 'available' | 'exists' | 'invalid';
botIdStatusText: string;
defaultSystemTimezone: string;
systemTimezoneOptions: Array<{ value: string; label?: string }>;
cpuCoresDraft: string;
memoryMbDraft: string;
storageGbDraft: string;
maxTokensDraft: string;
configuredChannelsLabel: string;
envEntries: Array<[string, string]>;
testResult: string;
isTestingProvider: boolean;
onProviderChange: (provider: string) => void;
onTestProvider: () => Promise<void>;
commitCpuCoresDraft: (raw: string) => void;
commitMemoryMbDraft: (raw: string) => void;
commitStorageGbDraft: (raw: string) => void;
commitMaxTokensDraft: (raw: string) => void;
setCpuCoresDraft: (value: string) => void;
setMemoryMbDraft: (value: string) => void;
setStorageGbDraft: (value: string) => void;
setMaxTokensDraft: (value: string) => void;
onOpenChannelModal: () => void;
onOpenToolsModal: () => void;
}
export function BotWizardBaseStep({
isZh,
ui,
lc,
passwordToggleLabels,
form,
setForm,
botIdStatus,
botIdStatusText,
defaultSystemTimezone,
systemTimezoneOptions,
cpuCoresDraft,
memoryMbDraft,
storageGbDraft,
maxTokensDraft,
configuredChannelsLabel,
envEntries,
testResult,
isTestingProvider,
onProviderChange,
onTestProvider,
commitCpuCoresDraft,
commitMemoryMbDraft,
commitStorageGbDraft,
commitMaxTokensDraft,
setCpuCoresDraft,
setMemoryMbDraft,
setStorageGbDraft,
setMaxTokensDraft,
onOpenChannelModal,
onOpenToolsModal,
}: BotWizardBaseStepProps) {
const providerOptions = buildLlmProviderOptions(form.llm_provider);
return (
<div className="bot-wizard-base-grid">
<div className="stack card bot-wizard-step-card">
<div className="section-mini-title">{ui.baseInfo}</div>
<input
className="input"
placeholder={ui.botIdPlaceholder}
value={form.id}
onChange={(e) => {
const normalized = e.target.value.replace(/[^A-Za-z0-9_]/g, '');
setForm((prev) => ({ ...prev, id: normalized }));
}}
/>
<div
className="field-label"
style={{
color:
botIdStatus === 'invalid' || botIdStatus === 'exists'
? 'var(--err)'
: botIdStatus === 'available'
? 'var(--ok)'
: 'var(--muted)',
}}
>
{botIdStatusText || ui.botIdHint}
</div>
<input
className="input"
placeholder={ui.botName}
value={form.name}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
/>
<div className="section-mini-title">{isZh ? '基础信息' : 'Basic Info'}</div>
<label className="field-label">{isZh ? '系统时区' : 'System Timezone'}</label>
<LucentSelect
value={form.system_timezone}
onChange={(e) => setForm((prev) => ({ ...prev, system_timezone: e.target.value }))}
>
<option value="" disabled>
{isZh ? '请选择系统时区' : 'Select system timezone'}
</option>
{systemTimezoneOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.value}
</option>
))}
</LucentSelect>
<div className="field-label">
{isZh ? '提示:系统时区保存后,在 Bot 重启后生效。' : 'Tip: timezone changes take effect after the bot restarts.'}
</div>
{defaultSystemTimezone ? (
<div className="field-label">
{isZh ? `系统默认值:${defaultSystemTimezone}` : `System default: ${defaultSystemTimezone}`}
</div>
) : null}
<div className="section-mini-title" style={{ marginTop: 10 }}>
{isZh ? '资源配额' : 'Resource Limits'}
</div>
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
<input
className="input"
type="number"
min="0"
max="16"
step="0.1"
value={cpuCoresDraft}
onChange={(e) => setCpuCoresDraft(e.target.value)}
onBlur={(e) => commitCpuCoresDraft(e.target.value)}
/>
<label className="field-label">{isZh ? '内存 (MB)' : 'Memory (MB)'}</label>
<input
className="input"
type="number"
min="0"
max="65536"
step="128"
value={memoryMbDraft}
onChange={(e) => setMemoryMbDraft(e.target.value)}
onBlur={(e) => commitMemoryMbDraft(e.target.value)}
/>
<label className="field-label">{isZh ? '存储 (GB)' : 'Storage (GB)'}</label>
<input
className="input"
type="number"
min="0"
max="1024"
step="1"
value={storageGbDraft}
onChange={(e) => setStorageGbDraft(e.target.value)}
onBlur={(e) => commitStorageGbDraft(e.target.value)}
/>
<div className="field-label">{isZh ? '提示:填写 0 表示不限制。' : 'Tip: value 0 means unlimited.'}</div>
</div>
<div className="stack card bot-wizard-step-card">
<div className="section-mini-title">{ui.modelAccess}</div>
<LucentSelect value={form.llm_provider} onChange={(e) => onProviderChange(e.target.value)}>
<option value="" disabled>
{isZh ? '请选择 Provider' : 'Select provider'}
</option>
{providerOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</LucentSelect>
<input className="input" placeholder={ui.modelNamePlaceholder} value={form.llm_model} onChange={(e) => setForm((prev) => ({ ...prev, llm_model: e.target.value }))} />
<PasswordInput className="input" placeholder="API Key" value={form.api_key} onChange={(e) => setForm((prev) => ({ ...prev, api_key: e.target.value }))} toggleLabels={passwordToggleLabels} />
<input className="input" placeholder="API Base" value={form.api_base} onChange={(e) => setForm((prev) => ({ ...prev, api_base: e.target.value }))} />
<button className="btn btn-secondary" onClick={() => void onTestProvider()} disabled={isTestingProvider}>
{isTestingProvider ? ui.testing : ui.test}
</button>
{testResult ? <div className="card bot-wizard-note-card">{testResult}</div> : null}
<div className="section-mini-title">{ui.modelParams}</div>
<div className="slider-row">
<label className="field-label">Temperature: {form.temperature.toFixed(2)}</label>
<input type="range" min="0" max="1" step="0.01" value={form.temperature} onChange={(e) => setForm((prev) => ({ ...prev, temperature: Number(e.target.value) }))} />
</div>
<div className="slider-row">
<label className="field-label">Top P: {form.top_p.toFixed(2)}</label>
<input type="range" min="0" max="1" step="0.01" value={form.top_p} onChange={(e) => setForm((prev) => ({ ...prev, top_p: Number(e.target.value) }))} />
</div>
<div className="slider-row token-input-row">
<label className="field-label" htmlFor="wizard-max-tokens">Max Tokens</label>
<input
id="wizard-max-tokens"
className="input token-number-input"
type="number"
step="1"
min="256"
max="32768"
value={maxTokensDraft}
onChange={(e) => setMaxTokensDraft(e.target.value)}
onBlur={(e) => commitMaxTokensDraft(e.target.value)}
/>
<div className="field-label">{ui.tokenRange}</div>
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{[4096, 8192, 16384, 32768].map((value) => (
<button
key={value}
className="btn btn-secondary btn-sm"
type="button"
onClick={() => {
setForm((prev) => ({ ...prev, max_tokens: value }));
setMaxTokensDraft(String(value));
}}
>
{value}
</button>
))}
</div>
</div>
<div className="stack card bot-wizard-step-card">
<div className="section-mini-title">{lc.wizardSectionTitle}</div>
<div className="card bot-wizard-note-card bot-wizard-channel-summary">
<div className="field-label">{lc.wizardSectionDesc}</div>
<div className="mono">{configuredChannelsLabel}</div>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={onOpenChannelModal} tooltip={lc.openManager} aria-label={lc.openManager}>
<Settings2 size={14} />
</LucentIconButton>
</div>
<div className="section-mini-title" style={{ marginTop: 6 }}>{ui.toolsConfig}</div>
<div className="card bot-wizard-note-card bot-wizard-channel-summary">
<div className="field-label">{ui.toolsDesc}</div>
<div className="mono">{envEntries.length > 0 ? envEntries.map(([key]) => key).join(', ') : ui.noEnvParams}</div>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={onOpenToolsModal} tooltip={ui.openToolsManager} aria-label={ui.openToolsManager}>
<Settings2 size={14} />
</LucentIconButton>
</div>
</div>
</div>
);
}