ds2api / webui /src /features /apiTester /ConfigPanel.jsx
huggeu's picture
Upload 532 files
8d3471e verified
import {
ChevronDown,
MessageSquare,
Cpu,
Search as SearchIcon,
Terminal,
Zap,
ToggleLeft,
ToggleRight
} from 'lucide-react'
import clsx from 'clsx'
import { maskSecret } from '../../utils/maskSecret'
export default function ConfigPanel({
t,
configExpanded,
setConfigExpanded,
models,
model,
setModel,
modelsLoaded,
streamingMode,
setStreamingMode,
selectedAccount,
setSelectedAccount,
accounts,
resolveAccountIdentifier,
apiKey,
setApiKey,
config,
customKeyActive,
customKeyManaged,
}) {
const iconMap = {
MessageSquare,
Cpu,
SearchIcon,
Terminal,
Zap,
ToggleLeft,
ToggleRight,
}
const selectedModel = models.find(m => m.id === model) || models[0]
const SelectedModelIcon = selectedModel ? (iconMap[selectedModel.icon] || MessageSquare) : MessageSquare
const defaultKeyPreview = maskSecret(config.keys?.[0])
const hasModels = models.length > 0
return (
<div className={clsx(
"lg:col-span-3 flex flex-col transition-all duration-300 ease-in-out z-20 min-h-0",
configExpanded ? "h-auto" : "h-14 lg:h-full"
)}>
<div className="bg-card border border-border rounded-xl flex flex-col h-full shadow-sm min-h-0 overflow-hidden">
<button
onClick={() => setConfigExpanded(!configExpanded)}
className="lg:hidden flex items-center justify-between p-4 w-full bg-muted/20 hover:bg-muted/30 transition-colors"
>
<div className="flex items-center gap-2.5 font-medium text-sm text-foreground">
<div className="p-1.5 rounded-md bg-transparent text-foreground">
<Terminal className="w-4 h-4" />
</div>
<span>{t('apiTester.config')}</span>
</div>
<div className={clsx("transition-transform duration-300 text-muted-foreground", configExpanded ? "rotate-180" : "") }>
<ChevronDown className="w-4 h-4" />
</div>
</button>
<div className={clsx(
"p-4 flex flex-col gap-5",
!configExpanded && "hidden lg:flex"
)}>
<div className="space-y-2 shrink-0">
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.modelLabel')}</label>
<div className="relative">
<select
className="w-full h-11 pl-3 pr-9 bg-secondary border border-border rounded-lg text-sm appearance-none focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all cursor-pointer hover:bg-muted/70 text-foreground disabled:opacity-60 disabled:cursor-not-allowed"
value={model}
onChange={e => setModel(e.target.value)}
disabled={!hasModels}
>
{hasModels ? models.map(m => (
<option key={m.id} value={m.id} className="bg-popover text-popover-foreground">
{m.name}
</option>
)) : (
<option value="" className="bg-popover text-popover-foreground">
{modelsLoaded ? t('apiTester.noModels') : t('apiTester.loadingModels')}
</option>
)}
</select>
<ChevronDown className="absolute right-2.5 top-3.5 w-4 h-4 text-muted-foreground pointer-events-none" />
</div>
{selectedModel ? (
<div className="mt-3 rounded-lg border border-border bg-muted/20 p-3">
<div className="flex items-start gap-3">
<div className={clsx(
"p-2 rounded-md shrink-0 border border-border bg-background/80",
selectedModel.color
)}>
<SelectedModelIcon className="w-4 h-4" />
</div>
<div className="min-w-0 flex-1">
<div className="font-medium text-sm text-foreground truncate">
{selectedModel.name}
</div>
<div className="text-[11px] text-muted-foreground mt-1 leading-relaxed">
{selectedModel.desc}
</div>
</div>
</div>
<p className="text-[11px] text-muted-foreground/70 mt-2">
{t('apiTester.modelPickerHint')}
</p>
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed border-border bg-muted/10 p-3 text-[11px] text-muted-foreground leading-relaxed">
{modelsLoaded ? t('apiTester.noModelsHint') : t('apiTester.loadingModelsHint')}
</div>
)}
</div>
<div className="space-y-2 shrink-0">
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.streamMode')}</label>
<button
onClick={() => setStreamingMode(!streamingMode)}
className={clsx(
"w-full flex items-center justify-between px-3 py-2 rounded-lg border transition-all duration-200",
streamingMode
? "bg-primary/10 border-primary/50 text-foreground"
: "bg-background border-border text-muted-foreground hover:bg-muted/50"
)}
>
<div className="flex items-center gap-2">
<div className={clsx("p-1.5 rounded-md", streamingMode ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground")}>
<Zap className="w-4 h-4" />
</div>
<span className="text-sm font-medium">{t('apiTester.streamMode')}</span>
</div>
{streamingMode ? <ToggleRight className="w-5 h-5 text-primary" /> : <ToggleLeft className="w-5 h-5 text-muted-foreground" />}
</button>
</div>
<div className="space-y-2 shrink-0">
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.accountSelector')}</label>
<div className="relative">
<select
className="w-full h-10 pl-3 pr-8 bg-secondary border border-border rounded-lg text-sm appearance-none focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all cursor-pointer hover:bg-muted"
value={selectedAccount}
onChange={e => setSelectedAccount(e.target.value)}
>
<option value="" className="bg-popover text-popover-foreground">{t('apiTester.autoRandom')}</option>
{accounts.map((acc, i) => {
const id = resolveAccountIdentifier(acc)
if (!id) return null
return (
<option key={i} value={id} className="bg-popover text-popover-foreground">
👤 {id}
</option>
)
})}
</select>
<ChevronDown className="absolute right-2.5 top-3 w-4 h-4 text-muted-foreground pointer-events-none" />
</div>
</div>
<div className="space-y-2 shrink-0">
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.apiKeyOptional')}</label>
<input
type="text"
autoComplete="off"
spellCheck={false}
className="w-full h-10 px-3 bg-muted/30 border border-border rounded-lg text-sm font-mono placeholder:text-muted-foreground/40 focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all"
placeholder={defaultKeyPreview ? t('apiTester.apiKeyDefault', { preview: defaultKeyPreview }) : t('apiTester.apiKeyPlaceholder')}
value={apiKey}
onChange={e => setApiKey(e.target.value)}
/>
{customKeyActive && (
<p className={clsx(
"text-[11px] mt-1",
customKeyManaged ? "text-emerald-600" : "text-amber-600"
)}>
{customKeyManaged ? t('apiTester.modeManaged') : t('apiTester.modeDirect')}
</p>
)}
</div>
</div>
</div>
</div>
)
}