IMPROVE: cho phép lưu dự án và phục hồi dự án vừa tắt
This commit is contained in:
@@ -0,0 +1,868 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi" class="h-full bg-slate-950 text-slate-100 select-none">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MIDI AI Prompt Generator & Preset Builder</title>
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Font Awesome Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: #090d16; }
|
||||
::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #334155; }
|
||||
|
||||
/* Form Panels */
|
||||
.panel-bg {
|
||||
background: linear-gradient(180deg, #111827 0%, #0f172a 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="h-full flex flex-col bg-slate-950 text-slate-200 overflow-hidden">
|
||||
|
||||
<!-- TOP HEADER TOOLBAR -->
|
||||
<header class="h-14 bg-slate-900 border-b border-slate-800 px-5 flex items-center justify-between z-20 shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-gradient-to-tr from-cyan-600 to-indigo-600 flex items-center justify-center text-white shadow-lg shadow-cyan-950">
|
||||
<i class="fa-solid fa-wand-magic-sparkles text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-sm font-bold tracking-wide text-white flex items-center gap-2">
|
||||
MIDI AI PROMPT GENERATOR <span class="text-[10px] bg-cyan-950 text-cyan-400 border border-cyan-800/80 px-1.5 py-0.5 rounded font-mono">FULL BEAT COVERAGE</span>
|
||||
</h1>
|
||||
<p class="text-[11px] text-slate-400">Công cụ tạo mẫu Prompt nhạc lý ép AI sinh đủ nốt phủ kín số ô nhịp</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Header Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btnLoadDefaultPreset" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-semibold flex items-center gap-1.5 border border-slate-700 transition-colors">
|
||||
<i class="fa-solid fa-rotate-left text-cyan-400"></i> Nạp Mẫu Piano
|
||||
</button>
|
||||
<button id="btnExportJSON" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-semibold flex items-center gap-1.5 border border-slate-700 transition-colors">
|
||||
<i class="fa-solid fa-download"></i> Xuất Preset JSON
|
||||
</button>
|
||||
<label id="btnImportJSON" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-semibold flex items-center gap-1.5 border border-slate-700 cursor-pointer transition-colors">
|
||||
<i class="fa-solid fa-upload"></i> Nhập JSON
|
||||
<input type="file" id="jsonFileInput" accept=".json" class="hidden">
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- MAIN WORKSPACE -->
|
||||
<main class="flex-1 grid grid-cols-12 overflow-hidden">
|
||||
|
||||
<!-- LEFT COLUMN: FORM CONTROLS (WIDTH: 7 COLS) -->
|
||||
<div class="col-span-7 border-r border-slate-800/80 p-5 overflow-y-auto space-y-5 custom-scrollbar bg-slate-950/60">
|
||||
|
||||
<!-- 1. VAI TRÒ & PHONG CÁCH -->
|
||||
<section class="panel-bg rounded-xl p-4 space-y-3">
|
||||
<div class="flex items-center gap-2 text-xs font-bold text-cyan-400 uppercase tracking-wider border-b border-slate-800/80 pb-2">
|
||||
<i class="fa-solid fa-user-ninja"></i> 1. Vai Trò Nghệ Sĩ & Phong Cách
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<label class="block text-slate-400 mb-1 font-medium">Vai trò / Chuyên môn:</label>
|
||||
<input type="text" id="inputRole" list="roleDatalist" class="w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-cyan-500 font-sans" value="nghệ sĩ piano chuyên nghiệp và là nhà soạn nhạc phim" placeholder="Chọn hoặc nhập vai trò mới...">
|
||||
<datalist id="roleDatalist"></datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-slate-400 mb-1 font-medium">Hành động / Thể loại đoạn nhạc:</label>
|
||||
<input type="text" id="inputActionDesc" list="actionDatalist" class="w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-cyan-500 font-sans" value="tiếp tục bản nhạc piano đầy cảm xúc này" placeholder="Chọn hoặc nhập hành động mới...">
|
||||
<datalist id="actionDatalist"></datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<label class="block text-slate-400 mb-1 font-medium">Loại Phân đoạn (Section Type):</label>
|
||||
<input type="text" id="inputSectionType" list="sectionTypeDatalist" class="w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-cyan-500 font-sans" value="Cao trào (Build-Up) kịch tính" placeholder="Chọn hoặc nhập phân đoạn mới...">
|
||||
<datalist id="sectionTypeDatalist"></datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-slate-400 mb-1 font-medium">Tên Tool AI Gọi (Function Tool Name):</label>
|
||||
<input type="text" id="inputToolName" class="w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-cyan-400 font-mono focus:outline-none focus:border-cyan-500" value="generate_multitrack_midi">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Ô NHỊP & PHÁCH (BARS & BEATS COMPUTATION) -->
|
||||
<section class="panel-bg rounded-xl p-4 space-y-3">
|
||||
<div class="flex items-center justify-between border-b border-slate-800/80 pb-2">
|
||||
<div class="flex items-center gap-2 text-xs font-bold text-amber-400 uppercase tracking-wider">
|
||||
<i class="fa-solid fa-stopwatch"></i> 2. Ô Nhịp & Phách (Bars & Beats Auto-Calc)
|
||||
</div>
|
||||
<span class="text-[10px] font-mono bg-amber-950/80 border border-amber-800/60 text-amber-300 px-2 py-0.5 rounded">Auto-Computed</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-3 text-xs">
|
||||
<div>
|
||||
<label class="block text-slate-400 mb-1 font-medium">Ô nhịp Bắt đầu:</label>
|
||||
<input type="number" id="inputStartBar" min="1" max="999" value="9" class="w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-amber-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-slate-400 mb-1 font-medium">Ô nhịp Kết thúc:</label>
|
||||
<input type="number" id="inputEndBar" min="1" max="999" value="16" class="w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-amber-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-slate-400 mb-1 font-medium">Số phách/Ô nhịp (Time Sig):</label>
|
||||
<select id="selectBeatsPerBar" class="w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-2 text-amber-300 font-mono focus:outline-none focus:border-amber-500">
|
||||
<option value="4">4 Phách (Nhịp 4/4)</option>
|
||||
<option value="3">3 Phách (Nhịp 3/4 Waltz)</option>
|
||||
<option value="6">6 Phách (Nhịp 6/8)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-slate-400 mb-1 font-medium">Quy đổi Tổng Số Phách:</label>
|
||||
<div id="readoutTotalBeats" class="w-full bg-slate-950 border border-amber-900/60 rounded-lg px-3 py-2 text-amber-400 font-mono font-bold">
|
||||
64.0 Phách
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-[11px] font-mono bg-slate-950 p-2.5 rounded-lg border border-slate-800 text-slate-400 flex items-center justify-between">
|
||||
<span>Chuỗi Ô nhịp tự động sinh:</span>
|
||||
<span id="readoutBarList" class="text-cyan-300 font-bold">9, 10, 11, 12, 13, 14, 15 và 16</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. MẬT ĐỘ NỐT & QUY TẮC HIỆU NĂNG -->
|
||||
<section class="panel-bg rounded-xl p-4 space-y-3">
|
||||
<div class="flex items-center gap-2 text-xs font-bold text-emerald-400 uppercase tracking-wider border-b border-slate-800/80 pb-2">
|
||||
<i class="fa-solid fa-gauge-simple-high"></i> 3. Mật Độ Nốt & Giới Hạn Token
|
||||
</div>
|
||||
|
||||
<div class="text-xs">
|
||||
<label class="block text-slate-400 mb-1 font-medium">Hướng dẫn Mật độ Nốt (Note Density Instruction):</label>
|
||||
<textarea id="inputDensityRule" rows="2" class="w-full bg-slate-900 border border-slate-800 rounded-lg p-3 text-white font-sans focus:outline-none focus:border-emerald-500 leading-relaxed text-xs">Hãy giữ mật độ nốt hiệu quả (chủ yếu là nốt đen và các đoạn rải hợp âm nốt móc đơn đều đặn, tránh các chuỗi nốt móc đôi/móc ba quá dày đặc) để tránh bị quá giới hạn thời gian xuất dữ liệu (token timeout).</textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. DANH SÁCH TRACKS (DYNAMIC TRACKS EDITOR) -->
|
||||
<section class="panel-bg rounded-xl p-4 space-y-3">
|
||||
<div class="flex items-center justify-between border-b border-slate-800/80 pb-2">
|
||||
<div class="flex items-center gap-2 text-xs font-bold text-indigo-400 uppercase tracking-wider">
|
||||
<i class="fa-solid fa-layer-group"></i> 4. Danh Sách Track Cấu Trúc (<span id="trackCountBadge">3</span> Tracks)
|
||||
</div>
|
||||
<button id="btnAddTrack" class="px-2.5 py-1 bg-indigo-600 hover:bg-indigo-500 text-white rounded text-xs font-bold transition-all flex items-center gap-1 shadow-md shadow-indigo-950">
|
||||
<i class="fa-solid fa-plus text-[10px]"></i> Thêm Track
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Datalists for Track Names and Track Labels History -->
|
||||
<datalist id="trackNameDatalist"></datalist>
|
||||
<datalist id="trackLabelDatalist"></datalist>
|
||||
|
||||
<!-- Tracks Container -->
|
||||
<div id="tracksContainer" class="space-y-3">
|
||||
<!-- Dynamic Track Cards Rendered via JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN: LIVE PROMPT PREVIEW & COPY (WIDTH: 5 COLS) -->
|
||||
<div class="col-span-5 bg-slate-900 flex flex-col overflow-hidden relative">
|
||||
|
||||
<!-- Preview Header -->
|
||||
<div class="h-10 bg-slate-950 border-b border-slate-800 px-4 flex items-center justify-between text-xs font-mono">
|
||||
<span class="text-slate-300 font-bold flex items-center gap-2">
|
||||
<i class="fa-solid fa-code text-cyan-400"></i> LIVE PROMPT PREVIEW
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btnCopyPrompt" class="px-2.5 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 font-sans font-semibold text-xs rounded flex items-center gap-1 transition-all border border-slate-700" title="Sao Chép Văn Bản">
|
||||
<i class="fa-solid fa-copy text-cyan-400"></i> <span>Sao Chép</span>
|
||||
</button>
|
||||
<button id="btnSavePresetModal" class="px-3 py-1 bg-purple-600 hover:bg-purple-500 text-white font-sans font-bold text-xs rounded flex items-center gap-1.5 transition-all shadow-md shadow-purple-950">
|
||||
<i class="fa-solid fa-bookmark"></i> <span>Lưu thành Preset</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Generated Text Output Box -->
|
||||
<div class="flex-1 p-4 overflow-y-auto custom-scrollbar">
|
||||
<div id="promptOutputText" class="w-full h-full bg-slate-950 border border-slate-800 rounded-xl p-4 font-mono text-xs text-slate-200 leading-relaxed whitespace-pre-wrap select-text selection:bg-cyan-900 selection:text-white">
|
||||
<!-- Text generated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div id="toastNotification" class="absolute bottom-5 right-5 bg-emerald-600 text-white px-4 py-2.5 rounded-xl shadow-2xl font-semibold text-xs flex items-center gap-2 transition-all duration-300 transform translate-y-10 opacity-0 pointer-events-none z-50">
|
||||
<i class="fa-solid fa-circle-check text-base"></i>
|
||||
<span id="toastMessage">Đã sao chép câu Prompt vào bộ nhớ tạm!</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- MODAL: AI PROMPT PRESET MANAGER (FORM TẠO PRESET MỚI) -->
|
||||
<div id="presetManagerModal" class="fixed inset-0 bg-black/80 backdrop-blur-md z-50 flex items-center justify-center hidden p-4">
|
||||
<div class="w-full max-w-3xl bg-[#13141a] border border-slate-800 rounded-2xl shadow-2xl p-6 space-y-4 text-slate-200 relative overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-slate-800/80 pb-3">
|
||||
<h2 class="text-sm font-bold tracking-widest text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-pink-400 to-indigo-400 font-mono uppercase">
|
||||
AI PROMPT PRESET MANAGER
|
||||
</h2>
|
||||
<button id="btnCloseModalX" class="w-7 h-7 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white flex items-center justify-center transition-colors">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Section Title -->
|
||||
<div class="text-xs font-bold text-slate-300 uppercase tracking-wider font-mono flex items-center gap-2">
|
||||
<i class="fa-solid fa-wand-magic-sparkles text-purple-400"></i> TẠO PRESET MỚI
|
||||
</div>
|
||||
|
||||
<!-- Preset Form Grid -->
|
||||
<div class="space-y-3 text-xs">
|
||||
|
||||
<!-- Row 1: Name & Category -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">TÊN PRESET</label>
|
||||
<input type="text" id="presetNameInput" class="w-full bg-[#1a1c23] border border-slate-800 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-purple-500 font-sans" placeholder="Nhập tên preset (VD: Epic Orchestra Intro)...">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">DANH MỤC</label>
|
||||
<input type="text" id="presetCategoryInput" list="categoryDatalist" class="w-full bg-[#1a1c23] border border-slate-800 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-purple-500 font-sans" value="Orchestral / Film Score" placeholder="Chọn hoặc nhập danh mục...">
|
||||
<datalist id="categoryDatalist">
|
||||
<option value="Orchestral / Film Score"></option>
|
||||
<option value="Piano Solo"></option>
|
||||
<option value="EDM / Pop"></option>
|
||||
<option value="Cinematic"></option>
|
||||
<option value="Jazz & Swing"></option>
|
||||
<option value="Lo-fi Chillhop"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Keywords -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">TỪ KHÓA KÍCH HOẠT (NGĂN CÁCH BẰNG DẤU PHẨY)</label>
|
||||
<input type="text" id="presetKeywordsInput" class="w-full bg-[#1a1c23] border border-slate-800 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-purple-500 font-sans" placeholder="Ví dụ: epic orchestra, hoành tráng, nhạc phim epic">
|
||||
</div>
|
||||
|
||||
<!-- Row 3: Bars, BPM, Scale -->
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">SỐ BARS MẶC ĐỊNH</label>
|
||||
<input type="number" id="presetBarsInput" value="8" class="w-full bg-[#1a1c23] border border-slate-800 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-purple-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">BPM MẶC ĐỊNH</label>
|
||||
<input type="number" id="presetBpmInput" value="120" class="w-full bg-[#1a1c23] border border-slate-800 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-purple-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">ÂM GIAI (SCALE) MẶC ĐỊNH</label>
|
||||
<input type="text" id="presetScaleInput" value="C Minor" class="w-full bg-[#1a1c23] border border-slate-800 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-purple-500" placeholder="VD: C Minor, A Major...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 4: System Prompt Template -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">SYSTEM PROMPT TEMPLATE / LUẬT SOẠN NHẠC</label>
|
||||
<textarea id="presetTemplateTextarea" rows="6" class="w-full bg-[#1a1c23] border border-slate-800 rounded-lg p-3 text-white font-mono text-xs focus:outline-none focus:border-purple-500 leading-relaxed custom-scrollbar"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons Row -->
|
||||
<div class="flex items-center justify-between pt-2 border-t border-slate-800/80">
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btnModalBack" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-semibold transition-colors">
|
||||
Quay lại
|
||||
</button>
|
||||
<button id="btnSavePresetSubmit" class="px-5 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded-lg text-xs font-bold transition-all shadow-lg shadow-purple-950/80 flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-floppy-disk"></i> <span>Lưu Preset</span>
|
||||
</button>
|
||||
</div>
|
||||
<button id="btnCloseModalBottom" class="px-4 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-semibold transition-colors">
|
||||
Đóng
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JAVASCRIPT LOGIC & STATE ENGINE -->
|
||||
<script>
|
||||
// Default Template State matching User Specification
|
||||
const state = {
|
||||
role: "nghệ sĩ piano chuyên nghiệp và là nhà soạn nhạc phim",
|
||||
actionDesc: "tiếp tục bản nhạc piano đầy cảm xúc này",
|
||||
sectionType: "Cao trào (Build-Up) kịch tính",
|
||||
startBar: 9,
|
||||
endBar: 16,
|
||||
beatsPerBar: 4,
|
||||
densityRule: "Hãy giữ mật độ nốt hiệu quả (chủ yếu là nốt đen và các đoạn rải hợp âm nốt móc đơn đều đặn, tránh các chuỗi nốt móc đôi/móc ba quá dày đặc) để tránh bị quá giới hạn thời gian xuất dữ liệu (token timeout).",
|
||||
toolName: "generate_multitrack_midi",
|
||||
tracks: [
|
||||
{
|
||||
id: "track_1",
|
||||
name: "Piano Bass & Octaves",
|
||||
label: "Tiếng Bass & Quãng 8 Piano",
|
||||
description: "đánh các xung bass trầm nốt đen đều đặn (trong dải C1-C3) liên tục trên mỗi phách từ phách 0.0 đến phách 64.0 (tổng cộng 64 nốt đen)."
|
||||
},
|
||||
{
|
||||
id: "track_2",
|
||||
name: "Piano Accompaniment",
|
||||
label: "Tiếng Đệm Piano",
|
||||
description: "đánh các đoạn rải hợp âm (broken chord arpeggios) nốt móc đơn có nhịp điệu ở âm vực trung (dải C3-C4), lấp đầy liên tục toàn bộ 8 ô nhịp từ phách 0.0 đến phách 64.0."
|
||||
},
|
||||
{
|
||||
id: "track_3",
|
||||
name: "Piano Melody - Right Hand",
|
||||
label: "Giai điệu Piano - Tay phải",
|
||||
description: "đánh một tuyến giai điệu đi lên ở âm vực cao (dải C4-C6) sử dụng nốt trắng và nốt tròn trải dài từ Ô nhịp 9 đến 16, kết thúc bằng một hợp âm chủ (tonic chord) ngân dài được giữ cho đến phách 64.0."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// History & Dropdown Options Management
|
||||
const defaultHistory = {
|
||||
roles: [
|
||||
"nghệ sĩ piano chuyên nghiệp và là nhà soạn nhạc phim",
|
||||
"nhà sản xuất âm nhạc EDM / Electro Pop",
|
||||
"nhà soạn nhạc phim Epic Orchestra hoành tráng",
|
||||
"nghệ sĩ nhạc Jazz ngẫu hứng chuyên nghiệp",
|
||||
"nhà sản xuất nhạc Lo-fi Chillhop & Beatmaker"
|
||||
],
|
||||
actions: [
|
||||
"tiếp tục bản nhạc piano đầy cảm xúc này",
|
||||
"tạo một đoạn nhạc dồn dập kịch tính",
|
||||
"phát triển giai điệu mở đầu du dương",
|
||||
"xây dựng đoạn điệp khúc bùng nổ và hoành tráng"
|
||||
],
|
||||
sectionTypes: [
|
||||
"Cao trào (Build-Up) kịch tính",
|
||||
"Mở đầu (Intro) du dương",
|
||||
"Điệp khúc (Chorus / Climax) bùng nổ",
|
||||
"Đoạn lắng (Breakdown / Interlude)",
|
||||
"Đoạn kết (Outro / Resolution)"
|
||||
],
|
||||
trackNames: [
|
||||
"Piano Bass & Octaves",
|
||||
"Piano Accompaniment",
|
||||
"Piano Melody - Right Hand",
|
||||
"Strings Ensemble",
|
||||
"Brass Theme",
|
||||
"Epic Percussion",
|
||||
"Synth Lead",
|
||||
"Bassline"
|
||||
],
|
||||
trackLabels: [
|
||||
"Tiếng Bass & Quãng 8 Piano",
|
||||
"Tiếng Đệm Piano",
|
||||
"Giai điệu Piano - Tay phải",
|
||||
"Dàn dây Ostinato",
|
||||
"Tuyến Kèn Brass",
|
||||
"Bộ Trống Epic",
|
||||
"Giai điệu Synth",
|
||||
"Tuyến Bass Sub"
|
||||
]
|
||||
};
|
||||
|
||||
// DOM References
|
||||
const inputRole = document.getElementById('inputRole');
|
||||
const inputActionDesc = document.getElementById('inputActionDesc');
|
||||
const inputSectionType = document.getElementById('inputSectionType');
|
||||
const inputToolName = document.getElementById('inputToolName');
|
||||
const roleDatalist = document.getElementById('roleDatalist');
|
||||
const actionDatalist = document.getElementById('actionDatalist');
|
||||
const sectionTypeDatalist = document.getElementById('sectionTypeDatalist');
|
||||
const inputStartBar = document.getElementById('inputStartBar');
|
||||
const inputEndBar = document.getElementById('inputEndBar');
|
||||
const selectBeatsPerBar = document.getElementById('selectBeatsPerBar');
|
||||
const inputDensityRule = document.getElementById('inputDensityRule');
|
||||
const readoutTotalBeats = document.getElementById('readoutTotalBeats');
|
||||
const readoutBarList = document.getElementById('readoutBarList');
|
||||
const tracksContainer = document.getElementById('tracksContainer');
|
||||
const trackCountBadge = document.getElementById('trackCountBadge');
|
||||
const promptOutputText = document.getElementById('promptOutputText');
|
||||
const btnAddTrack = document.getElementById('btnAddTrack');
|
||||
const btnCopyPrompt = document.getElementById('btnCopyPrompt');
|
||||
const btnLoadDefaultPreset = document.getElementById('btnLoadDefaultPreset');
|
||||
const btnExportJSON = document.getElementById('btnExportJSON');
|
||||
const jsonFileInput = document.getElementById('jsonFileInput');
|
||||
const toastNotification = document.getElementById('toastNotification');
|
||||
|
||||
// Preset Modal Elements
|
||||
const presetManagerModal = document.getElementById('presetManagerModal');
|
||||
const btnSavePresetModal = document.getElementById('btnSavePresetModal');
|
||||
const btnCloseModalX = document.getElementById('btnCloseModalX');
|
||||
const btnCloseModalBottom = document.getElementById('btnCloseModalBottom');
|
||||
const btnModalBack = document.getElementById('btnModalBack');
|
||||
const btnSavePresetSubmit = document.getElementById('btnSavePresetSubmit');
|
||||
const presetNameInput = document.getElementById('presetNameInput');
|
||||
const presetCategoryInput = document.getElementById('presetCategoryInput');
|
||||
const presetKeywordsInput = document.getElementById('presetKeywordsInput');
|
||||
const presetBarsInput = document.getElementById('presetBarsInput');
|
||||
const presetBpmInput = document.getElementById('presetBpmInput');
|
||||
const presetScaleInput = document.getElementById('presetScaleInput');
|
||||
const presetTemplateTextarea = document.getElementById('presetTemplateTextarea');
|
||||
|
||||
// LocalStorage Helper Functions for History
|
||||
function getHistory(key, defaults) {
|
||||
try {
|
||||
const saved = localStorage.getItem(`midi_prompt_history_${key}`);
|
||||
return saved ? JSON.parse(saved) : defaults;
|
||||
} catch (e) {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
function saveHistoryItem(key, newValue, defaults) {
|
||||
const val = (newValue || "").trim();
|
||||
if (!val) return;
|
||||
let list = getHistory(key, defaults);
|
||||
if (!list.includes(val)) {
|
||||
list.unshift(val); // Put newest entry first
|
||||
try {
|
||||
localStorage.setItem(`midi_prompt_history_${key}`, JSON.stringify(list));
|
||||
} catch (e) {}
|
||||
renderDatalistOptions(key, list);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDatalistOptions(key, list) {
|
||||
let datalistElem;
|
||||
if (key === 'roles') datalistElem = roleDatalist;
|
||||
else if (key === 'actions') datalistElem = actionDatalist;
|
||||
else if (key === 'sectionTypes') datalistElem = sectionTypeDatalist;
|
||||
else if (key === 'trackNames') datalistElem = document.getElementById('trackNameDatalist');
|
||||
else if (key === 'trackLabels') datalistElem = document.getElementById('trackLabelDatalist');
|
||||
|
||||
if (!datalistElem) return;
|
||||
datalistElem.innerHTML = list.map(item => `<option value="${escapeHtml(item)}"></option>`).join('');
|
||||
}
|
||||
|
||||
function initHistoryDatalists() {
|
||||
const roles = getHistory('roles', defaultHistory.roles);
|
||||
const actions = getHistory('actions', defaultHistory.actions);
|
||||
const sectionTypes = getHistory('sectionTypes', defaultHistory.sectionTypes);
|
||||
const trackNames = getHistory('trackNames', defaultHistory.trackNames);
|
||||
const trackLabels = getHistory('trackLabels', defaultHistory.trackLabels);
|
||||
|
||||
renderDatalistOptions('roles', roles);
|
||||
renderDatalistOptions('actions', actions);
|
||||
renderDatalistOptions('sectionTypes', sectionTypes);
|
||||
renderDatalistOptions('trackNames', trackNames);
|
||||
renderDatalistOptions('trackLabels', trackLabels);
|
||||
}
|
||||
|
||||
// Auto-save history on input change/blur
|
||||
inputRole.addEventListener('change', () => saveHistoryItem('roles', inputRole.value, defaultHistory.roles));
|
||||
inputActionDesc.addEventListener('change', () => saveHistoryItem('actions', inputActionDesc.value, defaultHistory.actions));
|
||||
inputSectionType.addEventListener('change', () => saveHistoryItem('sectionTypes', inputSectionType.value, defaultHistory.sectionTypes));
|
||||
|
||||
// Helper to calculate total bars, total beats, and format bar range list
|
||||
function computeTimeMetrics() {
|
||||
const start = parseInt(inputStartBar.value) || 1;
|
||||
const end = parseInt(inputEndBar.value) || 1;
|
||||
const beatsPerBar = parseInt(selectBeatsPerBar.value) || 4;
|
||||
|
||||
const barCount = Math.max(1, (end - start) + 1);
|
||||
const totalBeats = (barCount * beatsPerBar).toFixed(1);
|
||||
|
||||
// Format bar list string: e.g. "9, 10, 11, 12, 13, 14, 15 và 16"
|
||||
const barNumbers = [];
|
||||
for (let i = start; i <= end; i++) {
|
||||
barNumbers.push(i);
|
||||
}
|
||||
|
||||
let barListStr = "";
|
||||
if (barNumbers.length === 1) {
|
||||
barListStr = `${barNumbers[0]}`;
|
||||
} else if (barNumbers.length === 2) {
|
||||
barListStr = `${barNumbers[0]} và ${barNumbers[1]}`;
|
||||
} else {
|
||||
const leading = barNumbers.slice(0, -1).join(', ');
|
||||
const last = barNumbers[barNumbers.length - 1];
|
||||
barListStr = `${leading} và ${last}`;
|
||||
}
|
||||
|
||||
readoutTotalBeats.innerText = `${totalBeats} Phách`;
|
||||
readoutBarList.innerText = barListStr;
|
||||
|
||||
return {
|
||||
barCount,
|
||||
totalBeats,
|
||||
start,
|
||||
end,
|
||||
barListStr
|
||||
};
|
||||
}
|
||||
|
||||
// Render Dynamic Track Input Cards
|
||||
function renderTrackEditors() {
|
||||
tracksContainer.innerHTML = "";
|
||||
trackCountBadge.innerText = state.tracks.length;
|
||||
|
||||
state.tracks.forEach((track, idx) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = "bg-slate-900/90 border border-slate-800 p-3 rounded-xl space-y-2 relative group";
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="flex items-center justify-between text-xs font-bold text-slate-300">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="w-5 h-5 rounded-full bg-slate-800 border border-slate-700 flex items-center justify-center text-[10px] text-cyan-400 font-mono">${idx + 1}</span>
|
||||
Track ${idx + 1}
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button onclick="moveTrack(${idx}, -1)" class="w-6 h-6 rounded bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white flex items-center justify-center transition-colors" title="Di chuyển lên">
|
||||
<i class="fa-solid fa-chevron-up text-[10px]"></i>
|
||||
</button>
|
||||
<button onclick="moveTrack(${idx}, 1)" class="w-6 h-6 rounded bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white flex items-center justify-center transition-colors" title="Di chuyển xuống">
|
||||
<i class="fa-solid fa-chevron-down text-[10px]"></i>
|
||||
</button>
|
||||
<button onclick="removeTrack(${idx})" class="w-6 h-6 rounded bg-red-950/80 hover:bg-red-900 text-red-400 flex items-center justify-center transition-colors ml-1" title="Xóa Track">
|
||||
<i class="fa-solid fa-trash-can text-[10px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-500 font-mono mb-0.5">Tên Track (Mã Tiếng Anh):</label>
|
||||
<input type="text" list="trackNameDatalist" value="${escapeHtml(track.name)}" oninput="updateTrackData(${idx}, 'name', this.value)" onchange="saveHistoryItem('trackNames', this.value, defaultHistory.trackNames); updateTrackData(${idx}, 'name', this.value)" class="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-white font-mono text-xs focus:outline-none focus:border-cyan-500" placeholder="Chọn hoặc nhập tên track...">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-500 font-mono mb-0.5">Nhãn / Chú Thích Tiếng Việt:</label>
|
||||
<input type="text" list="trackLabelDatalist" value="${escapeHtml(track.label)}" oninput="updateTrackData(${idx}, 'label', this.value)" onchange="saveHistoryItem('trackLabels', this.value, defaultHistory.trackLabels); updateTrackData(${idx}, 'label', this.value)" class="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-300 text-xs focus:outline-none focus:border-cyan-500" placeholder="Chọn hoặc nhập nhãn chú thích...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs">
|
||||
<label class="block text-[10px] text-slate-500 font-mono mb-0.5">Mô tả Quy cách Đánh & Tiết tấu:</label>
|
||||
<textarea rows="2" oninput="updateTrackData(${idx}, 'description', this.value)" class="w-full bg-slate-950 border border-slate-800 rounded p-2 text-white font-sans text-xs focus:outline-none focus:border-cyan-500 leading-relaxed">${escapeHtml(track.description)}</textarea>
|
||||
</div>
|
||||
`;
|
||||
tracksContainer.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return (str || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
|
||||
// Main Prompt Generator
|
||||
function generatePromptText() {
|
||||
const timeData = computeTimeMetrics();
|
||||
const role = inputRole.value.trim();
|
||||
const actionDesc = inputActionDesc.value.trim();
|
||||
const sectionType = inputSectionType.value.trim();
|
||||
const toolName = inputToolName.value.trim();
|
||||
const densityRule = inputDensityRule.value.trim();
|
||||
const trackCount = state.tracks.length;
|
||||
|
||||
let promptStr = `Bạn là ${role}. Hãy ${actionDesc} bằng một đoạn ${sectionType} dài ${timeData.barCount} ô nhịp (Từ Ô nhịp ${timeData.start} đến ${timeData.end}, tương ứng với Phách 0.0 đến ${timeData.totalBeats}).\n\n`;
|
||||
|
||||
promptStr += `YÊU CẦU BẮT BUỘC VỀ ĐỘ DÀI TOÀN BỘ:\n\n`;
|
||||
promptStr += `Tất cả ${trackCount} track ĐỀU PHẢI tạo ra các nốt nhạc bao phủ toàn bộ khoảng từ phách 0.0 đến phách ${timeData.totalBeats}, không được dừng lại giữa chừng.\n\n`;
|
||||
promptStr += `Nhịp điệu phải liên tục không đứt đoạn qua các Ô nhịp ${timeData.barListStr}.\n\n`;
|
||||
promptStr += `Nốt/hợp âm cuối cùng của mỗi track phải kéo dài đến hoặc giải kết (resolve) tại phách ${timeData.totalBeats}.\n\n`;
|
||||
|
||||
if (densityRule) {
|
||||
promptStr += `${densityRule}\n\n`;
|
||||
}
|
||||
|
||||
promptStr += `Cấu trúc bắt buộc trả về thông qua công cụ ${toolName} gồm ${trackCount} track:\n\n`;
|
||||
|
||||
state.tracks.forEach((track, i) => {
|
||||
promptStr += `${track.name} (${track.label}): ${track.description.trim()}\n\n`;
|
||||
});
|
||||
|
||||
promptStr += `Đảm bảo mọi track đều chạm tới phách ${timeData.totalBeats}.`;
|
||||
|
||||
promptOutputText.innerText = promptStr;
|
||||
}
|
||||
|
||||
// Dynamic Track Actions
|
||||
window.updateTrackData = function(index, key, val) {
|
||||
state.tracks[index][key] = val;
|
||||
generatePromptText();
|
||||
};
|
||||
|
||||
window.removeTrack = function(index) {
|
||||
if (state.tracks.length <= 1) {
|
||||
showToast("Cần giữ ít nhất 1 Track!", "error");
|
||||
return;
|
||||
}
|
||||
state.tracks.splice(index, 1);
|
||||
renderTrackEditors();
|
||||
generatePromptText();
|
||||
};
|
||||
|
||||
window.moveTrack = function(index, direction) {
|
||||
const newIndex = index + direction;
|
||||
if (newIndex < 0 || newIndex >= state.tracks.length) return;
|
||||
const temp = state.tracks[index];
|
||||
state.tracks[index] = state.tracks[newIndex];
|
||||
state.tracks[newIndex] = temp;
|
||||
renderTrackEditors();
|
||||
generatePromptText();
|
||||
};
|
||||
|
||||
btnAddTrack.addEventListener('click', () => {
|
||||
const newId = `track_${Date.now()}`;
|
||||
state.tracks.push({
|
||||
id: newId,
|
||||
name: `Track ${state.tracks.length + 1}`,
|
||||
label: `Mô tả Track ${state.tracks.length + 1}`,
|
||||
description: `đánh tuyến nốt hòa âm liên tục từ phách 0.0 đến phách 64.0.`
|
||||
});
|
||||
renderTrackEditors();
|
||||
generatePromptText();
|
||||
});
|
||||
|
||||
// Toast Notification System
|
||||
function showToast(msg, type = "success") {
|
||||
const toastMsg = document.getElementById('toastMessage');
|
||||
toastMsg.innerText = msg;
|
||||
|
||||
if (type === "error") {
|
||||
toastNotification.classList.remove('bg-emerald-600');
|
||||
toastNotification.classList.add('bg-red-600');
|
||||
} else {
|
||||
toastNotification.classList.remove('bg-red-600');
|
||||
toastNotification.classList.add('bg-emerald-600');
|
||||
}
|
||||
|
||||
toastNotification.classList.remove('translate-y-10', 'opacity-0', 'pointer-events-none');
|
||||
setTimeout(() => {
|
||||
toastNotification.classList.add('translate-y-10', 'opacity-0', 'pointer-events-none');
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
// Open Save Preset Modal Event Handler
|
||||
btnSavePresetModal.addEventListener('click', () => {
|
||||
const timeData = computeTimeMetrics();
|
||||
const currentPromptText = promptOutputText.innerText;
|
||||
|
||||
// Auto-populate Preset Form Fields
|
||||
const sectionType = inputSectionType.value.trim() || "Building Section";
|
||||
const role = inputRole.value.trim() || "Composer";
|
||||
|
||||
presetNameInput.value = `${sectionType} (${timeData.barCount} Bars)`;
|
||||
presetCategoryInput.value = role.includes("piano") ? "Piano Solo" : "Orchestral / Film Score";
|
||||
presetKeywordsInput.value = `${sectionType.toLowerCase()}, ${role.toLowerCase().split(' ')[0]}, ${timeData.barCount} bars`;
|
||||
presetBarsInput.value = timeData.barCount;
|
||||
presetBpmInput.value = 120;
|
||||
presetScaleInput.value = "C Minor";
|
||||
presetTemplateTextarea.value = currentPromptText;
|
||||
|
||||
// Display Modal
|
||||
presetManagerModal.classList.remove('hidden');
|
||||
});
|
||||
|
||||
// Close Modal Event Handlers
|
||||
[btnCloseModalX, btnCloseModalBottom, btnModalBack].forEach(btn => {
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
presetManagerModal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Save Preset Action Handler (Saves to daw_ai_prompt_presets localStorage)
|
||||
btnSavePresetSubmit.addEventListener('click', () => {
|
||||
const name = presetNameInput.value.trim();
|
||||
const category = presetCategoryInput.value.trim() || "General";
|
||||
const keywordsRaw = presetKeywordsInput.value.trim();
|
||||
const defaultBars = parseInt(presetBarsInput.value) || 8;
|
||||
const defaultBpm = parseInt(presetBpmInput.value) || 120;
|
||||
const defaultScale = presetScaleInput.value.trim() || "C Minor";
|
||||
const template = presetTemplateTextarea.value.trim();
|
||||
|
||||
if (!name) {
|
||||
showToast("Vui lòng nhập Tên Preset!", "error");
|
||||
presetNameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
showToast("Nội dung System Prompt không được trống!", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const keywords = keywordsRaw ? keywordsRaw.split(',').map(k => k.trim()).filter(Boolean) : [name.toLowerCase()];
|
||||
|
||||
const newPresetObj = {
|
||||
id: `preset_${Date.now()}`,
|
||||
name: name,
|
||||
category: category,
|
||||
keywords: keywords,
|
||||
default_bars: defaultBars,
|
||||
default_bpm: defaultBpm,
|
||||
default_scale: defaultScale,
|
||||
system_instruction_template: template,
|
||||
is_user_defined: true,
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Read existing saved presets list from localStorage
|
||||
let savedPresets = [];
|
||||
try {
|
||||
const stored = localStorage.getItem('daw_ai_prompt_presets');
|
||||
savedPresets = stored ? JSON.parse(stored) : [];
|
||||
} catch (e) {
|
||||
savedPresets = [];
|
||||
}
|
||||
|
||||
savedPresets.unshift(newPresetObj);
|
||||
|
||||
try {
|
||||
localStorage.setItem('daw_ai_prompt_presets', JSON.stringify(savedPresets));
|
||||
showToast(`Đã lưu Preset "${name}" thành công!`);
|
||||
presetManagerModal.classList.add('hidden');
|
||||
} catch (e) {
|
||||
showToast("Lỗi khi lưu vào bộ nhớ trình duyệt!", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Copy Prompt Button
|
||||
btnCopyPrompt.addEventListener('click', () => {
|
||||
// Save current input values to history when copying
|
||||
saveHistoryItem('roles', inputRole.value, defaultHistory.roles);
|
||||
saveHistoryItem('actions', inputActionDesc.value, defaultHistory.actions);
|
||||
saveHistoryItem('sectionTypes', inputSectionType.value, defaultHistory.sectionTypes);
|
||||
|
||||
// Save tracks history
|
||||
state.tracks.forEach(track => {
|
||||
saveHistoryItem('trackNames', track.name, defaultHistory.trackNames);
|
||||
saveHistoryItem('trackLabels', track.label, defaultHistory.trackLabels);
|
||||
});
|
||||
|
||||
const textToCopy = promptOutputText.innerText;
|
||||
|
||||
// Fallback copy execCommand for iframe compatibility
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = textToCopy;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
showToast("Đã sao chép câu Prompt thành công!");
|
||||
} catch (err) {
|
||||
showToast("Lỗi khi sao chép!", "error");
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
});
|
||||
|
||||
// Reset Default Piano Preset
|
||||
btnLoadDefaultPreset.addEventListener('click', () => {
|
||||
inputRole.value = "nghệ sĩ piano chuyên nghiệp và là nhà soạn nhạc phim";
|
||||
inputActionDesc.value = "tiếp tục bản nhạc piano đầy cảm xúc này";
|
||||
inputSectionType.value = "Cao trào (Build-Up) kịch tính";
|
||||
inputToolName.value = "generate_multitrack_midi";
|
||||
inputStartBar.value = "9";
|
||||
inputEndBar.value = "16";
|
||||
selectBeatsPerBar.value = "4";
|
||||
inputDensityRule.value = "Hãy giữ mật độ nốt hiệu quả (chủ yếu là nốt đen và các đoạn rải hợp âm nốt móc đơn đều đặn, tránh các chuỗi nốt móc đôi/móc ba quá dày đặc) để tránh bị quá giới hạn thời gian xuất dữ liệu (token timeout).";
|
||||
|
||||
state.tracks = [
|
||||
{
|
||||
id: "track_1",
|
||||
name: "Piano Bass & Octaves",
|
||||
label: "Tiếng Bass & Quãng 8 Piano",
|
||||
description: "đánh các xung bass trầm nốt đen đều đặn (trong dải C1-C3) liên tục trên mỗi phách từ phách 0.0 đến phách 64.0 (tổng cộng 64 nốt đen)."
|
||||
},
|
||||
{
|
||||
id: "track_2",
|
||||
name: "Piano Accompaniment",
|
||||
label: "Tiếng Đệm Piano",
|
||||
description: "đánh các đoạn rải hợp âm (broken chord arpeggios) nốt móc đơn có nhịp điệu ở âm vực trung (dải C3-C4), lấp đầy liên tục toàn bộ 8 ô nhịp từ phách 0.0 đến phách 64.0."
|
||||
},
|
||||
{
|
||||
id: "track_3",
|
||||
name: "Piano Melody - Right Hand",
|
||||
label: "Giai điệu Piano - Tay phải",
|
||||
description: "đánh một tuyến giai điệu đi lên ở âm vực cao (dải C4-C6) sử dụng nốt trắng và nốt tròn trải dài từ Ô nhịp 9 đến 16, kết thúc bằng một hợp âm chủ (tonic chord) ngân dài được giữ cho đến phách 64.0."
|
||||
}
|
||||
];
|
||||
|
||||
renderTrackEditors();
|
||||
generatePromptText();
|
||||
showToast("Đã nạp lại Mẫu Prompt Piano!");
|
||||
});
|
||||
|
||||
// Export Preset JSON
|
||||
btnExportJSON.addEventListener('click', () => {
|
||||
const exportData = {
|
||||
role: inputRole.value,
|
||||
actionDesc: inputActionDesc.value,
|
||||
sectionType: inputSectionType.value,
|
||||
startBar: parseInt(inputStartBar.value),
|
||||
endBar: parseInt(inputEndBar.value),
|
||||
beatsPerBar: parseInt(selectBeatsPerBar.value),
|
||||
densityRule: inputDensityRule.value,
|
||||
toolName: inputToolName.value,
|
||||
tracks: state.tracks
|
||||
};
|
||||
|
||||
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportData, null, 2));
|
||||
const downloadAnchor = document.createElement('a');
|
||||
downloadAnchor.setAttribute("href", dataStr);
|
||||
downloadAnchor.setAttribute("download", `midi_prompt_preset_${Date.now()}.json`);
|
||||
document.body.appendChild(downloadAnchor);
|
||||
downloadAnchor.click();
|
||||
downloadAnchor.remove();
|
||||
showToast("Đã xuất tệp JSON Preset!");
|
||||
});
|
||||
|
||||
// Import Preset JSON
|
||||
jsonFileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const imported = JSON.parse(event.target.result);
|
||||
if (imported.role) inputRole.value = imported.role;
|
||||
if (imported.actionDesc) inputActionDesc.value = imported.actionDesc;
|
||||
if (imported.sectionType) inputSectionType.value = imported.sectionType;
|
||||
if (imported.startBar) inputStartBar.value = imported.startBar;
|
||||
if (imported.endBar) inputEndBar.value = imported.endBar;
|
||||
if (imported.beatsPerBar) selectBeatsPerBar.value = imported.beatsPerBar;
|
||||
if (imported.densityRule !== undefined) inputDensityRule.value = imported.densityRule;
|
||||
if (imported.toolName) inputToolName.value = imported.toolName;
|
||||
if (Array.isArray(imported.tracks)) state.tracks = imported.tracks;
|
||||
|
||||
renderTrackEditors();
|
||||
generatePromptText();
|
||||
showToast("Đã nạp Preset từ tệp JSON thành công!");
|
||||
} catch (err) {
|
||||
showToast("Tệp JSON không hợp lệ!", "error");
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
// Attach input listeners
|
||||
[inputRole, inputActionDesc, inputSectionType, inputToolName, inputStartBar, inputEndBar, selectBeatsPerBar, inputDensityRule].forEach(elem => {
|
||||
elem.addEventListener('input', generatePromptText);
|
||||
});
|
||||
|
||||
// Initial Render
|
||||
window.addEventListener('load', () => {
|
||||
initHistoryDatalists();
|
||||
renderTrackEditors();
|
||||
generatePromptText();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user