From cff39506709b2e4474be6a2bf96108425ae0257f Mon Sep 17 00:00:00 2001 From: 3dtours Date: Tue, 28 Jul 2026 11:20:26 +0700 Subject: [PATCH] feat: AI Copilot suggestion chips + typeahead dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- app/api/v1/ai_presets.py | 52 +++++++++++ app/static/js/app.jsx | 88 +++++++++++++++++-- .../js/services/promptTemplateManager.js | 8 ++ tests/test_ai_presets.py | 6 +- wiki.md | 6 ++ 5 files changed, 153 insertions(+), 7 deletions(-) diff --git a/app/api/v1/ai_presets.py b/app/api/v1/ai_presets.py index a1bd812..2617c51 100644 --- a/app/api/v1/ai_presets.py +++ b/app/api/v1/ai_presets.py @@ -48,6 +48,58 @@ DEFAULT_PRESETS = [ "is_user_defined": False, "is_favorite": False, "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" } ] diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 1dcceca..6851368 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -7202,6 +7202,8 @@ const App = () => { } }, [aiActionLog]); const [aiProcessing, setAiProcessing] = useState(false); + const [aiSuggestions, setAiSuggestions] = useState([]); + const [showAiTypeahead, setShowAiTypeahead] = useState(false); const [selectedProviderId, setSelectedProviderId] = useState(''); const [exportSettings, setExportSettings] = useState({ 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 track = activeTracks.find(t => t.id === trackId); if (!track) return null; @@ -15931,7 +15943,21 @@ const App = () => { "data-lucide": "download-cloud", className: "w-3 h-3" })), 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" }, /*#__PURE__*/React.createElement("div", { className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none", @@ -16034,6 +16060,26 @@ const App = () => { 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'}` }, 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" }, /*#__PURE__*/React.createElement("div", { 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" })), " Copilot Prompt"), /*#__PURE__*/React.createElement("textarea", { value: aiPrompt, - onChange: e => setAiPrompt(e.target.value), - placeholder: "Nhập lệnh điều khiển DAW... (VD: Tạo track mới tên Beat, đặt BPM 128)", + onChange: e => { + 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", rows: 4, onKeyDown: e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); 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) { e.preventDefault(); 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" }, /*#__PURE__*/React.createElement("button", { 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" }, "Clear")), /*#__PURE__*/React.createElement("div", { 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", { className: "flex flex-col h-full gap-1.5" }, /*#__PURE__*/React.createElement("div", { diff --git a/app/static/js/services/promptTemplateManager.js b/app/static/js/services/promptTemplateManager.js index 6bd1e73..4461c99 100644 --- a/app/static/js/services/promptTemplateManager.js +++ b/app/static/js/services/promptTemplateManager.js @@ -236,6 +236,14 @@ const PromptTemplateManager = (function() { 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) { const preset = this.presets.find(p => p.id === id); if (preset) { diff --git a/tests/test_ai_presets.py b/tests/test_ai_presets.py index 51ce653..eed64db 100644 --- a/tests/test_ai_presets.py +++ b/tests/test_ai_presets.py @@ -22,11 +22,13 @@ def test_list_presets_defaults(): assert res.status_code == 200 data = res.json() assert data["success"] is True - assert len(data["presets"]) == 3 + assert len(data["presets"]) == 7 names = [p["name"] for p in data["presets"]] assert "Epic Orchestra Intro (8 Bars)" in names assert "Pop Piano Chords (4 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(): @@ -54,7 +56,7 @@ def test_create_user_preset(): assert res2.status_code == 200 names = [p["name"] for p in res2.json()["presets"]] 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(): diff --git a/wiki.md b/wiki.md index 29fe446..09749de 100644 --- a/wiki.md +++ b/wiki.md @@ -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. --- +### [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) - **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`