FEAT: thêm nút Tạo preset với cấu trúc và lưu lại vào preset manager
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
# TECHNICAL SPECIFICATION: AUTOMATED DATA TRANSFER FROM MIDI PROMPT GENERATOR TO AI PRESET MANAGER
|
||||
|
||||
This document details the software implementation plan to automatically extract calculated parameters (Bars, BPM, Category, Keywords, Prompt Content) and auto-populate the AI Prompt Preset Manager form whenever a user clicks the "Save as Preset" button on the MIDI AI Prompt Generator tool, adhering strictly to the designated interface layout.
|
||||
|
||||
---
|
||||
|
||||
## 1. TECHNICAL GOALS
|
||||
|
||||
* **Zero Re-typing:** Eliminates manual copy-pasting of generated prompt text or re-entering parameters such as Bars, Role, and Category.
|
||||
* **Smart Data Mapping:**
|
||||
* Auto-suggests a **Preset Name** based on Section Type and Bar Count.
|
||||
* Auto-extracts **Trigger Keywords** from Role, Section Type, and Action Descriptions.
|
||||
* Auto-calculates **Default Bars** using the formula $(\text{EndBar} - \text{StartBar} + 1)$.
|
||||
* Overwrites the complete generated prompt text directly into the **SYSTEM PROMPT TEMPLATE** area.
|
||||
|
||||
|
||||
* **Structured Preset Generation ("Create Structured Preset" Green Button):** Enables converting static prompt text into a structured variable template containing dynamic placeholders (e.g., `{{start_bar}}`, `{{end_bar}}`, `{{total_beats}}`).
|
||||
|
||||
---
|
||||
|
||||
## 2. DATA MAPPING MATRIX
|
||||
|
||||
Upon clicking "Save as Preset", the system reads the active State from the Prompt Generator and maps it to the Modal fields:
|
||||
|
||||
| AI Prompt Preset Manager Field | Source Data from Prompt Generator | Processing Rules / Logic | Example Auto-Populated Data |
|
||||
| --- | --- | --- | --- |
|
||||
| **PRESET NAME** | `inputSectionType` + `barCount` | `${sectionType} (${barCount} Bars)` | `"Dramatic Build-Up (8 Bars)"` |
|
||||
| **CATEGORY** | `inputRole` | Evaluates role keywords to assign category (Piano, Orchestral, EDM, Cinematic, etc.) | `"Piano Solo"` or `"Orchestral / Film Score"` |
|
||||
| **TRIGGER KEYWORDS** | `sectionType` + `role` + `trackLabels` | Extracts key nouns, formatted as a comma-separated string | `"build-up, dramatic, piano solo, 8 bars"` |
|
||||
| **DEFAULT BARS** | `inputStartBar` & `inputEndBar` | $\max(1, (\text{EndBar} - \text{StartBar} + 1))$ | `8` |
|
||||
| **DEFAULT BPM** | Form / State Context | Inherits active project BPM or defaults to 120 | `120` |
|
||||
| **DEFAULT SCALE** | Form / State Context | Inherits active project key signature or defaults to C Minor | `"C Minor"` |
|
||||
| **SYSTEM PROMPT TEMPLATE** | `promptOutputText.innerText` | Extracts the compiled output text directly from the Live Preview component | *(Full generated prompt text)* |
|
||||
|
||||
---
|
||||
|
||||
## 3. DATA FLOW LIFECYCLE
|
||||
|
||||
```text
|
||||
[ USER GENERATES PROMPT ON GENERATOR FORM ]
|
||||
│
|
||||
▼
|
||||
[ CLICK BUTTON: "Save as Preset" ]
|
||||
│
|
||||
├─► 1. Call `computeTimeMetrics()` (Extract StartBar, EndBar, TotalBeats)
|
||||
├─► 2. Extract `generatePromptText()`
|
||||
├─► 3. Invoke `autoFillPresetModal(stateData)`
|
||||
│
|
||||
▼
|
||||
[ POPUP MODAL: "AI PROMPT PRESET MANAGER" OPENS ]
|
||||
- Preset Name <-- Auto-populated
|
||||
- Category <-- Auto-selected via dropdown
|
||||
- Trigger Keywords <-- Auto-generated tag string
|
||||
- Bars / BPM <-- Auto-populated numbers
|
||||
- System Prompt <-- Full prompt text inserted
|
||||
│
|
||||
├──► [ CLICK "Create Structured Preset" (Green Button) ]
|
||||
│ └─► Converts static text to template variable placeholders (`{{total_beats}}`)
|
||||
│
|
||||
└──► [ CLICK "Save Preset" (Purple Button) ]
|
||||
└─► Saves JSON object to `localStorage('daw_ai_prompt_presets')` & closes Modal
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. STEP-BY-STEP IMPLEMENTATION PLAN
|
||||
|
||||
### Step 1: Update Modal UI
|
||||
|
||||
Position the green **"Create Structured Preset"** button at the bottom-left of the modal action bar (adjacent to the Back, Save Preset, and Close button group).
|
||||
|
||||
### Step 2: Build `autoFillPresetModal()` Function
|
||||
|
||||
Implement a JavaScript function responsible for parsing active form control values and injecting them into the target modal input elements when triggered.
|
||||
|
||||
### Step 3: Implement "Create Structured Preset" Logic (Green Button Feature)
|
||||
|
||||
Program the handler to parse static numeric values within the prompt text (e.g., 8 bars, beat 64.0, Bars 9 to 16) and replace them with dynamic template placeholders (`{{bars}}`, `{{total_beats}}`, `{{start_bar}}`, `{{end_bar}}`). This allows saved presets to adapt dynamically to variable bar lengths when reused.
|
||||
|
||||
### Step 4: LocalStorage & Event Bus Integration
|
||||
|
||||
Save the compiled preset object to `localStorage` under the key `daw_ai_prompt_presets` and dispatch a `PRESET_CREATED` CustomEvent so that the AI Copilot Chat Bar updates its preset options instantly without requiring a page refresh.
|
||||
|
||||
---
|
||||
|
||||
## 5. CODE IMPLEMENTATION SNIPPETS
|
||||
|
||||
### 5.1 Modal HTML Markup
|
||||
|
||||
```html
|
||||
<!-- MODAL: AI PROMPT PRESET MANAGER -->
|
||||
<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 Sub-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> CREATE NEW PRESET
|
||||
</div>
|
||||
|
||||
<!-- Form Input 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">PRESET NAME</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="Enter preset name...">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">CATEGORY</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: Trigger Keywords -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">TRIGGER KEYWORDS (COMMA-SEPARATED)</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="e.g. epic orchestra, dramatic, film score">
|
||||
</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">DEFAULT BARS</label>
|
||||
<input type="number" id="presetBarsInput" 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">DEFAULT BPM</label>
|
||||
<input type="number" id="presetBpmInput" 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">DEFAULT SCALE</label>
|
||||
<input type="text" id="presetScaleInput" 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>
|
||||
|
||||
<!-- Row 4: Template Textarea -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">SYSTEM PROMPT TEMPLATE</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>
|
||||
|
||||
<!-- Bottom Action Row -->
|
||||
<div class="space-y-3 pt-2 border-t border-slate-800/80">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Green Button: Create Structured Preset -->
|
||||
<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">
|
||||
Create Structured Preset
|
||||
</button>
|
||||
|
||||
<!-- Right Button Group: Back + Save Preset -->
|
||||
<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">
|
||||
Back
|
||||
</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">
|
||||
Save Preset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Close Button -->
|
||||
<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">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Auto-Populate Logic (`autoFillPresetModal`)
|
||||
|
||||
```javascript
|
||||
// Function: Auto-populate Modal fields when "Save as Preset" is clicked
|
||||
function handleOpenSavePresetModal() {
|
||||
// 1. Calculate time metrics from the Form Engine
|
||||
const timeData = computeTimeMetrics(); // returns { barCount, totalBeats, start, end }
|
||||
const currentGeneratedPrompt = promptOutputText.innerText.trim();
|
||||
|
||||
// 2. Read values from form controls
|
||||
const sectionType = inputSectionType.value.trim() || "Building Section";
|
||||
const role = inputRole.value.trim() || "Composer";
|
||||
const actionDesc = inputActionDesc.value.trim();
|
||||
|
||||
// 3. Auto-infer Category based on Role or Section
|
||||
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";
|
||||
|
||||
// 4. Auto-generate Keyword Tags
|
||||
const keywordList = [
|
||||
sectionType.toLowerCase(),
|
||||
roleLower.split(' ')[0],
|
||||
`${timeData.barCount} bars`
|
||||
];
|
||||
const keywordsStr = keywordList.filter(Boolean).join(', ');
|
||||
|
||||
// 5. Populate Form Fields
|
||||
presetNameInput.value = `${sectionType} (${timeData.barCount} Bars)`;
|
||||
presetCategorySelect.value = category;
|
||||
presetKeywordsInput.value = keywordsStr;
|
||||
presetBarsInput.value = timeData.barCount;
|
||||
presetBpmInput.value = 120; // Default BPM or read from session context
|
||||
presetScaleInput.value = "C Minor";
|
||||
presetTemplateTextarea.value = currentGeneratedPrompt;
|
||||
|
||||
// 6. Display Modal
|
||||
presetManagerModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Event Listener for the main button
|
||||
btnSavePresetModal.addEventListener('click', handleOpenSavePresetModal);
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Structured Preset Variable Replacement ("Create Structured Preset")
|
||||
|
||||
```javascript
|
||||
// Function: Converts static numbers in prompt into dynamic placeholders
|
||||
btnCreateStructuredPreset.addEventListener('click', () => {
|
||||
let promptText = presetTemplateTextarea.value;
|
||||
const bars = presetBarsInput.value || "8";
|
||||
|
||||
// Replace specific numbers with template variables
|
||||
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("Converted Prompt to Structured Variable Template!");
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Save Preset Handler (`localStorage` & Event Dispatch)
|
||||
|
||||
```javascript
|
||||
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("Please enter a Preset Name!", "error");
|
||||
presetNameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
showToast("System Prompt content cannot be empty!", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const newPresetObj = {
|
||||
id: `preset_${Date.now()}`,
|
||||
name: name,
|
||||
category: category,
|
||||
keywords: keywordsRaw ? keywordsRaw.split(',').map(k => k.trim()) : [name.toLowerCase()],
|
||||
default_bars: defaultBars,
|
||||
default_bpm: defaultBpm,
|
||||
default_scale: defaultScale,
|
||||
system_instruction_template: template,
|
||||
is_user_defined: true,
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Read existing presets and prepend new one
|
||||
let savedPresets = [];
|
||||
try {
|
||||
const stored = localStorage.getItem('daw_ai_prompt_presets');
|
||||
savedPresets = stored ? JSON.parse(stored) : [];
|
||||
} catch (e) {
|
||||
savedPresets = [];
|
||||
}
|
||||
|
||||
savedPresets.unshift(newPresetObj);
|
||||
localStorage.setItem('daw_ai_prompt_presets', JSON.stringify(savedPresets));
|
||||
|
||||
// Dispatch Custom Event so Copilot Chat UI updates dynamically
|
||||
window.dispatchEvent(new CustomEvent('AI_PRESET_SAVED', { detail: newPresetObj }));
|
||||
|
||||
showToast(`Preset "${name}" saved successfully!`);
|
||||
presetManagerModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. OPERATIONAL VERIFICATION CHECKLIST
|
||||
|
||||
* [ ] Open Form Generator and set parameters (e.g., Bars 9 to 16, Role: Pianist).
|
||||
* [ ] Click "Save as Preset":
|
||||
* Modal opens.
|
||||
* **PRESET NAME** displays: `Dramatic Build-Up (8 Bars)`.
|
||||
* **CATEGORY** selects: `Piano Solo`.
|
||||
* **DEFAULT BARS** populates: `8`.
|
||||
* **SYSTEM PROMPT TEMPLATE** contains the generated prompt text.
|
||||
|
||||
|
||||
* [ ] Test the "Create Structured Preset" green button:
|
||||
* Click button $\rightarrow$ values such as 8 bars or beat 64.0 are converted to variables like `{{bars}}` and `{{total_beats}}`.
|
||||
|
||||
|
||||
* [ ] Click "Save Preset" (Purple Button):
|
||||
* Toast notification confirms save.
|
||||
* `localStorage.getItem('daw_ai_prompt_presets')` contains the new preset object at the beginning of the array.
|
||||
Reference in New Issue
Block a user