From ab5a967fe47487eac6fce231a0e1936734493dac Mon Sep 17 00:00:00 2001 From: orfelorfel23 Date: Thu, 4 Jun 2026 11:11:36 +0200 Subject: [PATCH] feat: add configurable video AI links --- .env.example | 3 + server.mjs | 20 ++++ src/components/cards/VideoPromptsCard.tsx | 20 ++++ src/lib/llm.ts | 12 +++ src/lib/types.ts | 6 ++ src/pages/SettingsPage.tsx | 122 ++++++++++++++++++++++ 6 files changed, 183 insertions(+) diff --git a/.env.example b/.env.example index c646f26..4f4534c 100644 --- a/.env.example +++ b/.env.example @@ -108,3 +108,6 @@ CORS_ORIGIN=* # Leave empty to use the dev proxy (see vite.config.ts). Set this if you want # the browser to talk to a server on a different origin. # VITE_API_BASE_URL=http://localhost:3000 + +# Video AI Links configuration (comma-separated Key=URL pairs) +VIDEO_AI_LINKS="Kling AI=https://klingai.com,Luma=https://lumalabs.ai,Runway=https://runwayml.com" diff --git a/server.mjs b/server.mjs index 68631b5..2c9120d 100644 --- a/server.mjs +++ b/server.mjs @@ -349,6 +349,23 @@ async function handleHealth(req, res) { }); } +function handleConfig(req, res) { + let videoAiLinks = []; + const envLinks = process.env.VIDEO_AI_LINKS || ''; + if (envLinks.trim()) { + // Expected format: "Kling AI=https://klingai.com,Luma=https://lumalabs.ai" + videoAiLinks = envLinks.split(',').map(part => { + const idx = part.indexOf('='); + if (idx > 0) { + return { name: part.slice(0, idx).trim(), url: part.slice(idx + 1).trim() }; + } + return null; + }).filter(link => link !== null); + } + + json(res, { videoAiLinks }); +} + async function handleStyleRandom(req, res) { let body; try { body = await readJsonBody(req); } @@ -512,6 +529,9 @@ const server = createServer(async (req, res) => { if (req.method === 'GET' && req.url.split('?')[0] === '/api/health') { return handleHealth(req, res); } + if (req.method === 'GET' && req.url.split('?')[0] === '/api/config') { + return handleConfig(req, res); + } if (req.method === 'POST' && req.url.split('?')[0] === '/api/generate') { return handleGenerate(req, res); } diff --git a/src/components/cards/VideoPromptsCard.tsx b/src/components/cards/VideoPromptsCard.tsx index 1e2320d..db7fe0e 100644 --- a/src/components/cards/VideoPromptsCard.tsx +++ b/src/components/cards/VideoPromptsCard.tsx @@ -9,6 +9,8 @@ import { type VideoPrompt, type VideoPromptType, } from "../../lib/types"; +import { useVideoAiLinks } from "../../lib/useVideoAiLinks"; +import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; interface VideoPromptsCardProps { prompts: VideoPrompt[]; @@ -34,6 +36,7 @@ export function VideoPromptsCard({ const dirty = current?.prompt !== originalCurrent?.prompt; const ref = useAutoHeight(current?.prompt ?? "", 120); + const { links } = useVideoAiLinks(); return ( + + {links && links.length > 0 && ( +
+ {links.map((link, i) => ( + + {link.name} + + + ))} +
+ )} ) : (

No prompt for this tab yet.

diff --git a/src/lib/llm.ts b/src/lib/llm.ts index 094bfc0..cf8f03e 100644 --- a/src/lib/llm.ts +++ b/src/lib/llm.ts @@ -71,6 +71,18 @@ export async function getServerStatus( return (await resp.json()) as ServerStatus; } +export interface ConfigResponse { + videoAiLinks: import("./types").VideoAiLink[]; +} + +export async function getConfig(signal?: AbortSignal): Promise { + const resp = await fetch(`${API_BASE}/api/config`, { signal }); + if (!resp.ok) { + throw new Error(`Server config check failed (${resp.status} ${resp.statusText})`); + } + return (await resp.json()) as ConfigResponse; +} + export interface GenerateOptions { signal?: AbortSignal; } diff --git a/src/lib/types.ts b/src/lib/types.ts index f6efa3f..2a39157 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -71,3 +71,9 @@ export const SECTION_LABELS: Record = { video_prompts: "video prompts", youtube_description: "description", }; + +export interface VideoAiLink { + name: string; + url: string; +} + diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 3f40381..de40471 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -9,11 +9,14 @@ import { faPlug, faTrashAlt, faServer, + faVideo, } from "@fortawesome/free-solid-svg-icons"; import { useToast } from "../lib/toast"; import { getServerStatus, type ServerStatus } from "../lib/llm"; import { clearHistory } from "../lib/history"; import { useI18n } from "../lib/i18n"; +import { useVideoAiLinks } from "../lib/useVideoAiLinks"; +import type { VideoAiLink } from "../lib/types"; const DRAFT_KEY = "melodymuse-draft"; @@ -41,6 +44,15 @@ export function SettingsPage() { hasDraft: false, }); + const { links, loading: linksLoading, saveLinks, clearLocalLinks, hasLocal } = useVideoAiLinks(); + const [editingLinks, setEditingLinks] = useState(null); + + useEffect(() => { + if (!linksLoading && editingLinks === null) { + setEditingLinks(links); + } + }, [links, linksLoading, editingLinks]); + const check = async (signal?: AbortSignal) => { setChecking(true); setStatusError(null); @@ -76,6 +88,40 @@ export function SettingsPage() { toast.info(t.clearDraft); }; + const handleLinkChange = (index: number, field: keyof VideoAiLink, value: string) => { + if (!editingLinks) return; + const next = [...editingLinks]; + next[index] = { ...next[index], [field]: value }; + setEditingLinks(next); + }; + + const handleAddLink = () => { + if (!editingLinks) return; + setEditingLinks([...editingLinks, { name: "", url: "" }]); + }; + + const handleRemoveLink = (index: number) => { + if (!editingLinks) return; + const next = [...editingLinks]; + next.splice(index, 1); + setEditingLinks(next); + }; + + const handleSaveLinks = () => { + if (!editingLinks) return; + const validLinks = editingLinks.filter(l => l.name.trim() && l.url.trim()); + saveLinks(validLinks); + setEditingLinks(validLinks); + toast.success("Video AI links saved to browser storage"); + }; + + const handleResetLinks = () => { + if (!window.confirm("Restore default Video AI links from the server?")) return; + clearLocalLinks(); + setEditingLinks(null); // will re-sync with hook + toast.info("Restored default links"); + }; + return (
@@ -177,6 +223,82 @@ export function SettingsPage() {
+
+ +
+

+ + + Video AI Links + + {hasLocal && ( + + )} +

+

+ Configure the quick links shown at the bottom of the Video Prompts card. + {hasLocal ? " Currently using your local browser overrides." : " Currently using server defaults."} +

+ + {linksLoading || editingLinks === null ? ( +
+ +
+ ) : ( +
+ {editingLinks.map((link, i) => ( +
+ handleLinkChange(i, "name", e.target.value)} + /> + handleLinkChange(i, "url", e.target.value)} + /> + +
+ ))} + +
+ + +
+
+ )} +
+