256 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|