fix: đã sửa lỗi AI gửi prompt

This commit is contained in:
2026-07-22 08:52:34 +07:00
parent 09bb431a2b
commit e0b849fdf2
6 changed files with 375 additions and 97 deletions
+129 -45
View File
@@ -2481,7 +2481,8 @@ const AuthModal = ({
};
const AIConfigModal = ({
isOpen,
onClose
onClose,
onConfigSaved
}) => {
if (!isOpen) return null;
const defaultProvidersList = [{
@@ -2552,6 +2553,7 @@ const AIConfigModal = ({
try {
const res = await window.SonicAPI.saveAIConfigs(providers);
setMsg(res.message || 'Đã lưu cấu hình AI Providers thành công!');
if (onConfigSaved) onConfigSaved(providers);
} catch (err) {
setError(err.message || 'Lỗi khi lưu cấu hình AI');
} finally {
@@ -2594,7 +2596,38 @@ const AIConfigModal = ({
className: "truncate"
}, p.name), p.is_active && /*#__PURE__*/React.createElement("span", {
className: "w-2 h-2 rounded-full bg-emerald-400"
})))), activeProvider && /*#__PURE__*/React.createElement("form", {
}))), /*#__PURE__*/React.createElement("div", {
className: "flex gap-1 mt-2"
}, /*#__PURE__*/React.createElement("button", {
onClick: e => {
e.stopPropagation();
const newId = 'provider_' + Date.now();
setProviders(prev => [...prev, {
id: newId,
name: 'New Provider',
provider_type: 'openai_compatible',
api_base_url: 'https://api.openai.com/v1',
api_key: '',
model_name: 'gpt-4o-mini',
temperature: 0.7,
is_active: false
}]);
setSelectedId(newId);
},
className: "flex-1 px-2 py-1 bg-emerald-700 hover:bg-emerald-600 text-white text-xs font-bold rounded"
}, "+ Thêm"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
onClick: e => {
e.stopPropagation();
if (confirm(`Xóa provider "${providers.find(p => p.id === selectedId)?.name}"?`)) {
setProviders(prev => {
const filtered = prev.filter(p => p.id !== selectedId);
if (filtered.length > 0) setSelectedId(filtered[0].id);
return filtered;
});
}
},
className: "px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"
}, "Xóa"))), activeProvider && /*#__PURE__*/React.createElement("form", {
onSubmit: handleSave,
className: "col-span-2 space-y-3.5"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
@@ -3004,6 +3037,19 @@ const App = () => {
apiKey: localStorage.getItem('ai_api_key') || '',
model: localStorage.getItem('ai_model') || 'deepseek-chat'
});
const [aiProviders, setAiProviders] = useState([]);
useEffect(() => {
(async () => {
try {
const data = await window.SonicAPI.getAIConfigs();
if (data && data.providers) {
setAiProviders(data.providers);
const active = data.providers.find(p => p.is_active) || data.providers[0];
if (active) setSelectedProviderId(active.id);
}
} catch (e) { /* server may not have config endpoint */ }
})();
}, []);
const [analysisState, setAnalysisState] = useState({
status: 'Sẵn sàng. Chạy AI để phân tích nhịp.',
data: null,
@@ -3014,6 +3060,7 @@ const App = () => {
const [aiModel, setAiModel] = useState('GPT-4o');
const [aiActionLog, setAiActionLog] = useState([]);
const [aiProcessing, setAiProcessing] = useState(false);
const [selectedProviderId, setSelectedProviderId] = useState('');
const [exportSettings, setExportSettings] = useState({
sampleRate: '44100',
bitDepth: '16',
@@ -3348,7 +3395,7 @@ const App = () => {
const sidebarEl = document.getElementById('right-sidebar');
const sidebarHeight = sidebarEl ? sidebarEl.getBoundingClientRect().height : 400;
const onMove = ev => {
const deltaY = ev.clientY - startY;
const deltaY = startY - ev.clientY;
const pct = ((startHeight / 100 * sidebarHeight + deltaY) / sidebarHeight) * 100;
const newPct = Math.max(20, Math.min(80, pct));
setMediaExplorerHeight(newPct);
@@ -6922,6 +6969,58 @@ const App = () => {
}, 800);
};
// AI Prompt Send to Active Provider
const handleAISend = async () => {
const prompt = aiPrompt.trim();
if (!prompt) { showToast('Vui lòng nhập nội dung prompt.', 'warning'); return; }
setAiProcessing(true);
setAiActionLog(prev => [...prev, { type: 'status', text: ` ⏳ Đang gửi prompt đến AI...`, time: Date.now() }]);
try {
const selectedProvider = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
const provider = selectedProvider || aiConfig;
const baseUrl = provider.api_base_url || provider.baseUrl || `${API_BASE_URL}`;
const apiKey = provider.api_key || provider.apiKey || '';
const model = provider.model_name || provider.model || 'deepseek-chat';
const dawContext = window.AIGateway.buildAIPromptContext({
tracks, bpm, selectedTrackId, currentTime, selLeft, selRight
});
const result = await window.AIGateway.executeAIPrompt({
prompt,
provider: provider.name || 'default',
model,
apiKey,
baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
dawContext,
tools: window.AIGateway.DEFAULT_TOOLS
});
if (result.textResponse) {
setAiActionLog(prev => [...prev, { type: 'status', text: ` AI: ${result.textResponse}`, time: Date.now() }]);
}
if (result.functionCalls && result.functionCalls.length > 0) {
for (const fc of result.functionCalls) {
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
const cmdName = fc.name.toUpperCase();
if (window.DAWCommandDispatcher) {
try {
const cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
setAiActionLog(prev => [...prev, { type: 'status', text: `${fc.name}: ${cmdResult.success ? 'thành công' : 'thất bại'}`, time: Date.now() }]);
} catch (cmdErr) {
setAiActionLog(prev => [...prev, { type: 'error', text: `${fc.name}: ${cmdErr.message}`, time: Date.now() }]);
}
}
}
}
setAiActionLog(prev => [...prev, { type: 'status', text: ` Hoàn tất.`, time: Date.now() }]);
setAiPrompt('');
setTimeout(() => lucide.createIcons(), 200);
} catch (err) {
setAiActionLog(prev => [...prev, { type: 'error', text: ` Lỗi: ${err.message}`, time: Date.now() }]);
showToast(`AI Error: ${err.message}`, 'error');
} finally {
setAiProcessing(false);
}
};
// Split Track at Playhead
const handleSplitTrackAtTime = (trackId, clipId, time) => {
const track = tracks.find(t => t.id === trackId);
@@ -8087,42 +8186,22 @@ const App = () => {
"data-lucide": "x",
className: "w-3 h-3"
}))))), /*#__PURE__*/React.createElement("div", {
className: "p-1.5 bg-[#141414] rounded border border-zinc-800 text-xs font-mono min-h-[20px]"
}, /*#__PURE__*/React.createElement("div", {
className: "text-zinc-500"
}, "// ", /*#__PURE__*/React.createElement("span", {
className: "text-zinc-300"
}, analysisState.status)), analysisState.data && /*#__PURE__*/React.createElement("div", {
className: "text-emerald-500 font-semibold"
}, "BPM: ", analysisState.data.bpm)), /*#__PURE__*/React.createElement("div", {
className: "grid grid-cols-3 gap-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: handleAIScan,
className: "py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-xs border border-purple-700 flex items-center justify-center gap-1"
className: "flex items-center gap-1.5 shrink-0"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "map-pin",
className: "w-3 h-3"
})), " Scan"), /*#__PURE__*/React.createElement("button", {
onClick: handleAICutToNewTrack,
disabled: analysisState.isRunning,
className: "py-1 bg-fuchsia-800 hover:bg-fuchsia-700 text-white font-bold rounded text-xs border border-fuchsia-600 flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "scissors",
className: "w-3 h-3"
})), " Cut"), /*#__PURE__*/React.createElement("button", {
onClick: handleAIAnalysicLoop,
disabled: analysisState.isRunning,
className: "py-1 bg-violet-800 hover:bg-violet-700 text-violet-100 font-bold rounded text-xs border border-violet-600 flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "sparkles",
className: "w-3 h-3"
})), " Loop")), /*#__PURE__*/React.createElement("div", {
"data-lucide": "cpu",
className: "w-3 h-3 text-purple-400"
})), /*#__PURE__*/React.createElement("select", {
value: selectedProviderId,
onChange: e => setSelectedProviderId(e.target.value),
className: "flex-1 bg-[#141414] text-zinc-200 border border-zinc-700 rounded px-1 py-0.5 text-xs font-mono focus:outline-none focus:border-purple-600"
}, aiProviders.length === 0 ? /*#__PURE__*/React.createElement("option", {
value: ""
}, "Chưa có provider") : aiProviders.map(p => /*#__PURE__*/React.createElement("option", {
key: p.id,
value: p.id
}, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1"
}, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1"
@@ -8138,15 +8217,15 @@ const App = () => {
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-none",
rows: 2,
onKeyDown: e => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
window.executeAIPrompt();
handleAISend();
}
}
}), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 mt-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => window.executeAIPrompt(),
onClick: handleAISend,
disabled: aiProcessing,
className: "flex-1 py-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded text-xs border border-purple-500 flex items-center justify-center gap-1"
}, aiProcessing ? 'Đang suy luận...' : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
@@ -8162,10 +8241,10 @@ 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 mt-0.5"
}, "Ctrl+Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1"
}, "Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1 flex-1 min-h-0 flex flex-col"
}, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center justify-between"
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center justify-between shrink-0"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center gap-1"
}, /*#__PURE__*/React.createElement("i", {
@@ -8186,9 +8265,9 @@ const App = () => {
},
className: "text-xs text-amber-400 hover:text-amber-300 border border-amber-800 rounded px-1 py-0.5"
}, "Undo"))), /*#__PURE__*/React.createElement("div", {
className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 p-1 max-h-[120px]"
className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 p-1 select-text"
}, aiActionLog.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 italic"
className: "text-xs text-zinc-600 italic select-text"
}, "Chưa có hành động nào.") : aiActionLog.map((entry, i) => /*#__PURE__*/React.createElement("div", {
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'}`
@@ -9637,7 +9716,12 @@ const App = () => {
onClose: () => setProfileModalOpen(false)
}), /*#__PURE__*/React.createElement(AIConfigModal, {
isOpen: aiConfigModalOpen,
onClose: () => setAiConfigModalOpen(false)
onClose: () => setAiConfigModalOpen(false),
onConfigSaved: (providers) => {
setAiProviders(providers);
const active = providers.find(p => p.is_active) || providers[0];
if (active) setSelectedProviderId(active.id);
}
}), /*#__PURE__*/React.createElement(SystemManagerModal, {
isOpen: systemManagerModalOpen,
onClose: () => setSystemManagerModalOpen(false)
+165 -45
View File
@@ -2481,7 +2481,8 @@ const AuthModal = ({
};
const AIConfigModal = ({
isOpen,
onClose
onClose,
onConfigSaved
}) => {
if (!isOpen) return null;
const defaultProvidersList = [{
@@ -2552,6 +2553,7 @@ const AIConfigModal = ({
try {
const res = await window.SonicAPI.saveAIConfigs(providers);
setMsg(res.message || 'Đã lưu cấu hình AI Providers thành công!');
if (onConfigSaved) onConfigSaved(providers);
} catch (err) {
setError(err.message || 'Lỗi khi lưu cấu hình AI');
} finally {
@@ -2594,7 +2596,38 @@ const AIConfigModal = ({
className: "truncate"
}, p.name), p.is_active && /*#__PURE__*/React.createElement("span", {
className: "w-2 h-2 rounded-full bg-emerald-400"
})))), activeProvider && /*#__PURE__*/React.createElement("form", {
}))), /*#__PURE__*/React.createElement("div", {
className: "flex gap-1 mt-2"
}, /*#__PURE__*/React.createElement("button", {
onClick: e => {
e.stopPropagation();
const newId = 'provider_' + Date.now();
setProviders(prev => [...prev, {
id: newId,
name: 'New Provider',
provider_type: 'openai_compatible',
api_base_url: 'https://api.openai.com/v1',
api_key: '',
model_name: 'gpt-4o-mini',
temperature: 0.7,
is_active: false
}]);
setSelectedId(newId);
},
className: "flex-1 px-2 py-1 bg-emerald-700 hover:bg-emerald-600 text-white text-xs font-bold rounded"
}, "+ Thêm"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
onClick: e => {
e.stopPropagation();
if (confirm(`Xóa provider "${providers.find(p => p.id === selectedId)?.name}"?`)) {
setProviders(prev => {
const filtered = prev.filter(p => p.id !== selectedId);
if (filtered.length > 0) setSelectedId(filtered[0].id);
return filtered;
});
}
},
className: "px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"
}, "Xóa"))), activeProvider && /*#__PURE__*/React.createElement("form", {
onSubmit: handleSave,
className: "col-span-2 space-y-3.5"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
@@ -3004,6 +3037,19 @@ const App = () => {
apiKey: localStorage.getItem('ai_api_key') || '',
model: localStorage.getItem('ai_model') || 'deepseek-chat'
});
const [aiProviders, setAiProviders] = useState([]);
useEffect(() => {
(async () => {
try {
const data = await window.SonicAPI.getAIConfigs();
if (data && data.providers) {
setAiProviders(data.providers);
const active = data.providers.find(p => p.is_active) || data.providers[0];
if (active) setSelectedProviderId(active.id);
}
} catch (e) {/* server may not have config endpoint */}
})();
}, []);
const [analysisState, setAnalysisState] = useState({
status: 'Sẵn sàng. Chạy AI để phân tích nhịp.',
data: null,
@@ -3014,6 +3060,7 @@ const App = () => {
const [aiModel, setAiModel] = useState('GPT-4o');
const [aiActionLog, setAiActionLog] = useState([]);
const [aiProcessing, setAiProcessing] = useState(false);
const [selectedProviderId, setSelectedProviderId] = useState('');
const [exportSettings, setExportSettings] = useState({
sampleRate: '44100',
bitDepth: '16',
@@ -3348,7 +3395,7 @@ const App = () => {
const sidebarEl = document.getElementById('right-sidebar');
const sidebarHeight = sidebarEl ? sidebarEl.getBoundingClientRect().height : 400;
const onMove = ev => {
const deltaY = ev.clientY - startY;
const deltaY = startY - ev.clientY;
const pct = (startHeight / 100 * sidebarHeight + deltaY) / sidebarHeight * 100;
const newPct = Math.max(20, Math.min(80, pct));
setMediaExplorerHeight(newPct);
@@ -6921,6 +6968,94 @@ const App = () => {
}, 800);
};
// ── AI Prompt Send to Active Provider ──
const handleAISend = async () => {
const prompt = aiPrompt.trim();
if (!prompt) {
showToast('Vui lòng nhập nội dung prompt.', 'warning');
return;
}
setAiProcessing(true);
setAiActionLog(prev => [...prev, {
type: 'status',
text: ` ⏳ Đang gửi prompt đến AI...`,
time: Date.now()
}]);
try {
const selectedProvider = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
const provider = selectedProvider || aiConfig;
const baseUrl = provider.api_base_url || provider.baseUrl || `${API_BASE_URL}`;
const apiKey = provider.api_key || provider.apiKey || '';
const model = provider.model_name || provider.model || 'deepseek-chat';
const dawContext = window.AIGateway.buildAIPromptContext({
tracks,
bpm,
selectedTrackId,
currentTime,
selLeft,
selRight
});
const result = await window.AIGateway.executeAIPrompt({
prompt,
provider: provider.name || 'default',
model,
apiKey,
baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
dawContext,
tools: window.AIGateway.DEFAULT_TOOLS
});
if (result.textResponse) {
setAiActionLog(prev => [...prev, {
type: 'status',
text: ` AI: ${result.textResponse}`,
time: Date.now()
}]);
}
if (result.functionCalls && result.functionCalls.length > 0) {
for (const fc of result.functionCalls) {
setAiActionLog(prev => [...prev, {
type: 'info',
text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`,
time: Date.now()
}]);
const cmdName = fc.name.toUpperCase();
if (window.DAWCommandDispatcher) {
try {
const cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
setAiActionLog(prev => [...prev, {
type: 'status',
text: `${fc.name}: ${cmdResult.success ? 'thành công' : 'thất bại'}`,
time: Date.now()
}]);
} catch (cmdErr) {
setAiActionLog(prev => [...prev, {
type: 'error',
text: `${fc.name}: ${cmdErr.message}`,
time: Date.now()
}]);
}
}
}
}
setAiActionLog(prev => [...prev, {
type: 'status',
text: ` Hoàn tất.`,
time: Date.now()
}]);
setAiPrompt('');
setTimeout(() => lucide.createIcons(), 200);
} catch (err) {
setAiActionLog(prev => [...prev, {
type: 'error',
text: ` Lỗi: ${err.message}`,
time: Date.now()
}]);
showToast(`AI Error: ${err.message}`, 'error');
} finally {
setAiProcessing(false);
}
};
// ── Split Track at Playhead ──
const handleSplitTrackAtTime = (trackId, clipId, time) => {
const track = tracks.find(t => t.id === trackId);
@@ -8174,42 +8309,22 @@ const App = () => {
"data-lucide": "x",
className: "w-3 h-3"
}))))), /*#__PURE__*/React.createElement("div", {
className: "p-1.5 bg-[#141414] rounded border border-zinc-800 text-xs font-mono min-h-[20px]"
}, /*#__PURE__*/React.createElement("div", {
className: "text-zinc-500"
}, "// ", /*#__PURE__*/React.createElement("span", {
className: "text-zinc-300"
}, analysisState.status)), analysisState.data && /*#__PURE__*/React.createElement("div", {
className: "text-emerald-500 font-semibold"
}, "BPM: ", analysisState.data.bpm)), /*#__PURE__*/React.createElement("div", {
className: "grid grid-cols-3 gap-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: handleAIScan,
className: "py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-xs border border-purple-700 flex items-center justify-center gap-1"
className: "flex items-center gap-1.5 shrink-0"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "map-pin",
className: "w-3 h-3"
})), " Scan"), /*#__PURE__*/React.createElement("button", {
onClick: handleAICutToNewTrack,
disabled: analysisState.isRunning,
className: "py-1 bg-fuchsia-800 hover:bg-fuchsia-700 text-white font-bold rounded text-xs border border-fuchsia-600 flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "scissors",
className: "w-3 h-3"
})), " Cut"), /*#__PURE__*/React.createElement("button", {
onClick: handleAIAnalysicLoop,
disabled: analysisState.isRunning,
className: "py-1 bg-violet-800 hover:bg-violet-700 text-violet-100 font-bold rounded text-xs border border-violet-600 flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "sparkles",
className: "w-3 h-3"
})), " Loop")), /*#__PURE__*/React.createElement("div", {
"data-lucide": "cpu",
className: "w-3 h-3 text-purple-400"
})), /*#__PURE__*/React.createElement("select", {
value: selectedProviderId,
onChange: e => setSelectedProviderId(e.target.value),
className: "flex-1 bg-[#141414] text-zinc-200 border border-zinc-700 rounded px-1 py-0.5 text-xs font-mono focus:outline-none focus:border-purple-600"
}, aiProviders.length === 0 ? /*#__PURE__*/React.createElement("option", {
value: ""
}, "Chưa có provider") : aiProviders.map(p => /*#__PURE__*/React.createElement("option", {
key: p.id,
value: p.id
}, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1"
}, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1"
@@ -8225,15 +8340,15 @@ const App = () => {
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-none",
rows: 2,
onKeyDown: e => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
window.executeAIPrompt();
handleAISend();
}
}
}), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 mt-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => window.executeAIPrompt(),
onClick: handleAISend,
disabled: aiProcessing,
className: "flex-1 py-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded text-xs border border-purple-500 flex items-center justify-center gap-1"
}, aiProcessing ? 'Đang suy luận...' : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
@@ -8249,10 +8364,10 @@ 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 mt-0.5"
}, "Ctrl+Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1"
}, "Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1 flex-1 min-h-0 flex flex-col"
}, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center justify-between"
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center justify-between shrink-0"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center gap-1"
}, /*#__PURE__*/React.createElement("i", {
@@ -8281,9 +8396,9 @@ const App = () => {
},
className: "text-xs text-amber-400 hover:text-amber-300 border border-amber-800 rounded px-1 py-0.5"
}, "Undo"))), /*#__PURE__*/React.createElement("div", {
className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 p-1 max-h-[120px]"
className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 p-1 select-text"
}, aiActionLog.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: "text-xs text-zinc-600 italic"
className: "text-xs text-zinc-600 italic select-text"
}, "Chưa có hành động nào.") : aiActionLog.map((entry, i) => /*#__PURE__*/React.createElement("div", {
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'}`
@@ -9732,7 +9847,12 @@ const App = () => {
onClose: () => setProfileModalOpen(false)
}), /*#__PURE__*/React.createElement(AIConfigModal, {
isOpen: aiConfigModalOpen,
onClose: () => setAiConfigModalOpen(false)
onClose: () => setAiConfigModalOpen(false),
onConfigSaved: providers => {
setAiProviders(providers);
const active = providers.find(p => p.is_active) || providers[0];
if (active) setSelectedProviderId(active.id);
}
}), /*#__PURE__*/React.createElement(SystemManagerModal, {
isOpen: systemManagerModalOpen,
onClose: () => setSystemManagerModalOpen(false)
+39 -7
View File
@@ -63,8 +63,26 @@ const AIGateway = (function() {
}
}];
function parseOrigin(urlStr) {
try { const u = new URL(urlStr); return `${u.protocol}//${u.hostname}${u.port ? ':'+u.port : ''}`; } catch (_) { return null; }
}
function isLocalhost(urlStr) {
try {
const u = new URL(urlStr);
return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '0.0.0.0' || u.hostname === '::1';
} catch (_) { return false; }
}
async function callLLM({ provider, model, apiKey, baseUrl, messages, tools, toolChoice }) {
const url = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
const base = baseUrl.replace(/\/$/, '');
const url = `${base}/chat/completions`;
const origin = window.location.origin;
const urlOrigin = parseOrigin(url);
const appOrigin = parseOrigin(origin);
const sameOrigin = urlOrigin === appOrigin;
const targetIsLocal = isLocalhost(url);
const headers = {
'Content-Type': 'application/json',
...(apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {})
@@ -77,15 +95,29 @@ const AIGateway = (function() {
...(toolChoice ? { tool_choice: toolChoice } : {})
};
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
let response;
if (sameOrigin) {
response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
} else if (targetIsLocal && !isLocalhost(origin)) {
throw new Error(`AI provider local (${url}) không khả dụng từ domain từ xa (${origin}).\nHãy dùng provider từ xa (OpenAI, Anthropic...) hoặc dùng CORS plugin trình duyệt.`);
} else {
response = await fetch(`${origin}/api/v1/ai/proxy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, headers, body })
});
}
if (!response.ok) {
const errText = await response.text();
throw new Error(`LLM API error ${response.status}: ${errText}`);
let detail = errText;
try { const j = JSON.parse(errText); if (j.detail) detail = j.detail; } catch (_) {}
throw new Error(detail);
}
return await response.json();