import { Fragment, useRef, useState } from 'react'
import {
useConfig, useSaveConfig, useDeleteConfigOverride, useEnv,
useImportEnv, downloadEnvExport,
} from '../../api/queries'
import EmptyState from '../../components/shared/EmptyState'
import ConfirmDialog from '../../components/shared/ConfirmDialog'
import type { EnvImportResponse, EnvImportError } from '../../api/types'
import { ManagedBackendsSection } from './ManagedBackendsSection'
const RESTART_KEY = 'env_import_pending_restart'
function restartPending() {
return sessionStorage.getItem(RESTART_KEY) === '1'
}
export default function Settings({ configured = true }: { configured?: boolean }) {
const { data: cfg, isLoading, error } = useConfig()
const { data: envData } = useEnv()
const save = useSaveConfig()
const del = useDeleteConfigOverride()
const importEnv = useImportEnv()
const fileRef = useRef<HTMLInputElement>(null)
const [form, setForm] = useState<Record<string, string>>({})
const [importResult, setImportResult] = useState<EnvImportResponse | null>(null)
const [importError, setImportError] = useState<EnvImportError | null>(null)
const [exportError, setExportError] = useState<string | null>(null)
const [showRestartBanner, setShowRestartBanner] = useState(restartPending)
const [pendingReset, setPendingReset] = useState<string | null>(null)
function doReset() {
if (!pendingReset) return Promise.resolve()
const key = pendingReset
return del.mutateAsync(key).then(() => undefined)
}
function handleSave(key: string, currentValue: string) {
save.mutate({ [key]: form[key] ?? currentValue })
}
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
setImportResult(null)
setImportError(null)
importEnv.mutate(file, {
onSuccess(data) {
setImportResult(data)
sessionStorage.setItem(RESTART_KEY, '1')
setShowRestartBanner(true)
},
onError(err) {
// Try to parse hard_errors from the response body
try {
const parsed = JSON.parse(err.message) as EnvImportError
if (parsed.hard_errors) {
setImportError(parsed)
return
}
} catch {
// fall through to generic error
}
setImportError({ hard_errors: [err.message], warnings: [] })
},
})
// Reset file input so the same file can be re-selected after fixing issues
if (fileRef.current) fileRef.current.value = ''
}
async function handleExport() {
setExportError(null)
try {
await downloadEnvExport()
} catch (err) {
setExportError(err instanceof Error ? err.message : String(err))
}
}
function dismissRestartBanner() {
sessionStorage.removeItem(RESTART_KEY)
setShowRestartBanner(false)
}
return (
<div>
{/* Managed backends — always shown first */}
<ManagedBackendsSection />
{/* Getting-started notice — shown when no backend is configured */}
{!configured && (
<div style={{ marginBottom: 20, padding: '12px 16px', border: '1px solid var(--border)', borderLeft: '3px solid var(--warn)', borderRadius: 'var(--r)', fontSize: 13 }}>
<div style={{ fontWeight: 600, marginBottom: 8 }}>No proxy configuration found — nothing to forward requests to.</div>
<div style={{ marginBottom: 10 }}>
The proxy needs a backend endpoint (where to forward) and a listen port (where to accept).
LISTEN_PORT defaults to 3000. Create a <span className="mono">.anyllm.env</span> and import it below,
or pass it at startup: <span className="mono">anyllm-proxy --webui --env-file .anyllm.env</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
<div>
<div style={{ fontWeight: 600, marginBottom: 4, fontSize: 12 }}>OpenAI</div>
<pre style={{ margin: 0, padding: '6px 10px', background: 'var(--surface-2)', borderRadius: 'var(--r)', fontSize: 11, overflowX: 'auto' }}>
{`OPENAI_API_KEY=sk-...
PROXY_API_KEYS=my-key`}
</pre>
</div>
<div>
<div style={{ fontWeight: 600, marginBottom: 4, fontSize: 12 }}>Ollama / local LLM</div>
<pre style={{ margin: 0, padding: '6px 10px', background: 'var(--surface-2)', borderRadius: 'var(--r)', fontSize: 11, overflowX: 'auto' }}>
{`OPENAI_BASE_URL=http://localhost:11434/v1
PROXY_OPEN_RELAY=true`}
</pre>
</div>
<div>
<div style={{ fontWeight: 600, marginBottom: 4, fontSize: 12 }}>OpenRouter / custom</div>
<pre style={{ margin: 0, padding: '6px 10px', background: 'var(--surface-2)', borderRadius: 'var(--r)', fontSize: 11, overflowX: 'auto' }}>
{`OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY=sk-or-...
PROXY_API_KEYS=my-key`}
</pre>
</div>
</div>
</div>
)}
{/* Restart-required banner — shown after a successful import */}
{showRestartBanner && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, padding: '8px 12px', background: 'var(--warn-dim)', borderLeft: '3px solid var(--warn)', borderRadius: 'var(--r)', fontSize: 13 }}>
<span>Restart the proxy for imported env vars to take effect.</span>
<button className="btn btn-secondary btn-sm" onClick={dismissRestartBanner}>Dismiss</button>
</div>
)}
{/* Env file import / export */}
<div style={{ marginBottom: 24 }}>
<div className="section-label" style={{ marginBottom: 8 }}>Env File</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input
ref={fileRef}
type="file"
accept=".env,.anyllm.env,text/plain"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
className="btn btn-secondary btn-sm"
onClick={() => fileRef.current?.click()}
disabled={importEnv.isPending}
>
{importEnv.isPending ? 'Importing…' : 'Import .anyllm.env'}
</button>
<button className="btn btn-secondary btn-sm" onClick={handleExport}>
Export .anyllm.env
</button>
</div>
{/* Import success */}
{importResult && (
<div style={{ marginTop: 10 }}>
<div className="dim" style={{ marginBottom: 4 }}>
{importResult.applied} variable{importResult.applied !== 1 ? 's' : ''} imported.
{importResult.warnings.length === 0 && ' No issues.'}
</div>
{importResult.warnings.length > 0 && (
<div style={{ marginTop: 8, padding: '8px 12px', background: 'var(--warn-dim)', borderLeft: '3px solid var(--warn)', borderRadius: 'var(--r)', fontSize: 12 }}>
<div style={{ fontWeight: 600, marginBottom: 4 }}>Warnings</div>
{importResult.warnings.map((w, i) => (
<div key={i} className="mono" style={{ fontSize: 12 }}>
{w.line != null && <span className="dim">[line {w.line}] </span>}
{w.key && <span>{w.key}: </span>}
{w.message}
</div>
))}
</div>
)}
</div>
)}
{/* Import hard error */}
{importError && (
<div style={{ marginTop: 10, padding: '8px 12px', background: 'var(--err-dim)', borderLeft: '3px solid var(--err)', borderRadius: 'var(--r)', fontSize: 12 }}>
<div style={{ fontWeight: 600, marginBottom: 4 }}>Import rejected</div>
{importError.hard_errors.map((e, i) => (
<div key={i} className="mono" style={{ fontSize: 12 }}>{e}</div>
))}
{importError.warnings.length > 0 && (
<>
<div style={{ fontWeight: 600, marginTop: 8, marginBottom: 4 }}>Warnings (from partial parse)</div>
{importError.warnings.map((w, i) => (
<div key={i} className="mono" style={{ fontSize: 12 }}>
{w.line != null && <span className="dim">[line {w.line}] </span>}
{w.message}
</div>
))}
</>
)}
</div>
)}
{/* Export error */}
{exportError && (
<div style={{ marginTop: 10, padding: '8px 12px', background: 'var(--err-dim)', borderLeft: '3px solid var(--err)', borderRadius: 'var(--r)', fontSize: 12 }}>
Export failed: {exportError}
</div>
)}
</div>
<EmptyState loading={isLoading} error={error?.message} />
{cfg && (
<div>
{cfg.entries.map((entry) => {
const inputId = `cfg-${entry.key}`
return (
<div className="form-group" key={entry.key}>
<label className="form-label" htmlFor={inputId}>{entry.key}</label>
<div className="form-row">
<input
id={inputId}
name={entry.key}
value={form[entry.key] ?? entry.value}
onChange={(e) => setForm((f) => ({ ...f, [entry.key]: e.target.value }))}
/>
<button className="btn btn-primary btn-sm" onClick={() => handleSave(entry.key, entry.value)}>Save</button>
<button className="btn btn-secondary btn-sm" onClick={() => setPendingReset(entry.key)}>Reset</button>
</div>
</div>
)
})}
</div>
)}
{envData && (
<div className="readonly-section" style={{ marginTop: 16 }}>
<div className="section-label">Environment</div>
<div style={{ display: 'grid', gridTemplateColumns: '220px 1fr', gap: '4px 12px', marginTop: 8, fontSize: 12 }}>
{Object.entries(envData).map(([k, v]) => (
<Fragment key={k}>
<span className="dim">{k}</span>
<span className="mono">{v}</span>
</Fragment>
))}
</div>
</div>
)}
<ConfirmDialog
open={pendingReset !== null}
onClose={() => setPendingReset(null)}
onConfirm={doReset}
title="Reset override?"
message={
<>
Reset override for <span className="mono">{pendingReset}</span>? The runtime value will revert
to the env-file or default. Active connections are not affected.
</>
}
confirmLabel="Reset"
variant="primary"
/>
</div>
)
}