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:
+83
-5
@@ -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", {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user