copilot-api / desktop /src /pages /AdvancedConfigPage.tsx
imspsycho's picture
Initial upload from Google Colab
98c9143 verified
Raw
History Blame Contribute Delete
11.4 kB
import { useEffect, useRef, useState } from 'react'
import { useLanguage } from '../contexts/LanguageContext'
interface AdvancedConfigPageProps {
onBack: () => void
serverRunning: boolean
}
interface ModelMappingRow {
id: string
source: string
target: string
}
function createModelMappingRow(source = '', target = ''): ModelMappingRow {
return {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
source,
target,
}
}
function toRows(modelMappings: Record<string, string>): ModelMappingRow[] {
return Object.entries(modelMappings).map(([source, target]) =>
createModelMappingRow(source, target),
)
}
export default function AdvancedConfigPage({ onBack, serverRunning }: AdvancedConfigPageProps) {
const { t } = useLanguage()
const translationRef = useRef(t)
const [configPath, setConfigPath] = useState('')
const [rows, setRows] = useState<ModelMappingRow[]>([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [saveMessage, setSaveMessage] = useState('')
useEffect(() => {
translationRef.current = t
}, [t])
useEffect(() => {
if (serverRunning) {
return
}
setLoading(false)
setConfigPath('')
setRows([])
setSaveMessage('')
setError(t('advancedConfig.serverRequired'))
}, [serverRunning, t])
useEffect(() => {
if (!serverRunning) {
return
}
let cancelled = false
const loadModelMappings = async () => {
setLoading(true)
setError('')
try {
const result = await window.electronAPI.getModelMappingsConfig()
if (cancelled) {
return
}
setConfigPath(result.configPath)
setRows(toRows(result.modelMappings))
} catch (err) {
if (cancelled) {
return
}
setError(
`${translationRef.current('advancedConfig.loadFailed')}: ${(err as Error).message}`,
)
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
void loadModelMappings()
return () => {
cancelled = true
}
}, [serverRunning])
const handleRowChange = (
id: string,
field: 'source' | 'target',
value: string,
) => {
setRows((currentRows) =>
currentRows.map((row) =>
row.id === id ? { ...row, [field]: value } : row,
),
)
setError('')
setSaveMessage('')
}
const handleAddRow = () => {
setRows((currentRows) => [...currentRows, createModelMappingRow()])
setError('')
setSaveMessage('')
}
const handleRemoveRow = (id: string) => {
setRows((currentRows) => currentRows.filter((row) => row.id !== id))
setError('')
setSaveMessage('')
}
const buildModelMappings = (): Record<string, string> | null => {
const modelMappings: Record<string, string> = {}
for (const row of rows) {
if (!row.source && !row.target) {
continue
}
if (!row.source || !row.target) {
setError(t('advancedConfig.validationIncomplete'))
return null
}
if (Object.hasOwn(modelMappings, row.source)) {
setError(t('advancedConfig.validationDuplicate', { model: row.source }))
return null
}
modelMappings[row.source] = row.target
}
return modelMappings
}
const handleSave = async () => {
setError('')
setSaveMessage('')
const nextModelMappings = buildModelMappings()
if (!nextModelMappings) {
return
}
setSaving(true)
try {
await window.electronAPI.saveModelMappings(nextModelMappings)
setRows(toRows(nextModelMappings))
setSaveMessage(t('advancedConfig.saved'))
} catch (err) {
setError(`${t('advancedConfig.saveFailed')}: ${(err as Error).message}`)
} finally {
setSaving(false)
}
}
return (
<div className="min-h-full bg-slate-50 px-6 py-6">
<div className="mx-auto flex max-w-5xl flex-col gap-5">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="space-y-2">
<div className="inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1 text-[12px] font-medium text-slate-500 shadow-sm">
{t('header.advancedConfig')}
</div>
<div>
<h1 className="text-[24px] font-bold text-[#0f172a]">
{t('advancedConfig.title')}
</h1>
<p className="mt-1 max-w-3xl text-[13px] leading-relaxed text-slate-500">
{t('advancedConfig.subtitle')}
</p>
</div>
</div>
<button
onClick={onBack}
className="inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2 text-[13px] font-medium text-slate-600 shadow-sm transition-colors hover:bg-slate-100"
>
{t('advancedConfig.back')}
</button>
</div>
{error && (
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-[13px] text-red-700 shadow-sm">
{error}
</div>
)}
{saveMessage && !error && (
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-[13px] text-emerald-700 shadow-sm">
{saveMessage}
</div>
)}
<div className="rounded-[28px] border border-slate-200 bg-white p-5 shadow-sm">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-1.5">
<h2 className="text-[16px] font-semibold text-[#0f172a]">
{t('advancedConfig.modelMappingsTitle')}
</h2>
<p className="max-w-3xl text-[13px] leading-relaxed text-slate-500">
{t('advancedConfig.modelMappingsDesc')}
</p>
</div>
<button
onClick={handleAddRow}
disabled={!serverRunning}
className="inline-flex items-center justify-center rounded-xl bg-[#0f172a] px-4 py-2 text-[13px] font-semibold text-white transition-colors hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
>
{t('advancedConfig.addMapping')}
</button>
</div>
<div className="mt-4 grid gap-3 lg:grid-cols-[1.4fr_1fr]">
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
<div className="text-[12px] font-semibold uppercase tracking-[0.14em] text-slate-400">
{t('advancedConfig.scopeLabel')}
</div>
<p className="mt-2 text-[13px] leading-relaxed text-slate-600">
{t('advancedConfig.scopeNote')}
</p>
<p className="mt-2 text-[12px] leading-relaxed text-emerald-700">
{t('advancedConfig.restartNote')}
</p>
</div>
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
<div className="text-[12px] font-semibold uppercase tracking-[0.14em] text-slate-400">
{t('advancedConfig.configPath')}
</div>
<div className="mt-2 break-all rounded-xl bg-white px-3 py-2 font-mono text-[12px] text-slate-600 shadow-sm">
{configPath || '—'}
</div>
</div>
</div>
<div className="mt-5 space-y-3">
{loading ? (
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-[13px] text-slate-400">
{t('dashboard.loading')}
</div>
) : rows.length === 0 ? (
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-10 text-center">
<div className="text-[14px] font-semibold text-[#0f172a]">
{t('advancedConfig.emptyTitle')}
</div>
<p className="mx-auto mt-2 max-w-xl text-[13px] leading-relaxed text-slate-500">
{t('advancedConfig.emptyDescription')}
</p>
</div>
) : (
rows.map((row) => (
<div
key={row.id}
className="grid gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] lg:items-end"
>
<label className="block">
<div className="mb-1.5 text-[12px] font-medium text-slate-500">
{t('advancedConfig.sourceModel')}
</div>
<input
type="text"
value={row.source}
onChange={(event) =>
handleRowChange(row.id, 'source', event.target.value)
}
placeholder="claude-opus-4-7"
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-[13px] text-[#0f172a] placeholder-slate-300 transition-colors focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-300"
/>
</label>
<label className="block">
<div className="mb-1.5 text-[12px] font-medium text-slate-500">
{t('advancedConfig.targetModel')}
</div>
<input
type="text"
value={row.target}
onChange={(event) =>
handleRowChange(row.id, 'target', event.target.value)
}
placeholder="gpt-5-mini"
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-[13px] text-[#0f172a] placeholder-slate-300 transition-colors focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-300"
/>
</label>
<button
onClick={() => handleRemoveRow(row.id)}
className="inline-flex items-center justify-center rounded-xl border border-red-200 px-3 py-2 text-[13px] font-medium text-red-600 transition-colors hover:bg-red-50"
>
{t('advancedConfig.remove')}
</button>
</div>
))
)}
</div>
</div>
<div className="flex flex-col gap-3 rounded-[24px] border border-slate-200 bg-white px-5 py-4 shadow-sm md:flex-row md:items-center md:justify-between">
<p className="text-[13px] leading-relaxed text-slate-500">
{t('advancedConfig.saveHelp')}
</p>
<button
onClick={handleSave}
disabled={!serverRunning || loading || saving}
className="inline-flex items-center justify-center rounded-xl bg-[#0f172a] px-5 py-2.5 text-[13px] font-semibold text-white transition-colors hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
>
{saving ? t('settings.saving') : t('settings.save')}
</button>
</div>
</div>
</div>
)
}