Files
SonicForgeStudio/md/49_AI_PROMPT_GENERATOR.md

46 KiB

<html lang="vi" class="h-full bg-slate-950 text-slate-100 select-none"> <head> <script src="https://cdn.tailwindcss.com"></script> <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>

MIDI AI PROMPT GENERATOR FULL BEAT COVERAGE

Công cụ tạo mẫu Prompt nhạc lý ép AI sinh đủ nốt phủ kín số ô nhịp

<!-- 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>
<!-- 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 shrink-0 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 flex flex-col min-h-0 p-4">
    <div id="promptOutputText" class="w-full flex-1 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 overflow-y-auto custom-scrollbar">
      <!-- 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>
  <!-- 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>
        <select id="presetCategorySelect" 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">
          <option value="Orchestral / Film Score">Orchestral / Film Score</option>
          <option value="Piano Solo">Piano Solo</option>
          <option value="EDM / Pop">EDM / Pop</option>
          <option value="Cinematic">Cinematic</option>
          <option value="Jazz & Swing">Jazz & Swing</option>
          <option value="Lo-fi Chillhop">Lo-fi Chillhop</option>
        </select>
      </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="space-y-3 pt-2 border-t border-slate-800/80">
    <div class="flex items-center justify-between">
      <button id="btnCreateStructuredPreset" class="px-4 py-2 bg-emerald-700 hover:bg-emerald-600 text-white rounded-lg text-xs font-bold transition-all shadow-md flex items-center gap-1.5">
        <i class="fa-solid fa-wand-magic-sparkles"></i> Tạo preset có cấu trúc
      </button>
      <div class="flex items-center gap-2">
        <button id="btnModalBack" class="px-4 py-2 bg-[#252836] 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>
    </div>
    <div class="flex justify-end">
      <button id="btnCloseModalBottom" class="px-4 py-1.5 bg-[#1f222e] hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-semibold transition-colors">
        Đóng
      </button>
    </div>
  </div>

</div>
<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 btnCreateStructuredPreset = document.getElementById('btnCreateStructuredPreset'); const presetNameInput = document.getElementById('presetNameInput'); const presetCategorySelect = document.getElementById('presetCategorySelect'); 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 => ``).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 = `
${idx + 1} Track ${idx + 1}
Tên Track (Mã Tiếng Anh):
Nhãn / Chú Thích Tiếng Việt:
Mô tả Quy cách Đánh & Tiết tấu: ${escapeHtml(track.description)}
      </div>
    `;
    tracksContainer.appendChild(card);
  });
}

function escapeHtml(str) {
  return (str || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

// 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 — post data to parent React app
btnSavePresetModal.addEventListener('click', () => {
  const timeData = computeTimeMetrics();
  const currentPromptText = promptOutputText.innerText.trim();

  const sectionType = inputSectionType.value.trim() || "Building Section";
  const role = inputRole.value.trim() || "Composer";

  // Auto-infer Category based on Role
  let category = "Orchestral / Film Score";
  const roleLower = role.toLowerCase();
  if (roleLower.includes("piano")) category = "Piano Solo";
  else if (roleLower.includes("edm") || roleLower.includes("electro")) category = "EDM / Pop";
  else if (roleLower.includes("jazz")) category = "Jazz & Swing";
  else if (roleLower.includes("lo-fi") || roleLower.includes("chillhop")) category = "Lo-fi Chillhop";

  // Auto-generate Keyword Tags
  const keywordList = [
    sectionType.toLowerCase(),
    roleLower.split(' ')[0],
    `${timeData.barCount} bars`
  ];
  const keywordsStr = keywordList.filter(Boolean).join(', ');

  // Post data to parent window (React AI PROMPT PRESET MANAGER)
  window.parent.postMessage({
    type: 'GENERATOR_PRESET_DATA',
    name: `${sectionType} (${timeData.barCount} Bars)`,
    category: category,
    keywords: keywordsStr,
    default_bars: timeData.barCount,
    default_bpm: 120,
    default_scale: "C Minor",
    template: currentPromptText
  }, '*');
});

// Close Modal Event Handlers
[btnCloseModalX, btnCloseModalBottom, btnModalBack].forEach(btn => {
  if (btn) {
    btn.addEventListener('click', () => {
      presetManagerModal.classList.add('hidden');
    });
  }
});

// Create Structured Preset — replace static numbers with template variables
btnCreateStructuredPreset.addEventListener('click', () => {
  let promptText = presetTemplateTextarea.value;
  const bars = presetBarsInput.value || "8";

  promptText = promptText
    .replace(new RegExp(`dài ${bars} ô nhịp`, 'g'), 'dài {{bars}} ô nhịp')
    .replace(/Từ Ô nhịp \d+ đến \d+/g, 'Từ Ô nhịp {{start_bar}} đến {{end_bar}}')
    .replace(/Phách 0\.0 đến \d+\.0/g, 'Phách 0.0 đến {{total_beats}}')
    .replace(/phách \d+\.0/g, 'phách {{total_beats}}');

  presetTemplateTextarea.value = promptText;
  showToast("Đã chuyển đổi Prompt thành mẫu biến cấu trúc!");
});

// Save Preset Action Handler (Saves to daw_ai_prompt_presets localStorage & dispatches event)
btnSavePresetSubmit.addEventListener('click', () => {
  const name = presetNameInput.value.trim();
  const category = presetCategorySelect.value;
  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));
    // Dispatch Custom Event so parent window updates preset list instantly
    window.dispatchEvent(new CustomEvent('AI_PRESET_SAVED', { detail: newPresetObj }));
    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>

</html>