| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- "use client";
- import { useState, useEffect } from "react";
- interface EnvVars {
- OPENAI_API_KEY: string;
- OPENAI_BASE_URL: string;
- OPENAI_TTS_API_KEY: string;
- OPENAI_TTS_BASE_URL: string;
- OPENAI_TTS_MODEL: string;
- FISH_AUDIO_API_KEY: string;
- MINIMAX_API_KEY: string;
- MINIMAX_GROUP_ID: string;
- ELEVENLABS_API_KEY: string;
- OSS_REGION: string;
- OSS_BUCKET: string;
- OSS_ACCESS_KEY_ID: string;
- OSS_ACCESS_KEY_SECRET: string;
- OSS_PUBLIC_DOMAIN: string;
- FEISHU_WEBHOOK_URL: string;
- FEISHU_WEBHOOK_SECRET: string;
- }
- const DEFAULT_ENV: EnvVars = {
- OPENAI_API_KEY: "",
- OPENAI_BASE_URL: "https://api.openai.com/v1",
- OPENAI_TTS_API_KEY: "",
- OPENAI_TTS_BASE_URL: "https://new-api.corp.shuidi.tech/v1",
- OPENAI_TTS_MODEL: "seed-tts-1.1",
- FISH_AUDIO_API_KEY: "",
- MINIMAX_API_KEY: "",
- MINIMAX_GROUP_ID: "",
- ELEVENLABS_API_KEY: "",
- OSS_REGION: "oss-cn-hangzhou",
- OSS_BUCKET: "",
- OSS_ACCESS_KEY_ID: "",
- OSS_ACCESS_KEY_SECRET: "",
- OSS_PUBLIC_DOMAIN: "",
- FEISHU_WEBHOOK_URL: "",
- FEISHU_WEBHOOK_SECRET: "",
- };
- export default function SettingsPage() {
- const [keys, setKeys] = useState<EnvVars>(DEFAULT_ENV);
- const [loaded, setLoaded] = useState(false);
- const [saving, setSaving] = useState(false);
- const [saved, setSaved] = useState(false);
- const [sources, setSources] = useState<{ name: string; url: string }[]>([]);
- useEffect(() => {
- fetch("/api/settings")
- .then((r) => r.json())
- .then((data) => {
- setKeys((prev) => ({ ...prev, ...data }));
- setLoaded(true);
- })
- .catch(() => setLoaded(true));
- fetch("/api/collect/sources")
- .then((r) => r.json())
- .then((data) => setSources(data.sources ?? []))
- .catch(() => {});
- }, []);
- async function handleSave() {
- setSaving(true);
- try {
- await fetch("/api/settings", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(keys),
- });
- setSaved(true);
- setTimeout(() => setSaved(false), 2000);
- } finally {
- setSaving(false);
- }
- }
- if (!loaded) {
- return (
- <div>
- <div className="page-header">
- <h1>Settings</h1>
- </div>
- <div className="card" style={{ padding: 32, textAlign: "center" }}>
- Loading...
- </div>
- </div>
- );
- }
- return (
- <div>
- <div className="page-header">
- <h1>Settings</h1>
- <p>Configure API keys and service endpoints (saved to .env)</p>
- </div>
- <div className="card" style={{ marginBottom: 24 }}>
- <h3 style={{ marginBottom: 20 }}>LLM Configuration</h3>
- <div className="form-group">
- <label>OpenAI API Key</label>
- <input
- className="form-input"
- type="password"
- value={keys.OPENAI_API_KEY}
- onChange={(e) => setKeys({ ...keys, OPENAI_API_KEY: e.target.value })}
- placeholder="sk-..."
- />
- </div>
- <div className="form-group">
- <label>Base URL</label>
- <input
- className="form-input"
- value={keys.OPENAI_BASE_URL}
- onChange={(e) => setKeys({ ...keys, OPENAI_BASE_URL: e.target.value })}
- placeholder="https://api.openai.com/v1"
- />
- <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 4 }}>
- OpenAI-compatible endpoints (DeepSeek, Qwen, etc.)
- </div>
- </div>
- </div>
- <div className="card" style={{ marginBottom: 24 }}>
- <h3 style={{ marginBottom: 20 }}>TTS Providers</h3>
- <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid var(--border)" }}>
- <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#3b82f6" }}>
- OpenAI TTS (Seed TTS) — Default
- </div>
- <div className="form-group">
- <label>API Key</label>
- <input
- className="form-input"
- type="password"
- value={keys.OPENAI_TTS_API_KEY}
- onChange={(e) => setKeys({ ...keys, OPENAI_TTS_API_KEY: e.target.value })}
- />
- </div>
- <div className="form-group">
- <label>Base URL</label>
- <input
- className="form-input"
- value={keys.OPENAI_TTS_BASE_URL}
- onChange={(e) => setKeys({ ...keys, OPENAI_TTS_BASE_URL: e.target.value })}
- />
- </div>
- <div className="form-group">
- <label>Model</label>
- <input
- className="form-input"
- value={keys.OPENAI_TTS_MODEL}
- onChange={(e) => setKeys({ ...keys, OPENAI_TTS_MODEL: e.target.value })}
- placeholder="seed-tts-1.1"
- />
- </div>
- </div>
- <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid var(--border)" }}>
- <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#10b981" }}>
- Fish Audio
- </div>
- <div className="form-group">
- <label>API Key</label>
- <input
- className="form-input"
- type="password"
- value={keys.FISH_AUDIO_API_KEY}
- onChange={(e) => setKeys({ ...keys, FISH_AUDIO_API_KEY: e.target.value })}
- />
- </div>
- </div>
- <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid var(--border)" }}>
- <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#0d9488" }}>
- MiniMax
- </div>
- <div className="form-group">
- <label>API Key</label>
- <input
- className="form-input"
- type="password"
- value={keys.MINIMAX_API_KEY}
- onChange={(e) => setKeys({ ...keys, MINIMAX_API_KEY: e.target.value })}
- />
- </div>
- <div className="form-group">
- <label>Group ID</label>
- <input
- className="form-input"
- value={keys.MINIMAX_GROUP_ID}
- onChange={(e) => setKeys({ ...keys, MINIMAX_GROUP_ID: e.target.value })}
- />
- </div>
- </div>
- <div>
- <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#9333ea" }}>
- ElevenLabs
- </div>
- <div className="form-group">
- <label>API Key</label>
- <input
- className="form-input"
- type="password"
- value={keys.ELEVENLABS_API_KEY}
- onChange={(e) => setKeys({ ...keys, ELEVENLABS_API_KEY: e.target.value })}
- />
- </div>
- </div>
- </div>
- <div className="card" style={{ marginBottom: 24 }}>
- <h3 style={{ marginBottom: 8 }}>Publish — OSS & Feishu</h3>
- <div style={{ fontSize: 13, color: "var(--text-muted)", marginBottom: 20 }}>
- Rendered videos are uploaded to OSS and results are pushed to a Feishu webhook. Also configurable in <code>config/default.yaml</code>.
- </div>
- <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid var(--border)" }}>
- <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#3b82f6" }}>
- Alibaba Cloud OSS
- </div>
- <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
- <div className="form-group">
- <label>Region</label>
- <input
- className="form-input"
- value={keys.OSS_REGION}
- onChange={(e) => setKeys({ ...keys, OSS_REGION: e.target.value })}
- placeholder="oss-cn-hangzhou"
- />
- </div>
- <div className="form-group">
- <label>Bucket</label>
- <input
- className="form-input"
- value={keys.OSS_BUCKET}
- onChange={(e) => setKeys({ ...keys, OSS_BUCKET: e.target.value })}
- />
- </div>
- <div className="form-group">
- <label>Access Key ID</label>
- <input
- className="form-input"
- type="password"
- value={keys.OSS_ACCESS_KEY_ID}
- onChange={(e) => setKeys({ ...keys, OSS_ACCESS_KEY_ID: e.target.value })}
- />
- </div>
- <div className="form-group">
- <label>Access Key Secret</label>
- <input
- className="form-input"
- type="password"
- value={keys.OSS_ACCESS_KEY_SECRET}
- onChange={(e) => setKeys({ ...keys, OSS_ACCESS_KEY_SECRET: e.target.value })}
- />
- </div>
- </div>
- <div className="form-group">
- <label>Public Domain (optional)</label>
- <input
- className="form-input"
- value={keys.OSS_PUBLIC_DOMAIN}
- onChange={(e) => setKeys({ ...keys, OSS_PUBLIC_DOMAIN: e.target.value })}
- placeholder="https://cdn.example.com"
- />
- <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 4 }}>
- Base URL used to build resource links (e.g. a CDN). If empty, the bucket default domain is used.
- </div>
- </div>
- </div>
- <div>
- <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#10b981" }}>
- Feishu Webhook
- </div>
- <div className="form-group">
- <label>Webhook URL</label>
- <input
- className="form-input"
- type="password"
- value={keys.FEISHU_WEBHOOK_URL}
- onChange={(e) => setKeys({ ...keys, FEISHU_WEBHOOK_URL: e.target.value })}
- placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/..."
- />
- </div>
- <div className="form-group">
- <label>Signing Secret (optional)</label>
- <input
- className="form-input"
- type="password"
- value={keys.FEISHU_WEBHOOK_SECRET}
- onChange={(e) => setKeys({ ...keys, FEISHU_WEBHOOK_SECRET: e.target.value })}
- />
- </div>
- </div>
- </div>
- <div className="card" style={{ marginBottom: 24 }}>
- <h3 style={{ marginBottom: 20 }}>Data Sources</h3>
- <div style={{ fontSize: 13, color: "var(--text-muted)", marginBottom: 16 }}>
- Configured in <code>config/default.yaml</code>
- </div>
- {sources.length === 0 ? (
- <div style={{ color: "var(--text-muted)", fontSize: 14 }}>No data sources configured.</div>
- ) : (
- <div style={{ display: "grid", gap: 12 }}>
- {sources.map((s) => (
- <div key={s.name} style={{
- padding: "12px 16px",
- background: "var(--bg)",
- borderRadius: 8,
- border: "1px solid var(--border)",
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- }}>
- <div>
- <div style={{ fontWeight: 600, marginBottom: 4 }}>{s.name}</div>
- <code style={{ fontSize: 12, color: "var(--text-muted)" }}>{s.url}</code>
- </div>
- <span className="badge badge-completed">active</span>
- </div>
- ))}
- </div>
- )}
- </div>
- <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
- <button className="btn btn-primary" onClick={handleSave} disabled={saving}>
- {saving ? "Saving..." : "Save to .env"}
- </button>
- {saved && <span style={{ color: "var(--success)", fontSize: 14 }}>Saved! Restart services to apply.</span>}
- </div>
- </div>
- );
- }
|