Some checks failed
Publish package to GitHub Packages / publish (push) Has been cancelled
Workstream D: a shared top bar for the SDC tool suite — app name (left), API settings cog, and the suite app directory (grid) — composed on the existing TopBar. Adds an ApiSettings dialog and sdc_api_key/sdc_api_endpoint credential helpers (shared once across all tools, with legacy-key migration). lucide-react becomes a peer dependency. Bump to 0.2.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import {
|
|
Dialog,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogContent,
|
|
DialogFooter,
|
|
} from '@/components/molecules/Dialog'
|
|
import { Button } from '@/components/atoms/Button'
|
|
import { Input } from '@/components/atoms/Input'
|
|
import { Alert } from '@/components/molecules/Alert'
|
|
import { Badge } from '@/components/atoms/Badge'
|
|
import {
|
|
getEndpoint,
|
|
getApiKey,
|
|
saveCredentials,
|
|
testConnection,
|
|
} from '@/lib/credentials'
|
|
|
|
export interface ApiSettingsProps {
|
|
open: boolean
|
|
onClose: () => void
|
|
/** Called after credentials are saved (e.g. to refresh a "no API key" badge). */
|
|
onSaved?: () => void
|
|
}
|
|
|
|
/**
|
|
* Shared API-settings dialog for the SDC tool suite. Reads/writes the suite-wide
|
|
* `sdc_api_endpoint` + `sdc_api_key` so the key is entered once across all tools.
|
|
*/
|
|
export function ApiSettings({ open, onClose, onSaved }: ApiSettingsProps) {
|
|
const [endpoint, setEndpoint] = useState('')
|
|
const [apiKey, setApiKey] = useState('')
|
|
const [status, setStatus] = useState<{ type: 'success' | 'error' | 'info'; message: string } | null>(null)
|
|
const [testing, setTesting] = useState(false)
|
|
const [models, setModels] = useState<string[]>([])
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setEndpoint(getEndpoint())
|
|
setApiKey(getApiKey())
|
|
setStatus(null)
|
|
setModels([])
|
|
}
|
|
}, [open])
|
|
|
|
function handleSave() {
|
|
if (!endpoint.trim()) {
|
|
setStatus({ type: 'error', message: 'Please enter a gateway endpoint.' })
|
|
return
|
|
}
|
|
if (!apiKey.trim()) {
|
|
setStatus({ type: 'error', message: 'Please enter an API key.' })
|
|
return
|
|
}
|
|
saveCredentials(endpoint, apiKey)
|
|
onSaved?.()
|
|
setStatus({ type: 'success', message: 'Credentials saved. They apply across all SDC tools.' })
|
|
}
|
|
|
|
async function handleTest() {
|
|
if (!endpoint.trim() || !apiKey.trim()) {
|
|
setStatus({ type: 'error', message: 'Enter an endpoint and key first.' })
|
|
return
|
|
}
|
|
saveCredentials(endpoint, apiKey)
|
|
onSaved?.()
|
|
setTesting(true)
|
|
setStatus({ type: 'info', message: 'Testing connection…' })
|
|
setModels([])
|
|
try {
|
|
const result = await testConnection(endpoint, apiKey)
|
|
setModels(result.models)
|
|
setStatus({
|
|
type: 'success',
|
|
message: 'Connected. ' + result.models.length + ' model(s) available.',
|
|
})
|
|
} catch (e) {
|
|
setStatus({ type: 'error', message: (e as Error).message })
|
|
} finally {
|
|
setTesting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} size="lg">
|
|
<DialogHeader onClose={onClose}>
|
|
<DialogTitle>API Settings</DialogTitle>
|
|
<DialogDescription>
|
|
Configure your gateway connection. This is shared across all SDC tools.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogContent>
|
|
<div className="space-y-4">
|
|
<Input
|
|
variant="stacked"
|
|
label="Gateway Endpoint"
|
|
description="The base URL of your LiteLLM gateway"
|
|
type="url"
|
|
placeholder="https://your-gateway.example.com"
|
|
value={endpoint}
|
|
onChange={(e) => setEndpoint(e.target.value)}
|
|
/>
|
|
<Input
|
|
variant="stacked"
|
|
label="API Key"
|
|
type="password"
|
|
placeholder="Paste your API key here"
|
|
value={apiKey}
|
|
onChange={(e) => setApiKey(e.target.value)}
|
|
/>
|
|
|
|
{status && (
|
|
<Alert
|
|
variant={status.type === 'success' ? 'success' : status.type === 'error' ? 'error' : 'info'}
|
|
title={status.type === 'success' ? 'Success' : status.type === 'error' ? 'Error' : ''}
|
|
onClose={() => setStatus(null)}
|
|
>
|
|
{status.message}
|
|
</Alert>
|
|
)}
|
|
|
|
{models.length > 0 && (
|
|
<div className="space-y-2">
|
|
<p className="text-small font-semibold text-text-secondary">Available Models</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{models.map((m) => (
|
|
<Badge key={m} variant="info-light">{m}</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
<DialogFooter>
|
|
<Button variant="secondary" onClick={handleTest} loading={testing}>
|
|
Test Connection
|
|
</Button>
|
|
<Button variant="primary" onClick={handleSave}>
|
|
Save
|
|
</Button>
|
|
</DialogFooter>
|
|
</Dialog>
|
|
)
|
|
}
|