From 01eea83b05131f226624666a3ab8c7b3b48d0bc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 13:53:47 +0000 Subject: [PATCH] feat(web): media settings section for the deployment owner (wip) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u --- apps/web/src/components/MediaSettings.tsx | 278 ++++++++++++++++++++++ apps/web/src/pages/SettingsOverlay.tsx | 5 + 2 files changed, 283 insertions(+) create mode 100644 apps/web/src/components/MediaSettings.tsx diff --git a/apps/web/src/components/MediaSettings.tsx b/apps/web/src/components/MediaSettings.tsx new file mode 100644 index 0000000..ad93abb --- /dev/null +++ b/apps/web/src/components/MediaSettings.tsx @@ -0,0 +1,278 @@ +import { Trans, useLingui } from "@lingui/react/macro"; +import type { MediaSettingsUpdate, MediaSettingsView } from "@rakazo/contracts"; +import { Button, Field, FieldLabel, Input, Textarea } from "@rakazo/ui-web"; +import { useEffect, useId, useState } from "react"; +import { rpc } from "../lib/rpc"; +import { SuccessPop } from "./ai/primitives"; + +type KeyDraft = { value: string; remove: boolean }; + +const emptyKey: KeyDraft = { value: "", remove: false }; + +/** Omitted keeps the saved key, empty removes it, anything else replaces it. */ +function keyUpdate(draft: KeyDraft): string | undefined { + if (draft.value.trim()) return draft.value.trim(); + return draft.remove ? "" : undefined; +} + +function ApiKeyField({ + saved, + draft, + disabled, + onChange, +}: { + saved: boolean; + draft: KeyDraft; + disabled: boolean; + onChange: (draft: KeyDraft) => void; +}) { + const { t } = useLingui(); + const id = useId(); + const showSaved = saved && !draft.remove; + return ( + +
+ + API key + + {showSaved && !draft.value ? ( + + ) : null} +
+ onChange({ ...draft, value: event.target.value })} + /> +
+ ); +} + +/** The deployment owner's media servers: ComfyUI for generation, a transcription server. */ +export function MediaSettings() { + const { t } = useLingui(); + const ids = useId(); + const [view, setView] = useState(null); + const [comfyUrl, setComfyUrl] = useState(""); + const [comfyKey, setComfyKey] = useState(emptyKey); + const [imageModel, setImageModel] = useState(""); + const [imageWorkflow, setImageWorkflow] = useState(""); + const [videoWorkflow, setVideoWorkflow] = useState(""); + const [transcriptionUrl, setTranscriptionUrl] = useState(""); + const [transcriptionModel, setTranscriptionModel] = useState(""); + const [transcriptionKey, setTranscriptionKey] = useState(emptyKey); + const [busy, setBusy] = useState(false); + const [saved, setSaved] = useState(false); + const [error, setError] = useState(null); + + function apply(next: MediaSettingsView) { + setView(next); + setComfyUrl(next.comfyui?.baseUrl ?? ""); + setImageModel(next.comfyui?.imageModel ?? ""); + setImageWorkflow(next.comfyui?.imageWorkflow ?? ""); + setVideoWorkflow(next.comfyui?.videoWorkflow ?? ""); + setTranscriptionUrl(next.transcription?.baseUrl ?? ""); + setTranscriptionModel(next.transcription?.model ?? ""); + setComfyKey(emptyKey); + setTranscriptionKey(emptyKey); + } + + useEffect(() => { + let cancelled = false; + rpc.media.settings + .get() + .then((next) => { + if (!cancelled) apply(next); + }) + .catch((err: unknown) => { + if (!cancelled) setError(err instanceof Error ? err.message : t`Couldn't load`); + }); + return () => { + cancelled = true; + }; + }, [t]); + + async function save() { + if (busy) return; + setBusy(true); + setSaved(false); + setError(null); + const update: MediaSettingsUpdate = { + comfyui: comfyUrl.trim() + ? { + baseUrl: comfyUrl.trim(), + apiKey: keyUpdate(comfyKey), + imageModel, + imageWorkflow, + videoWorkflow, + } + : null, + transcription: transcriptionUrl.trim() + ? { + baseUrl: transcriptionUrl.trim(), + model: transcriptionModel, + apiKey: keyUpdate(transcriptionKey), + } + : null, + }; + try { + apply(await rpc.media.settings.update(update)); + setSaved(true); + } catch (err) { + setError(err instanceof Error ? err.message : t`Couldn't save`); + } finally { + setBusy(false); + } + } + + const disabled = busy || view === null; + const advancedSaved = Boolean( + view?.comfyui?.imageModel || view?.comfyui?.imageWorkflow || view?.comfyui?.videoWorkflow, + ); + const workflowClass = "max-h-48 overflow-auto font-mono text-xs"; + + return ( +
+
+

ComfyUI

+ + + Server URL + + setComfyUrl(event.target.value)} + /> + + +
+ + + Advanced + + + + + + Image model + + setImageModel(event.target.value)} + /> + + + + Image workflow + +