feat: AI Copilot suggestion chips + typeahead dropdown

- Suggestion chips bar: context-aware (amber for rearrange, indigo for creation)
- Typeahead dropdown: keyword matching on ≥2 chars, Tab to select
- getSelectedMidiItemInfo helper for context detection
- Backend: 4 rearrange presets in ai_presets.py
- PromptTemplateManager: getContextualSuggestions(hasSelectedItem)
This commit is contained in:
2026-07-28 11:20:26 +07:00
parent 3f7abe0c95
commit cff3950670
5 changed files with 153 additions and 7 deletions
+52
View File
@@ -48,6 +48,58 @@ DEFAULT_PRESETS = [
"is_user_defined": False, "is_user_defined": False,
"is_favorite": False, "is_favorite": False,
"created_at": "2026-07-23T16:00:00Z" "created_at": "2026-07-23T16:00:00Z"
},
{
"id": "preset_rearrange_jazz_swing",
"name": "Jazz Swing Variation",
"keywords": ["jazz", "swing", "rearrange to jazz", "jazz improv"],
"category": "Rearrange / Variation",
"default_bars": 4,
"default_bpm": 120,
"default_scale": "C Major",
"system_instruction_template": "Rearrange the original melody into a rhythmic Jazz Swing style. Apply syncopation, 7th/9th chord extensions, and off-beat rhythmic feel.",
"is_user_defined": False,
"is_favorite": True,
"created_at": "2026-07-28T11:00:00Z"
},
{
"id": "preset_rearrange_synth_arpeggio",
"name": "80s Synthwave Arpeggiator",
"keywords": ["synthwave", "arpeggio", "arp", "16th variation", "80s synth"],
"category": "Rearrange / Variation",
"default_bars": 4,
"default_bpm": 120,
"default_scale": "C Minor",
"system_instruction_template": "Transform this sequence into a driving, shimmering 16th-note Arpeggio string in classic 80s Synthwave/Trance style.",
"is_user_defined": False,
"is_favorite": False,
"created_at": "2026-07-28T11:00:00Z"
},
{
"id": "preset_rearrange_harmonize",
"name": "Chords Harmonization (4-Note)",
"keywords": ["harmonize", "harmony", "add chord notes", "rich melody"],
"category": "Rearrange / Variation",
"default_bars": 4,
"default_bpm": 120,
"default_scale": "C Major",
"system_instruction_template": "Add harmonizing notes to create lush 4-note chord voicing extensions beneath the lead melody.",
"is_user_defined": False,
"is_favorite": False,
"created_at": "2026-07-28T11:00:00Z"
},
{
"id": "preset_rearrange_cinematic_strings",
"name": "Cinematic Strings Staccato",
"keywords": ["cinematic", "strings", "staccato", "staccato strings"],
"category": "Rearrange / Variation",
"default_bars": 4,
"default_bpm": 120,
"default_scale": "C Minor",
"system_instruction_template": "Rearrange the original line into a dramatic, driving Staccato rhythm for a Strings Ensemble.",
"is_user_defined": False,
"is_favorite": True,
"created_at": "2026-07-28T11:00:00Z"
} }
] ]
+83 -5
View File
@@ -7202,6 +7202,8 @@ const App = () => {
} }
}, [aiActionLog]); }, [aiActionLog]);
const [aiProcessing, setAiProcessing] = useState(false); const [aiProcessing, setAiProcessing] = useState(false);
const [aiSuggestions, setAiSuggestions] = useState([]);
const [showAiTypeahead, setShowAiTypeahead] = useState(false);
const [selectedProviderId, setSelectedProviderId] = useState(''); const [selectedProviderId, setSelectedProviderId] = useState('');
const [exportSettings, setExportSettings] = useState({ const [exportSettings, setExportSettings] = useState({
sampleRate: '44100', sampleRate: '44100',
@@ -7269,6 +7271,16 @@ const App = () => {
}; };
})); }));
}; };
const getSelectedMidiItemInfo = () => {
if (!selectedItemIds || selectedItemIds.size !== 1) return null;
const selId = selectedItemIds.values().next().value;
const tlist = activeTracks || tracks || [];
for (const t of tlist) {
const found = (t.midiItems || []).find(m => m.id === selId);
if (found) return { itemName: found.name, trackName: t.name, trackId: t.id, itemId: selId };
}
return null;
};
const captureTrackSnapshot = trackId => { const captureTrackSnapshot = trackId => {
const track = activeTracks.find(t => t.id === trackId); const track = activeTracks.find(t => t.id === trackId);
if (!track) return null; if (!track) return null;
@@ -15931,7 +15943,21 @@ const App = () => {
"data-lucide": "download-cloud", "data-lucide": "download-cloud",
className: "w-3 h-3" className: "w-3 h-3"
})), isExporting ? '...' : 'Export')); })), isExporting ? '...' : 'Export'));
if (panelId === 'ai') return /*#__PURE__*/React.createElement("div", { if (panelId === 'ai') {
const selMidiInfo = getSelectedMidiItemInfo();
const hasSelItem = !!selMidiInfo;
const promptMgr = window.PromptTemplateManager ? new window.PromptTemplateManager() : null;
const suggestions = promptMgr ? promptMgr.getContextualSuggestions(hasSelItem) : [];
const handleApplySuggestion = (preset) => {
if (hasSelItem) {
setAiPrompt(`Rearrange this melody line in ${preset.name} style`);
} else {
setAiPrompt(preset.system_instruction_template);
}
setShowAiTypeahead(false);
setAiSuggestions([]);
};
return /*#__PURE__*/React.createElement("div", {
className: "flex flex-col gap-1.5 flex-1 min-h-0" className: "flex flex-col gap-1.5 flex-1 min-h-0"
}, /*#__PURE__*/React.createElement("div", { }, /*#__PURE__*/React.createElement("div", {
className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none", className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
@@ -16034,6 +16060,26 @@ const App = () => {
key: i, key: i,
className: `text-xs font-mono py-0.5 border-b border-zinc-900 last:border-0 ${entry.type === 'error' ? 'text-red-400' : entry.type === 'status' ? 'text-zinc-400 italic' : entry.type === 'undo' ? 'text-amber-400' : 'text-zinc-300'}` className: `text-xs font-mono py-0.5 border-b border-zinc-900 last:border-0 ${entry.type === 'error' ? 'text-red-400' : entry.type === 'status' ? 'text-zinc-400 italic' : entry.type === 'undo' ? 'text-amber-400' : 'text-zinc-300'}`
}, new Date(entry.time).toLocaleTimeString(), entry.text)))), /*#__PURE__*/React.createElement("div", { }, new Date(entry.time).toLocaleTimeString(), entry.text)))), /*#__PURE__*/React.createElement("div", {
className: `flex items-center gap-1 px-1 py-0.5 rounded mb-1 shrink-0 ${hasSelItem ? 'bg-amber-950/30 border border-amber-800/40' : ''}`
}, hasSelItem ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
className: "flex items-center gap-1 text-[10px] text-amber-400 font-semibold shrink-0"
}, /*#__PURE__*/React.createElement("span", {
className: "w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse"
}), "MIDI: ", /*#__PURE__*/React.createElement("strong", {
className: "truncate max-w-[80px]"
}, selMidiInfo.itemName || selMidiInfo.itemId)), /*#__PURE__*/React.createElement("div", {
className: "flex flex-wrap gap-1 overflow-x-auto"
}, suggestions.slice(0, 6).map(p => /*#__PURE__*/React.createElement("button", {
key: p.id,
onClick: () => handleApplySuggestion(p),
className: "px-1.5 py-0.5 rounded-full bg-amber-950/40 border border-amber-800/50 text-[10px] text-amber-200 hover:bg-amber-800/50 whitespace-nowrap"
}, p.is_favorite ? "★" : "✨", " ", p.name)))) : /*#__PURE__*/React.createElement("div", {
className: "flex flex-wrap gap-1"
}, suggestions.slice(0, 4).map(p => /*#__PURE__*/React.createElement("button", {
key: p.id,
onClick: () => handleApplySuggestion(p),
className: "px-1.5 py-0.5 rounded-full bg-indigo-950/40 border border-indigo-800/50 text-[10px] text-indigo-200 hover:bg-indigo-800/50 whitespace-nowrap"
}, p.is_favorite ? "★" : "✨", " ", p.name)))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1 shrink-0" className: "border-t border-zinc-800 pt-1.5 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("div", { }, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1" className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1"
@@ -16044,14 +16090,30 @@ const App = () => {
className: "w-3 h-3" className: "w-3 h-3"
})), " Copilot Prompt"), /*#__PURE__*/React.createElement("textarea", { })), " Copilot Prompt"), /*#__PURE__*/React.createElement("textarea", {
value: aiPrompt, value: aiPrompt,
onChange: e => setAiPrompt(e.target.value), onChange: e => {
placeholder: "Nhập lệnh điều khiển DAW... (VD: Tạo track mới tên Beat, đặt BPM 128)", const v = e.target.value;
setAiPrompt(v);
if (v.trim().length >= 2 && promptMgr) {
const matches = promptMgr.presets.filter(p =>
p.keywords.some(kw => kw.toLowerCase().includes(v.toLowerCase())) ||
p.name.toLowerCase().includes(v.toLowerCase())
);
setAiSuggestions(matches);
setShowAiTypeahead(matches.length > 0);
} else {
setShowAiTypeahead(false);
}
},
placeholder: hasSelItem ? "Nhập lệnh rearrange... (VD: Jazz Swing, Arpeggio)" : "Nhập lệnh điều khiển DAW... (VD: Tạo track mới tên Beat, đặt BPM 128)",
className: "w-full bg-[#1e1e1e] text-zinc-200 p-1.5 rounded border border-zinc-700 focus:outline-none focus:border-purple-600 font-mono text-xs resize-y", className: "w-full bg-[#1e1e1e] text-zinc-200 p-1.5 rounded border border-zinc-700 focus:outline-none focus:border-purple-600 font-mono text-xs resize-y",
rows: 4, rows: 4,
onKeyDown: e => { onKeyDown: e => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
handleAISend(); handleAISend();
} else if (e.key === 'Tab' && showAiTypeahead && aiSuggestions.length > 0) {
e.preventDefault();
handleApplySuggestion(aiSuggestions[0]);
} else if (e.key === 'ArrowUp' && promptHistRef.current.length > 0 && e.target.selectionStart === 0) { } else if (e.key === 'ArrowUp' && promptHistRef.current.length > 0 && e.target.selectionStart === 0) {
e.preventDefault(); e.preventDefault();
const idx = promptHistIdx === -1 ? promptHistRef.current.length - 1 : Math.max(0, promptHistIdx - 1); const idx = promptHistIdx === -1 ? promptHistRef.current.length - 1 : Math.max(0, promptHistIdx - 1);
@@ -16070,7 +16132,22 @@ const App = () => {
} }
} }
} }
})), /*#__PURE__*/React.createElement("div", { }), showAiTypeahead && aiSuggestions.length > 0 && /*#__PURE__*/React.createElement("div", {
ref: aiTypeaheadRef,
className: "absolute bottom-full left-0 right-0 bg-[#1e1e1e] border border-indigo-600/50 rounded-lg shadow-2xl z-50 max-h-36 overflow-y-auto mb-1"
}, /*#__PURE__*/React.createElement("div", {
className: "px-2 py-1 text-[10px] uppercase tracking-wider font-semibold text-indigo-400 bg-[#141414] border-b border-zinc-800"
}, "Gợi ý (", aiSuggestions.length, ")"), aiSuggestions.slice(0, 8).map(p => /*#__PURE__*/React.createElement("div", {
key: p.id,
onClick: () => handleApplySuggestion(p),
className: "px-2 py-1 hover:bg-indigo-700/30 cursor-pointer border-b border-zinc-800/30 flex items-center justify-between text-[11px]"
}, /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement("span", {
className: "font-semibold text-zinc-200"
}, p.name), /*#__PURE__*/React.createElement("span", {
className: "ml-1.5 text-zinc-500"
}, "(", p.category, ")")), /*#__PURE__*/React.createElement("span", {
className: "text-[10px] bg-zinc-800 text-zinc-400 px-1 py-0.5 rounded"
}, "Tab"))))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 mt-1 shrink-0" className: "flex items-center gap-1 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("button", { }, /*#__PURE__*/React.createElement("button", {
onClick: handleAISend, onClick: handleAISend,
@@ -16089,7 +16166,8 @@ const App = () => {
className: "px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded text-xs border border-zinc-700" className: "px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded text-xs border border-zinc-700"
}, "Clear")), /*#__PURE__*/React.createElement("div", { }, "Clear")), /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 shrink-0" className: "text-xs text-zinc-600 shrink-0"
}, "Enter để gửi nhanh")); }, hasSelItem ? "Enter gửi rearrange | Tab chọn gợi ý" : "Enter để gửi nhanh"));
}
if (panelId === 'python_tools') return /*#__PURE__*/React.createElement("div", { if (panelId === 'python_tools') return /*#__PURE__*/React.createElement("div", {
className: "flex flex-col h-full gap-1.5" className: "flex flex-col h-full gap-1.5"
}, /*#__PURE__*/React.createElement("div", { }, /*#__PURE__*/React.createElement("div", {
@@ -236,6 +236,14 @@ const PromptTemplateManager = (function() {
this.savePresets(); this.savePresets();
}; };
PromptTemplateManager.prototype.getContextualSuggestions = function(hasSelectedItem) {
if (hasSelectedItem) {
return this.presets.filter(p => p.category === 'Rearrange / Variation' || p.is_favorite);
} else {
return this.presets.filter(p => p.category !== 'Rearrange / Variation');
}
};
PromptTemplateManager.prototype.toggleFavorite = function(id) { PromptTemplateManager.prototype.toggleFavorite = function(id) {
const preset = this.presets.find(p => p.id === id); const preset = this.presets.find(p => p.id === id);
if (preset) { if (preset) {
+4 -2
View File
@@ -22,11 +22,13 @@ def test_list_presets_defaults():
assert res.status_code == 200 assert res.status_code == 200
data = res.json() data = res.json()
assert data["success"] is True assert data["success"] is True
assert len(data["presets"]) == 3 assert len(data["presets"]) == 7
names = [p["name"] for p in data["presets"]] names = [p["name"] for p in data["presets"]]
assert "Epic Orchestra Intro (8 Bars)" in names assert "Epic Orchestra Intro (8 Bars)" in names
assert "Pop Piano Chords (4 Bars)" in names assert "Pop Piano Chords (4 Bars)" in names
assert "Cyberpunk Synthwave (8 Bars)" in names assert "Cyberpunk Synthwave (8 Bars)" in names
assert "Jazz Swing Variation" in names
assert "80s Synthwave Arpeggiator" in names
def test_create_user_preset(): def test_create_user_preset():
@@ -54,7 +56,7 @@ def test_create_user_preset():
assert res2.status_code == 200 assert res2.status_code == 200
names = [p["name"] for p in res2.json()["presets"]] names = [p["name"] for p in res2.json()["presets"]]
assert "My Beat (4 Bars)" in names assert "My Beat (4 Bars)" in names
assert len(res2.json()["presets"]) == 4 # 3 defaults + 1 user assert len(res2.json()["presets"]) == 8 # 7 defaults + 1 user
def test_update_user_preset(): def test_update_user_preset():
+6
View File
@@ -539,6 +539,12 @@
- **Ghi chú/Test (nếu có):** `npm run build` pass. Ctrl+Click+Drag section/MIDI item → copy đến vị trí mới, item không bị selected. - **Ghi chú/Test (nếu có):** `npm run build` pass. Ctrl+Click+Drag section/MIDI item → copy đến vị trí mới, item không bị selected.
--- ---
### [2026-07-28 11:17] Task: AI Copilot Suggestion Chips + Typeahead (spec 44_AI_SUGGEST_PROMT.md)
- **Tóm tắt thay đổi:** Thêm suggestion chips bar trên Copilot Prompt: context-aware (MIDI selected → amber rearrange chips, no selection → indigo creation chips). Typeahead dropdown với keyword matching khi gõ ≥2 ký tự, Tab để chọn. `getSelectedMidiItemInfo` helper. Backend ai_presets.py thêm 4 rearrange presets. `promptTemplateManager.js` thêm `getContextualSuggestions(hasSelectedItem)`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/promptTemplateManager.js`, `app/api/v1/ai_presets.py`, `tests/test_ai_presets.py`
- **Ghi chú/Test (nếu có):** `npx babel` compile pass. `pytest tests/test_ai_presets.py -v` — 7/7 pass.
---
### [2026-07-28 11:02] Task: AI Rearrange Scenario Detection (spec 45_SCENARIAO_AI.md) ### [2026-07-28 11:02] Task: AI Rearrange Scenario Detection (spec 45_SCENARIAO_AI.md)
- **Tóm tắt thay đổi:** Thêm 9 rearrange scenarios vào `aiGateway.js` (arpeggio, harmonies, syncopation, walking bass, jazz swing, synthwave, cinematic, simplify, passing tones) với keyword mapping → `detectRearrangeScenario()`. `buildRearrangeMessage` tự động inject specific technique rules dựa trên scenario detect được. Thêm 9 rearrange presets vào `promptTemplateManager.js` với category "Rearrange / Variation". - **Tóm tắt thay đổi:** Thêm 9 rearrange scenarios vào `aiGateway.js` (arpeggio, harmonies, syncopation, walking bass, jazz swing, synthwave, cinematic, simplify, passing tones) với keyword mapping → `detectRearrangeScenario()`. `buildRearrangeMessage` tự động inject specific technique rules dựa trên scenario detect được. Thêm 9 rearrange presets vào `promptTemplateManager.js` với category "Rearrange / Variation".
- **Các file ảnh hưởng:** `app/static/js/services/aiGateway.js`, `app/static/js/services/promptTemplateManager.js` - **Các file ảnh hưởng:** `app/static/js/services/aiGateway.js`, `app/static/js/services/promptTemplateManager.js`