page.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. "use client";
  2. import { useState, useEffect } from "react";
  3. interface EnvVars {
  4. OPENAI_API_KEY: string;
  5. OPENAI_BASE_URL: string;
  6. OPENAI_TTS_API_KEY: string;
  7. OPENAI_TTS_BASE_URL: string;
  8. OPENAI_TTS_MODEL: string;
  9. FISH_AUDIO_API_KEY: string;
  10. MINIMAX_API_KEY: string;
  11. MINIMAX_GROUP_ID: string;
  12. ELEVENLABS_API_KEY: string;
  13. OSS_REGION: string;
  14. OSS_BUCKET: string;
  15. OSS_ACCESS_KEY_ID: string;
  16. OSS_ACCESS_KEY_SECRET: string;
  17. OSS_PUBLIC_DOMAIN: string;
  18. FEISHU_WEBHOOK_URL: string;
  19. FEISHU_WEBHOOK_SECRET: string;
  20. }
  21. const DEFAULT_ENV: EnvVars = {
  22. OPENAI_API_KEY: "",
  23. OPENAI_BASE_URL: "https://api.openai.com/v1",
  24. OPENAI_TTS_API_KEY: "",
  25. OPENAI_TTS_BASE_URL: "https://new-api.corp.shuidi.tech/v1",
  26. OPENAI_TTS_MODEL: "seed-tts-1.1",
  27. FISH_AUDIO_API_KEY: "",
  28. MINIMAX_API_KEY: "",
  29. MINIMAX_GROUP_ID: "",
  30. ELEVENLABS_API_KEY: "",
  31. OSS_REGION: "oss-cn-hangzhou",
  32. OSS_BUCKET: "",
  33. OSS_ACCESS_KEY_ID: "",
  34. OSS_ACCESS_KEY_SECRET: "",
  35. OSS_PUBLIC_DOMAIN: "",
  36. FEISHU_WEBHOOK_URL: "",
  37. FEISHU_WEBHOOK_SECRET: "",
  38. };
  39. export default function SettingsPage() {
  40. const [keys, setKeys] = useState<EnvVars>(DEFAULT_ENV);
  41. const [loaded, setLoaded] = useState(false);
  42. const [saving, setSaving] = useState(false);
  43. const [saved, setSaved] = useState(false);
  44. const [sources, setSources] = useState<{ name: string; url: string }[]>([]);
  45. useEffect(() => {
  46. fetch("/api/settings")
  47. .then((r) => r.json())
  48. .then((data) => {
  49. setKeys((prev) => ({ ...prev, ...data }));
  50. setLoaded(true);
  51. })
  52. .catch(() => setLoaded(true));
  53. fetch("/api/collect/sources")
  54. .then((r) => r.json())
  55. .then((data) => setSources(data.sources ?? []))
  56. .catch(() => {});
  57. }, []);
  58. async function handleSave() {
  59. setSaving(true);
  60. try {
  61. await fetch("/api/settings", {
  62. method: "POST",
  63. headers: { "Content-Type": "application/json" },
  64. body: JSON.stringify(keys),
  65. });
  66. setSaved(true);
  67. setTimeout(() => setSaved(false), 2000);
  68. } finally {
  69. setSaving(false);
  70. }
  71. }
  72. if (!loaded) {
  73. return (
  74. <div>
  75. <div className="page-header">
  76. <h1>Settings</h1>
  77. </div>
  78. <div className="card" style={{ padding: 32, textAlign: "center" }}>
  79. Loading...
  80. </div>
  81. </div>
  82. );
  83. }
  84. return (
  85. <div>
  86. <div className="page-header">
  87. <h1>Settings</h1>
  88. <p>Configure API keys and service endpoints (saved to .env)</p>
  89. </div>
  90. <div className="card" style={{ marginBottom: 24 }}>
  91. <h3 style={{ marginBottom: 20 }}>LLM Configuration</h3>
  92. <div className="form-group">
  93. <label>OpenAI API Key</label>
  94. <input
  95. className="form-input"
  96. type="password"
  97. value={keys.OPENAI_API_KEY}
  98. onChange={(e) => setKeys({ ...keys, OPENAI_API_KEY: e.target.value })}
  99. placeholder="sk-..."
  100. />
  101. </div>
  102. <div className="form-group">
  103. <label>Base URL</label>
  104. <input
  105. className="form-input"
  106. value={keys.OPENAI_BASE_URL}
  107. onChange={(e) => setKeys({ ...keys, OPENAI_BASE_URL: e.target.value })}
  108. placeholder="https://api.openai.com/v1"
  109. />
  110. <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 4 }}>
  111. OpenAI-compatible endpoints (DeepSeek, Qwen, etc.)
  112. </div>
  113. </div>
  114. </div>
  115. <div className="card" style={{ marginBottom: 24 }}>
  116. <h3 style={{ marginBottom: 20 }}>TTS Providers</h3>
  117. <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid var(--border)" }}>
  118. <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#3b82f6" }}>
  119. OpenAI TTS (Seed TTS) — Default
  120. </div>
  121. <div className="form-group">
  122. <label>API Key</label>
  123. <input
  124. className="form-input"
  125. type="password"
  126. value={keys.OPENAI_TTS_API_KEY}
  127. onChange={(e) => setKeys({ ...keys, OPENAI_TTS_API_KEY: e.target.value })}
  128. />
  129. </div>
  130. <div className="form-group">
  131. <label>Base URL</label>
  132. <input
  133. className="form-input"
  134. value={keys.OPENAI_TTS_BASE_URL}
  135. onChange={(e) => setKeys({ ...keys, OPENAI_TTS_BASE_URL: e.target.value })}
  136. />
  137. </div>
  138. <div className="form-group">
  139. <label>Model</label>
  140. <input
  141. className="form-input"
  142. value={keys.OPENAI_TTS_MODEL}
  143. onChange={(e) => setKeys({ ...keys, OPENAI_TTS_MODEL: e.target.value })}
  144. placeholder="seed-tts-1.1"
  145. />
  146. </div>
  147. </div>
  148. <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid var(--border)" }}>
  149. <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#10b981" }}>
  150. Fish Audio
  151. </div>
  152. <div className="form-group">
  153. <label>API Key</label>
  154. <input
  155. className="form-input"
  156. type="password"
  157. value={keys.FISH_AUDIO_API_KEY}
  158. onChange={(e) => setKeys({ ...keys, FISH_AUDIO_API_KEY: e.target.value })}
  159. />
  160. </div>
  161. </div>
  162. <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid var(--border)" }}>
  163. <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#0d9488" }}>
  164. MiniMax
  165. </div>
  166. <div className="form-group">
  167. <label>API Key</label>
  168. <input
  169. className="form-input"
  170. type="password"
  171. value={keys.MINIMAX_API_KEY}
  172. onChange={(e) => setKeys({ ...keys, MINIMAX_API_KEY: e.target.value })}
  173. />
  174. </div>
  175. <div className="form-group">
  176. <label>Group ID</label>
  177. <input
  178. className="form-input"
  179. value={keys.MINIMAX_GROUP_ID}
  180. onChange={(e) => setKeys({ ...keys, MINIMAX_GROUP_ID: e.target.value })}
  181. />
  182. </div>
  183. </div>
  184. <div>
  185. <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#9333ea" }}>
  186. ElevenLabs
  187. </div>
  188. <div className="form-group">
  189. <label>API Key</label>
  190. <input
  191. className="form-input"
  192. type="password"
  193. value={keys.ELEVENLABS_API_KEY}
  194. onChange={(e) => setKeys({ ...keys, ELEVENLABS_API_KEY: e.target.value })}
  195. />
  196. </div>
  197. </div>
  198. </div>
  199. <div className="card" style={{ marginBottom: 24 }}>
  200. <h3 style={{ marginBottom: 8 }}>Publish — OSS &amp; Feishu</h3>
  201. <div style={{ fontSize: 13, color: "var(--text-muted)", marginBottom: 20 }}>
  202. Rendered videos are uploaded to OSS and results are pushed to a Feishu webhook. Also configurable in <code>config/default.yaml</code>.
  203. </div>
  204. <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid var(--border)" }}>
  205. <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#3b82f6" }}>
  206. Alibaba Cloud OSS
  207. </div>
  208. <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
  209. <div className="form-group">
  210. <label>Region</label>
  211. <input
  212. className="form-input"
  213. value={keys.OSS_REGION}
  214. onChange={(e) => setKeys({ ...keys, OSS_REGION: e.target.value })}
  215. placeholder="oss-cn-hangzhou"
  216. />
  217. </div>
  218. <div className="form-group">
  219. <label>Bucket</label>
  220. <input
  221. className="form-input"
  222. value={keys.OSS_BUCKET}
  223. onChange={(e) => setKeys({ ...keys, OSS_BUCKET: e.target.value })}
  224. />
  225. </div>
  226. <div className="form-group">
  227. <label>Access Key ID</label>
  228. <input
  229. className="form-input"
  230. type="password"
  231. value={keys.OSS_ACCESS_KEY_ID}
  232. onChange={(e) => setKeys({ ...keys, OSS_ACCESS_KEY_ID: e.target.value })}
  233. />
  234. </div>
  235. <div className="form-group">
  236. <label>Access Key Secret</label>
  237. <input
  238. className="form-input"
  239. type="password"
  240. value={keys.OSS_ACCESS_KEY_SECRET}
  241. onChange={(e) => setKeys({ ...keys, OSS_ACCESS_KEY_SECRET: e.target.value })}
  242. />
  243. </div>
  244. </div>
  245. <div className="form-group">
  246. <label>Public Domain (optional)</label>
  247. <input
  248. className="form-input"
  249. value={keys.OSS_PUBLIC_DOMAIN}
  250. onChange={(e) => setKeys({ ...keys, OSS_PUBLIC_DOMAIN: e.target.value })}
  251. placeholder="https://cdn.example.com"
  252. />
  253. <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 4 }}>
  254. Base URL used to build resource links (e.g. a CDN). If empty, the bucket default domain is used.
  255. </div>
  256. </div>
  257. </div>
  258. <div>
  259. <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, color: "#10b981" }}>
  260. Feishu Webhook
  261. </div>
  262. <div className="form-group">
  263. <label>Webhook URL</label>
  264. <input
  265. className="form-input"
  266. type="password"
  267. value={keys.FEISHU_WEBHOOK_URL}
  268. onChange={(e) => setKeys({ ...keys, FEISHU_WEBHOOK_URL: e.target.value })}
  269. placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/..."
  270. />
  271. </div>
  272. <div className="form-group">
  273. <label>Signing Secret (optional)</label>
  274. <input
  275. className="form-input"
  276. type="password"
  277. value={keys.FEISHU_WEBHOOK_SECRET}
  278. onChange={(e) => setKeys({ ...keys, FEISHU_WEBHOOK_SECRET: e.target.value })}
  279. />
  280. </div>
  281. </div>
  282. </div>
  283. <div className="card" style={{ marginBottom: 24 }}>
  284. <h3 style={{ marginBottom: 20 }}>Data Sources</h3>
  285. <div style={{ fontSize: 13, color: "var(--text-muted)", marginBottom: 16 }}>
  286. Configured in <code>config/default.yaml</code>
  287. </div>
  288. {sources.length === 0 ? (
  289. <div style={{ color: "var(--text-muted)", fontSize: 14 }}>No data sources configured.</div>
  290. ) : (
  291. <div style={{ display: "grid", gap: 12 }}>
  292. {sources.map((s) => (
  293. <div key={s.name} style={{
  294. padding: "12px 16px",
  295. background: "var(--bg)",
  296. borderRadius: 8,
  297. border: "1px solid var(--border)",
  298. display: "flex",
  299. justifyContent: "space-between",
  300. alignItems: "center",
  301. }}>
  302. <div>
  303. <div style={{ fontWeight: 600, marginBottom: 4 }}>{s.name}</div>
  304. <code style={{ fontSize: 12, color: "var(--text-muted)" }}>{s.url}</code>
  305. </div>
  306. <span className="badge badge-completed">active</span>
  307. </div>
  308. ))}
  309. </div>
  310. )}
  311. </div>
  312. <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
  313. <button className="btn btn-primary" onClick={handleSave} disabled={saving}>
  314. {saving ? "Saving..." : "Save to .env"}
  315. </button>
  316. {saved && <span style={{ color: "var(--success)", fontSize: 14 }}>Saved! Restart services to apply.</span>}
  317. </div>
  318. </div>
  319. );
  320. }