Drop the Supabase backend and call the AI provider directly from the
browser. The endpoint URL, API key, and model name are stored in
localStorage and used for direct /chat/completions requests.
- src/lib/llm.ts: config persistence, direct fetch, JSON extraction,
shape validation, connection test
- src/lib/prompts.ts: full + partial-regeneration system prompts and
user-message builder
- src/lib/{api,supabase}.ts removed
- supabase/ directory removed
- @supabase/supabase-js dropped from package.json
- README updated to describe the standalone architecture and CORS caveats
- .gitignore: drop Supabase entries, exclude *.tsbuildinfo
245 lines
7.9 KiB
TypeScript
245 lines
7.9 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import { ArrowLeft, Eye, EyeOff, Loader2, Save, Plug } from "lucide-react";
|
|
import { ThemeToggle } from "../components/ThemeToggle";
|
|
import { useToast } from "../lib/toast";
|
|
import { getConfigDisplay, setConfig, testConnection } from "../lib/llm";
|
|
|
|
const DEFAULTS = {
|
|
api_endpoint: "",
|
|
api_key: "",
|
|
model_name: "MiniMax-M3",
|
|
};
|
|
|
|
export function SettingsPage() {
|
|
const toast = useToast();
|
|
const navigate = useNavigate();
|
|
|
|
const [endpoint, setEndpoint] = useState(DEFAULTS.api_endpoint);
|
|
const [model, setModel] = useState(DEFAULTS.model_name);
|
|
const [apiKey, setApiKey] = useState(""); // never pre-populated from storage
|
|
const [apiKeySet, setApiKeySet] = useState(false);
|
|
const [showKey, setShowKey] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [testing, setTesting] = useState(false);
|
|
|
|
// Load current (non-secret) values on mount.
|
|
useEffect(() => {
|
|
const cfg = getConfigDisplay();
|
|
setEndpoint(cfg.api_endpoint);
|
|
setModel(cfg.model_name);
|
|
setApiKeySet(cfg.api_key_set);
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
const onSave = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSaving(true);
|
|
try {
|
|
const result = setConfig({
|
|
api_endpoint: endpoint.trim(),
|
|
api_key: apiKey,
|
|
model_name: model.trim(),
|
|
});
|
|
setApiKey("");
|
|
setApiKeySet(result.api_key_set);
|
|
toast.success("Configuration saved");
|
|
} catch (err) {
|
|
toast.error(
|
|
err instanceof Error ? err.message : "Failed to save configuration",
|
|
);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const onTest = async () => {
|
|
setTesting(true);
|
|
// Persist current form values first so the test reflects them. setConfig
|
|
// preserves the existing key when the field is empty, so this is safe.
|
|
try {
|
|
const result = setConfig({
|
|
api_endpoint: endpoint.trim(),
|
|
api_key: apiKey,
|
|
model_name: model.trim(),
|
|
});
|
|
setApiKey("");
|
|
setApiKeySet(result.api_key_set);
|
|
} catch (err) {
|
|
setTesting(false);
|
|
toast.error(
|
|
err instanceof Error ? err.message : "Failed to save configuration",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await testConnection();
|
|
if (result.success) {
|
|
toast.success(`✅ ${result.message}`);
|
|
} else {
|
|
toast.error(`❌ ${result.message}`);
|
|
}
|
|
setTesting(false);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
<header className="sticky top-0 z-20 bg-bg/70 backdrop-blur border-b border-border">
|
|
<div className="max-w-md mx-auto px-4 h-14 flex items-center justify-between">
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate("/")}
|
|
className="btn-ghost p-2"
|
|
aria-label="Back"
|
|
>
|
|
<ArrowLeft className="w-5 h-5" />
|
|
</button>
|
|
<h1 className="text-sm font-semibold text-fg">API Configuration</h1>
|
|
<ThemeToggle />
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-md mx-auto px-4 mt-16">
|
|
<div className="card p-6">
|
|
{loading ? (
|
|
<div className="space-y-4 animate-pulse">
|
|
<div className="h-4 w-1/3 bg-bg-hover rounded" />
|
|
<div className="h-10 bg-bg-hover rounded" />
|
|
<div className="h-4 w-1/3 bg-bg-hover rounded" />
|
|
<div className="h-10 bg-bg-hover rounded" />
|
|
<div className="h-4 w-1/3 bg-bg-hover rounded" />
|
|
<div className="h-10 bg-bg-hover rounded" />
|
|
<div className="h-12 bg-bg-hover rounded" />
|
|
<div className="h-12 bg-bg-hover rounded" />
|
|
</div>
|
|
) : (
|
|
<form onSubmit={onSave} className="space-y-4">
|
|
<div>
|
|
<label
|
|
htmlFor="endpoint"
|
|
className="block text-xs uppercase tracking-wider text-fg-muted mb-1"
|
|
>
|
|
API Endpoint URL
|
|
</label>
|
|
<input
|
|
id="endpoint"
|
|
type="text"
|
|
className="input"
|
|
placeholder="https://api.minimax.chat/v1"
|
|
value={endpoint}
|
|
onChange={(e) => setEndpoint(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="apiKey"
|
|
className="block text-xs uppercase tracking-wider text-fg-muted mb-1"
|
|
>
|
|
API Key
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
id="apiKey"
|
|
type={showKey ? "text" : "password"}
|
|
className="input pr-10 font-mono"
|
|
placeholder={
|
|
apiKeySet
|
|
? "API key saved — enter a new one to replace it"
|
|
: "sk-..."
|
|
}
|
|
value={apiKey}
|
|
onChange={(e) => setApiKey(e.target.value)}
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowKey((s) => !s)}
|
|
className="absolute inset-y-0 right-0 flex items-center px-3 text-fg-muted hover:text-fg transition-colors"
|
|
aria-label={showKey ? "Hide API key" : "Show API key"}
|
|
tabIndex={-1}
|
|
>
|
|
{showKey ? (
|
|
<EyeOff className="w-4 h-4" />
|
|
) : (
|
|
<Eye className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
{apiKeySet && !apiKey && (
|
|
<p className="mt-1 text-xs text-emerald-400">
|
|
A key is currently saved.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="model"
|
|
className="block text-xs uppercase tracking-wider text-fg-muted mb-1"
|
|
>
|
|
Model Name
|
|
</label>
|
|
<input
|
|
id="model"
|
|
type="text"
|
|
className="input"
|
|
placeholder="MiniMax-M3"
|
|
value={model}
|
|
onChange={(e) => setModel(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={saving || testing}
|
|
className="btn-primary w-full py-3"
|
|
>
|
|
{saving ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Save className="w-4 h-4" />
|
|
)}
|
|
Save Configuration
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={onTest}
|
|
disabled={saving || testing}
|
|
className="btn-secondary w-full py-3"
|
|
>
|
|
{testing ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Plug className="w-4 h-4" />
|
|
)}
|
|
Test Connection
|
|
</button>
|
|
</form>
|
|
)}
|
|
|
|
<p className="mt-6 text-center text-xs text-fg-muted">
|
|
Your settings are stored in this browser's local storage. Do not use
|
|
this app on a device that other people have access to.
|
|
</p>
|
|
|
|
<div className="mt-4 text-center">
|
|
<Link
|
|
to="/"
|
|
className="text-xs text-fg-muted hover:text-fg transition-colors"
|
|
>
|
|
← Back to generator
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|