feat: add editable system prompts from web UI
This commit is contained in:
+68
-2
@@ -26,6 +26,8 @@ import {
|
|||||||
buildStylePrompt,
|
buildStylePrompt,
|
||||||
buildSystemPrompt,
|
buildSystemPrompt,
|
||||||
buildUserMessage,
|
buildUserMessage,
|
||||||
|
setActivePrompts,
|
||||||
|
DEFAULT_PROMPTS,
|
||||||
} from './server/prompts.mjs';
|
} from './server/prompts.mjs';
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -38,6 +40,7 @@ const PORT = parseInt(process.env.PORT || '3000', 10);
|
|||||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || '*';
|
const CORS_ORIGIN = process.env.CORS_ORIGIN || '*';
|
||||||
const DATA_DIR = process.env.DATA_DIR || join(__dirname, 'data');
|
const DATA_DIR = process.env.DATA_DIR || join(__dirname, 'data');
|
||||||
const HISTORY_FILE = join(DATA_DIR, 'history.json');
|
const HISTORY_FILE = join(DATA_DIR, 'history.json');
|
||||||
|
const PROMPTS_FILE = join(DATA_DIR, 'prompts.json');
|
||||||
const MAX_HISTORY = 500;
|
const MAX_HISTORY = 500;
|
||||||
|
|
||||||
const LLM = {
|
const LLM = {
|
||||||
@@ -58,8 +61,20 @@ if (!LLM.endpoint || !LLM.apiKey) {
|
|||||||
// Ensure data directory exists
|
// Ensure data directory exists
|
||||||
await mkdir(DATA_DIR, { recursive: true });
|
await mkdir(DATA_DIR, { recursive: true });
|
||||||
|
|
||||||
console.log(`[startup] LLM endpoint: ${LLM.endpoint}`);
|
// Load prompts
|
||||||
console.log(`[startup] LLM model: ${LLM.model}`);
|
try {
|
||||||
|
const customPromptsStr = await readFile(PROMPTS_FILE, 'utf-8');
|
||||||
|
const customPrompts = JSON.parse(customPromptsStr);
|
||||||
|
setActivePrompts(customPrompts);
|
||||||
|
console.log('[startup] Custom system prompts loaded');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
console.error('[startup] Failed to read prompts.json:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[startup] Model: ${LLM.model}`);
|
||||||
|
console.log(`[startup] Endpoint: ${LLM.endpoint}`);
|
||||||
console.log(`[startup] Data dir: ${DATA_DIR}`);
|
console.log(`[startup] Data dir: ${DATA_DIR}`);
|
||||||
console.log(`[startup] Listening on: http://0.0.0.0:${PORT}`);
|
console.log(`[startup] Listening on: http://0.0.0.0:${PORT}`);
|
||||||
|
|
||||||
@@ -512,6 +527,45 @@ async function handleGenerate(req, res) {
|
|||||||
json(res, { error: lastErr?.message || 'Generation failed' }, 500);
|
json(res, { error: lastErr?.message || 'Generation failed' }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// System Prompts Config Handlers
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
import { activePrompts } from './server/prompts.mjs';
|
||||||
|
import { unlink } from 'node:fs/promises';
|
||||||
|
|
||||||
|
function handleGetSystemPrompts(req, res) {
|
||||||
|
json(res, activePrompts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePostSystemPrompts(req, res) {
|
||||||
|
try {
|
||||||
|
const body = await parseJsonBody(req);
|
||||||
|
// Merge with defaults to ensure all keys exist
|
||||||
|
const newPrompts = { ...DEFAULT_PROMPTS, ...body };
|
||||||
|
setActivePrompts(newPrompts);
|
||||||
|
await writeFile(PROMPTS_FILE, JSON.stringify(newPrompts, null, 2), 'utf-8');
|
||||||
|
json(res, { success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[prompts] failed to save', err);
|
||||||
|
json(res, { error: 'Failed to save prompts' }, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteSystemPrompts(req, res) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await unlink(PROMPTS_FILE);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== 'ENOENT') throw e;
|
||||||
|
}
|
||||||
|
setActivePrompts(DEFAULT_PROMPTS);
|
||||||
|
json(res, { success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[prompts] failed to delete', err);
|
||||||
|
json(res, { error: 'Failed to restore default prompts' }, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
// Server
|
// Server
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -532,6 +586,18 @@ const server = createServer(async (req, res) => {
|
|||||||
if (req.method === 'GET' && req.url.split('?')[0] === '/api/config') {
|
if (req.method === 'GET' && req.url.split('?')[0] === '/api/config') {
|
||||||
return handleConfig(req, res);
|
return handleConfig(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// System Prompts endpoints
|
||||||
|
if (req.method === 'GET' && req.url.split('?')[0] === '/api/system-prompts') {
|
||||||
|
return handleGetSystemPrompts(req, res);
|
||||||
|
}
|
||||||
|
if (req.method === 'POST' && req.url.split('?')[0] === '/api/system-prompts') {
|
||||||
|
return handlePostSystemPrompts(req, res);
|
||||||
|
}
|
||||||
|
if (req.method === 'DELETE' && req.url.split('?')[0] === '/api/system-prompts') {
|
||||||
|
return handleDeleteSystemPrompts(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === 'POST' && req.url.split('?')[0] === '/api/generate') {
|
if (req.method === 'POST' && req.url.split('?')[0] === '/api/generate') {
|
||||||
return handleGenerate(req, res);
|
return handleGenerate(req, res);
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-65
@@ -1,20 +1,8 @@
|
|||||||
// System + user prompt construction for the LLM. Server-side only — the
|
// System + user prompt construction for the LLM. Server-side only — the
|
||||||
// browser never sees these prompts.
|
// browser never sees these prompts.
|
||||||
|
|
||||||
export function getSystemPromptAll() {
|
export const DEFAULT_PROMPTS = {
|
||||||
let tools = 'Runway Gen-3 Alpha, Kling AI, Luma Dream Machine, Pika 2.0, Haiper';
|
all: `You are a music production assistant specializing in Suno AI song creation.
|
||||||
const envLinks = process.env.VIDEO_AI_LINKS || '';
|
|
||||||
if (envLinks.trim()) {
|
|
||||||
const names = envLinks.split(',').map(part => {
|
|
||||||
const idx = part.indexOf('=');
|
|
||||||
return idx > 0 ? part.slice(0, idx).trim() : null;
|
|
||||||
}).filter(Boolean);
|
|
||||||
if (names.length > 0) {
|
|
||||||
tools = names.join(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `You are a music production assistant specializing in Suno AI song creation.
|
|
||||||
|
|
||||||
The user will describe a music idea. Generate all song assets as a single JSON
|
The user will describe a music idea. Generate all song assets as a single JSON
|
||||||
object. Output ONLY valid JSON — no markdown, no backticks, no preamble.
|
object. Output ONLY valid JSON — no markdown, no backticks, no preamble.
|
||||||
@@ -62,23 +50,8 @@ REQUIRED JSON STRUCTURE:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Keep each video_prompts[].prompt under 900 characters (excluding the Negative line).
|
Keep each video_prompts[].prompt under 900 characters (excluding the Negative line).
|
||||||
Recommended video tools (choose the best fit per prompt): ${tools}. Never recommend Sora.`;
|
Recommended video tools (choose the best fit per prompt): {{VIDEO_TOOLS}}. Never recommend Sora.`,
|
||||||
}
|
partial: `You are a music production assistant specializing in Suno AI song creation.
|
||||||
|
|
||||||
export function getSystemPromptPartial() {
|
|
||||||
let tools = 'Runway Gen-3 Alpha, Kling AI, Luma Dream Machine, Pika 2.0, Haiper';
|
|
||||||
const envLinks = process.env.VIDEO_AI_LINKS || '';
|
|
||||||
if (envLinks.trim()) {
|
|
||||||
const names = envLinks.split(',').map(part => {
|
|
||||||
const idx = part.indexOf('=');
|
|
||||||
return idx > 0 ? part.slice(0, idx).trim() : null;
|
|
||||||
}).filter(Boolean);
|
|
||||||
if (names.length > 0) {
|
|
||||||
tools = names.join(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `You are a music production assistant specializing in Suno AI song creation.
|
|
||||||
|
|
||||||
The user has an existing song and wants to regenerate ONE section. Output ONLY
|
The user has an existing song and wants to regenerate ONE section. Output ONLY
|
||||||
valid JSON — no markdown, no backticks, no preamble.
|
valid JSON — no markdown, no backticks, no preamble.
|
||||||
@@ -106,12 +79,10 @@ QUALITY CONSTRAINTS:
|
|||||||
- style: comma-separated, max 120 words, include tempo + BPM range, never
|
- style: comma-separated, max 120 words, include tempo + BPM range, never
|
||||||
name an artist, no "sounds like" phrases.
|
name an artist, no "sounds like" phrases.
|
||||||
- video_prompts: 5-second seamless loops, 16:9. Include the "Negative" line
|
- video_prompts: 5-second seamless loops, 16:9. Include the "Negative" line
|
||||||
inside each prompt. Keep prompts under 900 characters. Recommended tools: ${tools}. Never recommend Sora.
|
inside each prompt. Keep prompts under 900 characters. Recommended tools: {{VIDEO_TOOLS}}. Never recommend Sora.
|
||||||
- youtube_description: follow the hook → description → divider → lyrics →
|
- youtube_description: follow the hook → description → divider → lyrics →
|
||||||
divider → CTA → hashtags structure.`;
|
divider → CTA → hashtags structure.`,
|
||||||
}
|
style_normal: `You are a Suno style-prompt generator.
|
||||||
|
|
||||||
export const SYSTEM_PROMPT_STYLE_NORMAL = `You are a Suno style-prompt generator.
|
|
||||||
|
|
||||||
Output ONE short Suno style description.
|
Output ONE short Suno style description.
|
||||||
|
|
||||||
@@ -128,9 +99,8 @@ Examples of the expected output (do NOT copy these verbatim):
|
|||||||
- dark synthwave, analog pads, driving bassline, 110 BPM, male vocals
|
- dark synthwave, analog pads, driving bassline, 110 BPM, male vocals
|
||||||
- indie folk, fingerpicked acoustic guitar, soft female vocals, 90 BPM
|
- indie folk, fingerpicked acoustic guitar, soft female vocals, 90 BPM
|
||||||
- trap, heavy 808s, dark piano, fast hi-hats, 140 BPM, autotune vocals
|
- trap, heavy 808s, dark piano, fast hi-hats, 140 BPM, autotune vocals
|
||||||
- dreamy shoegaze, layered reverb guitars, hushed vocals, 95 BPM`;
|
- dreamy shoegaze, layered reverb guitars, hushed vocals, 95 BPM`,
|
||||||
|
style_crazy: `You are a Suno style-prompt generator with permission to break conventions.
|
||||||
export const SYSTEM_PROMPT_STYLE_CRAZY = `You are a Suno style-prompt generator with permission to break conventions.
|
|
||||||
|
|
||||||
Output ONE short Suno style description.
|
Output ONE short Suno style description.
|
||||||
|
|
||||||
@@ -150,9 +120,8 @@ Examples of the expected output (do NOT copy these verbatim):
|
|||||||
- baroque chamber orchestra meets dubstep, cellos, glitch drops, 160 BPM
|
- baroque chamber orchestra meets dubstep, cellos, glitch drops, 160 BPM
|
||||||
- lo-fi mariachi breakcore, trumpets, chopped breaks, 90 BPM, vinyl crackle
|
- lo-fi mariachi breakcore, trumpets, chopped breaks, 90 BPM, vinyl crackle
|
||||||
- medieval lute and drum & bass, fingerpicked strings, reese bass, 174 BPM
|
- medieval lute and drum & bass, fingerpicked strings, reese bass, 174 BPM
|
||||||
- tuvan throat singing over tropical house, guttural vocals, marimba, 120 BPM`;
|
- tuvan throat singing over tropical house, guttural vocals, marimba, 120 BPM`,
|
||||||
|
lyrics_partial: `You are a music production assistant specializing in Suno AI song creation.
|
||||||
export const SYSTEM_PROMPT_LYRICS_PARTIAL = `You are a music production assistant specializing in Suno AI song creation.
|
|
||||||
|
|
||||||
The user has existing lyrics and wants to regenerate a SPECIFIC selected passage only.
|
The user has existing lyrics and wants to regenerate a SPECIFIC selected passage only.
|
||||||
|
|
||||||
@@ -162,12 +131,44 @@ Rules:
|
|||||||
- Keep the same emotional tone, language, and Suno section tags.
|
- Keep the same emotional tone, language, and Suno section tags.
|
||||||
- The lyrics must NOT reference the genre, instruments, or music production.
|
- The lyrics must NOT reference the genre, instruments, or music production.
|
||||||
- Output ONLY the rewritten passage text — no JSON, no explanation, no quotes around it.
|
- Output ONLY the rewritten passage text — no JSON, no explanation, no quotes around it.
|
||||||
- Do NOT include section tags in your output unless the selected passage already contained them.`;
|
- Do NOT include section tags in your output unless the selected passage already contained them.`
|
||||||
|
};
|
||||||
|
|
||||||
|
export let activePrompts = { ...DEFAULT_PROMPTS };
|
||||||
|
|
||||||
|
export function setActivePrompts(prompts) {
|
||||||
|
activePrompts = { ...DEFAULT_PROMPTS, ...prompts };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToolsString() {
|
||||||
|
let tools = 'Runway Gen-3 Alpha, Kling AI, Haiper';
|
||||||
|
const envLinks = process.env.VIDEO_AI_LINKS || '';
|
||||||
|
if (envLinks.trim()) {
|
||||||
|
const names = envLinks.split(',').map(part => {
|
||||||
|
const idx = part.indexOf('=');
|
||||||
|
return idx > 0 ? part.slice(0, idx).trim() : null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
if (names.length > 0) {
|
||||||
|
tools = names.join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildSystemPrompt(req) {
|
export function buildSystemPrompt(req) {
|
||||||
if (req.section === 'lyrics_partial') return SYSTEM_PROMPT_LYRICS_PARTIAL;
|
const toolsStr = getToolsString();
|
||||||
if (req.section === 'all') return getSystemPromptAll();
|
|
||||||
return getSystemPromptPartial();
|
if (req.section === 'lyrics_partial') {
|
||||||
|
return activePrompts.lyrics_partial;
|
||||||
|
}
|
||||||
|
if (req.section === 'all') {
|
||||||
|
return activePrompts.all.replace('{{VIDEO_TOOLS}}', toolsStr);
|
||||||
|
}
|
||||||
|
return activePrompts.partial.replace('{{VIDEO_TOOLS}}', toolsStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStylePrompt(crazy) {
|
||||||
|
return crazy ? activePrompts.style_crazy : activePrompts.style_normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildUserMessage(req) {
|
export function buildUserMessage(req) {
|
||||||
@@ -182,29 +183,34 @@ export function buildUserMessage(req) {
|
|||||||
req.context?.lyrics ?? '',
|
req.context?.lyrics ?? '',
|
||||||
'',
|
'',
|
||||||
'Selected passage to rewrite:',
|
'Selected passage to rewrite:',
|
||||||
req.selected_text ?? '',
|
req.passage ?? '',
|
||||||
].filter(l => l !== null).join('\n');
|
]
|
||||||
|
.filter((line) => line !== null)
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = [];
|
const lines = [
|
||||||
lines.push(`Music idea: ${req.input}`);
|
`Music idea: ${req.input}`,
|
||||||
lines.push(`Language: ${req.language}`);
|
`Language: ${req.language}`,
|
||||||
if (req.mood) lines.push(`Mood: ${req.mood}`);
|
req.mood ? `Mood: ${req.mood}` : '',
|
||||||
if (req.style_hint) lines.push(`Style hint (user-provided): ${req.style_hint}`);
|
req.vocals ? `Vocals: ${req.vocals}` : '',
|
||||||
lines.push(`Vocals: ${req.vocals}`);
|
];
|
||||||
lines.push(`Section to generate: ${req.section}`);
|
|
||||||
|
|
||||||
if (req.context && Object.keys(req.context).length > 0) {
|
if (req.section !== 'all' && req.context) {
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('Context — the current values of the other sections of this song:');
|
lines.push('CURRENT STATE OF OTHER SECTIONS:');
|
||||||
lines.push(JSON.stringify(req.context, null, 2));
|
if (req.context.titles) {
|
||||||
|
lines.push('Titles:', ...req.context.titles.map((t) => `- ${t}`));
|
||||||
|
}
|
||||||
|
if (req.context.style) {
|
||||||
|
lines.push(`Style: ${req.context.style}`);
|
||||||
|
}
|
||||||
|
if (req.context.lyrics) {
|
||||||
|
lines.push('Lyrics:', req.context.lyrics);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
lines.push(`Please regenerate ONLY the following section: ${req.section}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join('\n');
|
return lines.filter((line) => line !== null).join('\n');
|
||||||
}
|
|
||||||
|
|
||||||
export function buildStylePrompt(mode, idea) {
|
|
||||||
const base = mode === 'crazy' ? SYSTEM_PROMPT_STYLE_CRAZY : SYSTEM_PROMPT_STYLE_NORMAL;
|
|
||||||
if (!idea || !idea.trim()) return base;
|
|
||||||
return base + `\n\nUser's music idea (use as context for tailoring the style): "${idea.trim()}"`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import {
|
|||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faServer,
|
faServer,
|
||||||
faVideo,
|
faVideo,
|
||||||
|
faTerminal,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useToast } from "../lib/toast";
|
import { useToast } from "../lib/toast";
|
||||||
import { getServerStatus, type ServerStatus } from "../lib/llm";
|
import { getServerStatus, type ServerStatus } from "../lib/llm";
|
||||||
import { clearHistory } from "../lib/history";
|
import { clearHistory } from "../lib/history";
|
||||||
import { useI18n } from "../lib/i18n";
|
import { useI18n } from "../lib/i18n";
|
||||||
import { useVideoAiLinks } from "../lib/useVideoAiLinks";
|
import { useVideoAiLinks } from "../lib/useVideoAiLinks";
|
||||||
|
import { useSystemPrompts, type SystemPrompts } from "../lib/useSystemPrompts";
|
||||||
import type { VideoAiLink } from "../lib/types";
|
import type { VideoAiLink } from "../lib/types";
|
||||||
|
|
||||||
const DRAFT_KEY = "melodymuse-draft";
|
const DRAFT_KEY = "melodymuse-draft";
|
||||||
@@ -47,12 +49,22 @@ export function SettingsPage() {
|
|||||||
const { links, loading: linksLoading, saveLinks, clearLocalLinks, hasLocal } = useVideoAiLinks();
|
const { links, loading: linksLoading, saveLinks, clearLocalLinks, hasLocal } = useVideoAiLinks();
|
||||||
const [editingLinks, setEditingLinks] = useState<VideoAiLink[] | null>(null);
|
const [editingLinks, setEditingLinks] = useState<VideoAiLink[] | null>(null);
|
||||||
|
|
||||||
|
const { prompts, loading: promptsLoading, savePrompts, resetPrompts } = useSystemPrompts();
|
||||||
|
const [editingPrompts, setEditingPrompts] = useState<SystemPrompts | null>(null);
|
||||||
|
const [activePromptTab, setActivePromptTab] = useState<keyof SystemPrompts>("all");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!linksLoading && editingLinks === null) {
|
if (!linksLoading && editingLinks === null) {
|
||||||
setEditingLinks(links);
|
setEditingLinks(links);
|
||||||
}
|
}
|
||||||
}, [links, linksLoading, editingLinks]);
|
}, [links, linksLoading, editingLinks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!promptsLoading && editingPrompts === null && prompts) {
|
||||||
|
setEditingPrompts(prompts);
|
||||||
|
}
|
||||||
|
}, [prompts, promptsLoading, editingPrompts]);
|
||||||
|
|
||||||
const check = async (signal?: AbortSignal) => {
|
const check = async (signal?: AbortSignal) => {
|
||||||
setChecking(true);
|
setChecking(true);
|
||||||
setStatusError(null);
|
setStatusError(null);
|
||||||
@@ -122,6 +134,32 @@ export function SettingsPage() {
|
|||||||
toast.info("Restored default links");
|
toast.info("Restored default links");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePromptChange = (val: string) => {
|
||||||
|
if (!editingPrompts) return;
|
||||||
|
setEditingPrompts({ ...editingPrompts, [activePromptTab]: val });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSavePrompts = async () => {
|
||||||
|
if (!editingPrompts) return;
|
||||||
|
const ok = await savePrompts(editingPrompts);
|
||||||
|
if (ok) {
|
||||||
|
toast.success("System Prompts saved to server");
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to save prompts");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetPrompts = async () => {
|
||||||
|
if (!window.confirm("Restore default System Prompts? This will overwrite your changes.")) return;
|
||||||
|
const ok = await resetPrompts();
|
||||||
|
if (ok) {
|
||||||
|
setEditingPrompts(null); // Will re-sync with hook
|
||||||
|
toast.info("Restored default system prompts");
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to restore prompts");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<header className="sticky top-0 z-20 bg-bg/70 backdrop-blur border-b border-border">
|
<header className="sticky top-0 z-20 bg-bg/70 backdrop-blur border-b border-border">
|
||||||
@@ -299,6 +337,64 @@ export function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<hr className="border-border" />
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-sm font-semibold text-fg mb-3 flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faTerminal} className="w-4 h-4 text-accent-primary" />
|
||||||
|
System Prompts
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleResetPrompts}
|
||||||
|
className="text-[10px] uppercase font-bold text-fg-muted hover:text-fg transition-colors"
|
||||||
|
>
|
||||||
|
Restore Defaults
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-fg-muted mb-4">
|
||||||
|
Edit the prompts used by the LLM. Use <code className="font-mono text-accent-secondary">{"{{VIDEO_TOOLS}}"}</code> as a placeholder for where the Video AI links should be injected in the <code className="font-mono">all</code> and <code className="font-mono">partial</code> prompts.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{promptsLoading || editingPrompts === null ? (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<FontAwesomeIcon icon={faCircleNotch} spin className="text-fg-muted w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<select
|
||||||
|
value={activePromptTab}
|
||||||
|
onChange={(e) => setActivePromptTab(e.target.value as keyof SystemPrompts)}
|
||||||
|
className="input py-1.5 text-xs w-full"
|
||||||
|
>
|
||||||
|
<option value="all">Full Generation (all)</option>
|
||||||
|
<option value="partial">Partial Generation (partial)</option>
|
||||||
|
<option value="style_normal">Style: Normal (style_normal)</option>
|
||||||
|
<option value="style_crazy">Style: Crazy (style_crazy)</option>
|
||||||
|
<option value="lyrics_partial">Lyrics Rewrite (lyrics_partial)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
className="input w-full min-h-[300px] text-xs font-mono p-3 leading-relaxed"
|
||||||
|
value={editingPrompts[activePromptTab]}
|
||||||
|
onChange={(e) => handlePromptChange(e.target.value)}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSavePrompts}
|
||||||
|
className="btn-primary py-1 text-sm font-semibold px-6"
|
||||||
|
>
|
||||||
|
Save Prompts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="text-center pt-2">
|
<div className="text-center pt-2">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
|
|||||||
Reference in New Issue
Block a user