fix: đã sửa lỗi AI gửi prompt
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, Any, Dict, List
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
class ProxyRequest(BaseModel):
|
||||||
|
url: str
|
||||||
|
headers: Dict[str, str] = {}
|
||||||
|
body: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
@router.post("/proxy")
|
||||||
|
async def proxy_llm(req: ProxyRequest):
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
req.url,
|
||||||
|
headers={k: v for k, v in req.headers.items() if k.lower() not in ('host', 'origin', 'referer')},
|
||||||
|
json=req.body
|
||||||
|
)
|
||||||
|
raw = resp.text
|
||||||
|
try:
|
||||||
|
return resp.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
try:
|
||||||
|
return json.loads(raw[:raw.find('\n')])
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
return {"content": raw}
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
raise HTTPException(status_code=504, detail="AI provider timeout")
|
||||||
|
except httpx.ConnectError as e:
|
||||||
|
msg = f"Cannot connect to AI provider: {e}"
|
||||||
|
if 'localhost' in req.url or '127.0.0.1' in req.url:
|
||||||
|
msg += "\nNếu app chạy trong Docker, localhost trỏ vào container, không ra host.\nHãy thay localhost bằng host.docker.internal hoặc IP bridge Docker (172.17.0.1)."
|
||||||
|
raise HTTPException(status_code=502, detail=msg)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@@ -11,6 +11,7 @@ from app.api.v1.auth import router as auth_router
|
|||||||
from app.api.v1.admin import router as admin_router
|
from app.api.v1.admin import router as admin_router
|
||||||
from app.api.v1.projects import router as projects_router
|
from app.api.v1.projects import router as projects_router
|
||||||
from app.api.v1.user_config import router as user_config_router
|
from app.api.v1.user_config import router as user_config_router
|
||||||
|
from app.api.v1.ai_proxy import router as ai_proxy_router
|
||||||
from app.core.auth import seed_admin
|
from app.core.auth import seed_admin
|
||||||
|
|
||||||
# Ensure storage directories exist
|
# Ensure storage directories exist
|
||||||
@@ -45,6 +46,7 @@ app.include_router(auth_router, prefix="/api/v1/auth", tags=["auth"])
|
|||||||
app.include_router(admin_router, prefix="/api/v1/admin", tags=["admin"])
|
app.include_router(admin_router, prefix="/api/v1/admin", tags=["admin"])
|
||||||
app.include_router(projects_router, prefix="/api/v1/projects", tags=["projects"])
|
app.include_router(projects_router, prefix="/api/v1/projects", tags=["projects"])
|
||||||
app.include_router(user_config_router, prefix="/api/v1/user", tags=["user_config"])
|
app.include_router(user_config_router, prefix="/api/v1/user", tags=["user_config"])
|
||||||
|
app.include_router(ai_proxy_router, prefix="/api/v1/ai", tags=["ai"])
|
||||||
|
|
||||||
# Seed admin user on startup
|
# Seed admin user on startup
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
+129
-45
@@ -2481,7 +2481,8 @@ const AuthModal = ({
|
|||||||
};
|
};
|
||||||
const AIConfigModal = ({
|
const AIConfigModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose
|
onClose,
|
||||||
|
onConfigSaved
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
const defaultProvidersList = [{
|
const defaultProvidersList = [{
|
||||||
@@ -2552,6 +2553,7 @@ const AIConfigModal = ({
|
|||||||
try {
|
try {
|
||||||
const res = await window.SonicAPI.saveAIConfigs(providers);
|
const res = await window.SonicAPI.saveAIConfigs(providers);
|
||||||
setMsg(res.message || 'Đã lưu cấu hình AI Providers thành công!');
|
setMsg(res.message || 'Đã lưu cấu hình AI Providers thành công!');
|
||||||
|
if (onConfigSaved) onConfigSaved(providers);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Lỗi khi lưu cấu hình AI');
|
setError(err.message || 'Lỗi khi lưu cấu hình AI');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2594,7 +2596,38 @@ const AIConfigModal = ({
|
|||||||
className: "truncate"
|
className: "truncate"
|
||||||
}, p.name), p.is_active && /*#__PURE__*/React.createElement("span", {
|
}, p.name), p.is_active && /*#__PURE__*/React.createElement("span", {
|
||||||
className: "w-2 h-2 rounded-full bg-emerald-400"
|
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,
|
onSubmit: handleSave,
|
||||||
className: "col-span-2 space-y-3.5"
|
className: "col-span-2 space-y-3.5"
|
||||||
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
|
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
|
||||||
@@ -3004,6 +3037,19 @@ const App = () => {
|
|||||||
apiKey: localStorage.getItem('ai_api_key') || '',
|
apiKey: localStorage.getItem('ai_api_key') || '',
|
||||||
model: localStorage.getItem('ai_model') || 'deepseek-chat'
|
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({
|
const [analysisState, setAnalysisState] = useState({
|
||||||
status: 'Sẵn sàng. Chạy AI để phân tích nhịp.',
|
status: 'Sẵn sàng. Chạy AI để phân tích nhịp.',
|
||||||
data: null,
|
data: null,
|
||||||
@@ -3014,6 +3060,7 @@ const App = () => {
|
|||||||
const [aiModel, setAiModel] = useState('GPT-4o');
|
const [aiModel, setAiModel] = useState('GPT-4o');
|
||||||
const [aiActionLog, setAiActionLog] = useState([]);
|
const [aiActionLog, setAiActionLog] = useState([]);
|
||||||
const [aiProcessing, setAiProcessing] = useState(false);
|
const [aiProcessing, setAiProcessing] = useState(false);
|
||||||
|
const [selectedProviderId, setSelectedProviderId] = useState('');
|
||||||
const [exportSettings, setExportSettings] = useState({
|
const [exportSettings, setExportSettings] = useState({
|
||||||
sampleRate: '44100',
|
sampleRate: '44100',
|
||||||
bitDepth: '16',
|
bitDepth: '16',
|
||||||
@@ -3348,7 +3395,7 @@ const App = () => {
|
|||||||
const sidebarEl = document.getElementById('right-sidebar');
|
const sidebarEl = document.getElementById('right-sidebar');
|
||||||
const sidebarHeight = sidebarEl ? sidebarEl.getBoundingClientRect().height : 400;
|
const sidebarHeight = sidebarEl ? sidebarEl.getBoundingClientRect().height : 400;
|
||||||
const onMove = ev => {
|
const onMove = ev => {
|
||||||
const deltaY = ev.clientY - startY;
|
const deltaY = startY - ev.clientY;
|
||||||
const pct = ((startHeight / 100 * sidebarHeight + deltaY) / sidebarHeight) * 100;
|
const pct = ((startHeight / 100 * sidebarHeight + deltaY) / sidebarHeight) * 100;
|
||||||
const newPct = Math.max(20, Math.min(80, pct));
|
const newPct = Math.max(20, Math.min(80, pct));
|
||||||
setMediaExplorerHeight(newPct);
|
setMediaExplorerHeight(newPct);
|
||||||
@@ -6922,6 +6969,58 @@ const App = () => {
|
|||||||
}, 800);
|
}, 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 ──
|
// ── Split Track at Playhead ──
|
||||||
const handleSplitTrackAtTime = (trackId, clipId, time) => {
|
const handleSplitTrackAtTime = (trackId, clipId, time) => {
|
||||||
const track = tracks.find(t => t.id === trackId);
|
const track = tracks.find(t => t.id === trackId);
|
||||||
@@ -8087,42 +8186,22 @@ const App = () => {
|
|||||||
"data-lucide": "x",
|
"data-lucide": "x",
|
||||||
className: "w-3 h-3"
|
className: "w-3 h-3"
|
||||||
}))))), /*#__PURE__*/React.createElement("div", {
|
}))))), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "p-1.5 bg-[#141414] rounded border border-zinc-800 text-xs font-mono min-h-[20px]"
|
className: "flex items-center gap-1.5 shrink-0"
|
||||||
}, /*#__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"
|
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "map-pin",
|
"data-lucide": "cpu",
|
||||||
className: "w-3 h-3"
|
className: "w-3 h-3 text-purple-400"
|
||||||
})), " Scan"), /*#__PURE__*/React.createElement("button", {
|
})), /*#__PURE__*/React.createElement("select", {
|
||||||
onClick: handleAICutToNewTrack,
|
value: selectedProviderId,
|
||||||
disabled: analysisState.isRunning,
|
onChange: e => setSelectedProviderId(e.target.value),
|
||||||
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"
|
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"
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, aiProviders.length === 0 ? /*#__PURE__*/React.createElement("option", {
|
||||||
className: "inline-flex items-center shrink-0"
|
value: ""
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, "Chưa có provider") : aiProviders.map(p => /*#__PURE__*/React.createElement("option", {
|
||||||
"data-lucide": "scissors",
|
key: p.id,
|
||||||
className: "w-3 h-3"
|
value: p.id
|
||||||
})), " Cut"), /*#__PURE__*/React.createElement("button", {
|
}, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
|
||||||
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", {
|
|
||||||
className: "border-t border-zinc-800 pt-1.5 mt-1"
|
className: "border-t border-zinc-800 pt-1.5 mt-1"
|
||||||
}, /*#__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"
|
||||||
@@ -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",
|
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,
|
rows: 2,
|
||||||
onKeyDown: e => {
|
onKeyDown: e => {
|
||||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.executeAIPrompt();
|
handleAISend();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), /*#__PURE__*/React.createElement("div", {
|
}), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex items-center gap-1 mt-1"
|
className: "flex items-center gap-1 mt-1"
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => window.executeAIPrompt(),
|
onClick: handleAISend,
|
||||||
disabled: aiProcessing,
|
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"
|
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", {
|
}, 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"
|
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 mt-0.5"
|
className: "text-xs text-zinc-600 mt-0.5"
|
||||||
}, "Ctrl+Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
|
}, "Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "border-t border-zinc-800 pt-1.5 mt-1"
|
className: "border-t border-zinc-800 pt-1.5 mt-1 flex-1 min-h-0 flex flex-col"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center gap-1"
|
className: "inline-flex items-center gap-1"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__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"
|
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", {
|
}, "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", {
|
}, 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", {
|
}, "Chưa có hành động nào.") : aiActionLog.map((entry, i) => /*#__PURE__*/React.createElement("div", {
|
||||||
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'}`
|
||||||
@@ -9637,7 +9716,12 @@ const App = () => {
|
|||||||
onClose: () => setProfileModalOpen(false)
|
onClose: () => setProfileModalOpen(false)
|
||||||
}), /*#__PURE__*/React.createElement(AIConfigModal, {
|
}), /*#__PURE__*/React.createElement(AIConfigModal, {
|
||||||
isOpen: aiConfigModalOpen,
|
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, {
|
}), /*#__PURE__*/React.createElement(SystemManagerModal, {
|
||||||
isOpen: systemManagerModalOpen,
|
isOpen: systemManagerModalOpen,
|
||||||
onClose: () => setSystemManagerModalOpen(false)
|
onClose: () => setSystemManagerModalOpen(false)
|
||||||
|
|||||||
@@ -2481,7 +2481,8 @@ const AuthModal = ({
|
|||||||
};
|
};
|
||||||
const AIConfigModal = ({
|
const AIConfigModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose
|
onClose,
|
||||||
|
onConfigSaved
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
const defaultProvidersList = [{
|
const defaultProvidersList = [{
|
||||||
@@ -2552,6 +2553,7 @@ const AIConfigModal = ({
|
|||||||
try {
|
try {
|
||||||
const res = await window.SonicAPI.saveAIConfigs(providers);
|
const res = await window.SonicAPI.saveAIConfigs(providers);
|
||||||
setMsg(res.message || 'Đã lưu cấu hình AI Providers thành công!');
|
setMsg(res.message || 'Đã lưu cấu hình AI Providers thành công!');
|
||||||
|
if (onConfigSaved) onConfigSaved(providers);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Lỗi khi lưu cấu hình AI');
|
setError(err.message || 'Lỗi khi lưu cấu hình AI');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2594,7 +2596,38 @@ const AIConfigModal = ({
|
|||||||
className: "truncate"
|
className: "truncate"
|
||||||
}, p.name), p.is_active && /*#__PURE__*/React.createElement("span", {
|
}, p.name), p.is_active && /*#__PURE__*/React.createElement("span", {
|
||||||
className: "w-2 h-2 rounded-full bg-emerald-400"
|
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,
|
onSubmit: handleSave,
|
||||||
className: "col-span-2 space-y-3.5"
|
className: "col-span-2 space-y-3.5"
|
||||||
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
|
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
|
||||||
@@ -3004,6 +3037,19 @@ const App = () => {
|
|||||||
apiKey: localStorage.getItem('ai_api_key') || '',
|
apiKey: localStorage.getItem('ai_api_key') || '',
|
||||||
model: localStorage.getItem('ai_model') || 'deepseek-chat'
|
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({
|
const [analysisState, setAnalysisState] = useState({
|
||||||
status: 'Sẵn sàng. Chạy AI để phân tích nhịp.',
|
status: 'Sẵn sàng. Chạy AI để phân tích nhịp.',
|
||||||
data: null,
|
data: null,
|
||||||
@@ -3014,6 +3060,7 @@ const App = () => {
|
|||||||
const [aiModel, setAiModel] = useState('GPT-4o');
|
const [aiModel, setAiModel] = useState('GPT-4o');
|
||||||
const [aiActionLog, setAiActionLog] = useState([]);
|
const [aiActionLog, setAiActionLog] = useState([]);
|
||||||
const [aiProcessing, setAiProcessing] = useState(false);
|
const [aiProcessing, setAiProcessing] = useState(false);
|
||||||
|
const [selectedProviderId, setSelectedProviderId] = useState('');
|
||||||
const [exportSettings, setExportSettings] = useState({
|
const [exportSettings, setExportSettings] = useState({
|
||||||
sampleRate: '44100',
|
sampleRate: '44100',
|
||||||
bitDepth: '16',
|
bitDepth: '16',
|
||||||
@@ -3348,7 +3395,7 @@ const App = () => {
|
|||||||
const sidebarEl = document.getElementById('right-sidebar');
|
const sidebarEl = document.getElementById('right-sidebar');
|
||||||
const sidebarHeight = sidebarEl ? sidebarEl.getBoundingClientRect().height : 400;
|
const sidebarHeight = sidebarEl ? sidebarEl.getBoundingClientRect().height : 400;
|
||||||
const onMove = ev => {
|
const onMove = ev => {
|
||||||
const deltaY = ev.clientY - startY;
|
const deltaY = startY - ev.clientY;
|
||||||
const pct = (startHeight / 100 * sidebarHeight + deltaY) / sidebarHeight * 100;
|
const pct = (startHeight / 100 * sidebarHeight + deltaY) / sidebarHeight * 100;
|
||||||
const newPct = Math.max(20, Math.min(80, pct));
|
const newPct = Math.max(20, Math.min(80, pct));
|
||||||
setMediaExplorerHeight(newPct);
|
setMediaExplorerHeight(newPct);
|
||||||
@@ -6921,6 +6968,94 @@ const App = () => {
|
|||||||
}, 800);
|
}, 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 ──
|
// ── Split Track at Playhead ──
|
||||||
const handleSplitTrackAtTime = (trackId, clipId, time) => {
|
const handleSplitTrackAtTime = (trackId, clipId, time) => {
|
||||||
const track = tracks.find(t => t.id === trackId);
|
const track = tracks.find(t => t.id === trackId);
|
||||||
@@ -8174,42 +8309,22 @@ const App = () => {
|
|||||||
"data-lucide": "x",
|
"data-lucide": "x",
|
||||||
className: "w-3 h-3"
|
className: "w-3 h-3"
|
||||||
}))))), /*#__PURE__*/React.createElement("div", {
|
}))))), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "p-1.5 bg-[#141414] rounded border border-zinc-800 text-xs font-mono min-h-[20px]"
|
className: "flex items-center gap-1.5 shrink-0"
|
||||||
}, /*#__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"
|
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "map-pin",
|
"data-lucide": "cpu",
|
||||||
className: "w-3 h-3"
|
className: "w-3 h-3 text-purple-400"
|
||||||
})), " Scan"), /*#__PURE__*/React.createElement("button", {
|
})), /*#__PURE__*/React.createElement("select", {
|
||||||
onClick: handleAICutToNewTrack,
|
value: selectedProviderId,
|
||||||
disabled: analysisState.isRunning,
|
onChange: e => setSelectedProviderId(e.target.value),
|
||||||
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"
|
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"
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, aiProviders.length === 0 ? /*#__PURE__*/React.createElement("option", {
|
||||||
className: "inline-flex items-center shrink-0"
|
value: ""
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, "Chưa có provider") : aiProviders.map(p => /*#__PURE__*/React.createElement("option", {
|
||||||
"data-lucide": "scissors",
|
key: p.id,
|
||||||
className: "w-3 h-3"
|
value: p.id
|
||||||
})), " Cut"), /*#__PURE__*/React.createElement("button", {
|
}, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
|
||||||
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", {
|
|
||||||
className: "border-t border-zinc-800 pt-1.5 mt-1"
|
className: "border-t border-zinc-800 pt-1.5 mt-1"
|
||||||
}, /*#__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"
|
||||||
@@ -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",
|
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,
|
rows: 2,
|
||||||
onKeyDown: e => {
|
onKeyDown: e => {
|
||||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.executeAIPrompt();
|
handleAISend();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), /*#__PURE__*/React.createElement("div", {
|
}), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex items-center gap-1 mt-1"
|
className: "flex items-center gap-1 mt-1"
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => window.executeAIPrompt(),
|
onClick: handleAISend,
|
||||||
disabled: aiProcessing,
|
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"
|
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", {
|
}, 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"
|
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 mt-0.5"
|
className: "text-xs text-zinc-600 mt-0.5"
|
||||||
}, "Ctrl+Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
|
}, "Enter để gửi nhanh")), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "border-t border-zinc-800 pt-1.5 mt-1"
|
className: "border-t border-zinc-800 pt-1.5 mt-1 flex-1 min-h-0 flex flex-col"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center gap-1"
|
className: "inline-flex items-center gap-1"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__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"
|
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", {
|
}, "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", {
|
}, 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", {
|
}, "Chưa có hành động nào.") : aiActionLog.map((entry, i) => /*#__PURE__*/React.createElement("div", {
|
||||||
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'}`
|
||||||
@@ -9732,7 +9847,12 @@ const App = () => {
|
|||||||
onClose: () => setProfileModalOpen(false)
|
onClose: () => setProfileModalOpen(false)
|
||||||
}), /*#__PURE__*/React.createElement(AIConfigModal, {
|
}), /*#__PURE__*/React.createElement(AIConfigModal, {
|
||||||
isOpen: aiConfigModalOpen,
|
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, {
|
}), /*#__PURE__*/React.createElement(SystemManagerModal, {
|
||||||
isOpen: systemManagerModalOpen,
|
isOpen: systemManagerModalOpen,
|
||||||
onClose: () => setSystemManagerModalOpen(false)
|
onClose: () => setSystemManagerModalOpen(false)
|
||||||
|
|||||||
@@ -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 }) {
|
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 = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {})
|
...(apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {})
|
||||||
@@ -77,15 +95,29 @@ const AIGateway = (function() {
|
|||||||
...(toolChoice ? { tool_choice: toolChoice } : {})
|
...(toolChoice ? { tool_choice: toolChoice } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(url, {
|
let response;
|
||||||
method: 'POST',
|
|
||||||
headers,
|
if (sameOrigin) {
|
||||||
body: JSON.stringify(body)
|
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) {
|
if (!response.ok) {
|
||||||
const errText = await response.text();
|
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();
|
return await response.json();
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user