feat: AI intent router live + TOTP repeat-auth fix
AI Intent Router:
- Wired /api/v1/ai/intent and /api/v1/ai/capabilities into app.js
- Pattern matching works offline, no API key needed
- Handles: deploy, recommend, diagnose, backup, health, list
- AI chat floating button on dashboard (🤖)
- Suggestion chips: Deploy Plex, Stream movies, Block ads, System health
- Deploy buttons in chat launch the app selector
TOTP Fix:
- secureFetch() was missing credentials: same-origin
- Session cookie was not being sent on API calls
- Added credentials: same-origin to all fetch calls
- Users no longer prompted for TOTP on every action
Nesting Guard:
- Fixed logging module path (../utils/logging not ./logging)
- Switched to console.log to avoid module export mismatch
MCP Server:
- 551-line JSON-RPC server ready at src/mcp/mcp-server.js
- Configurable via DASHCADDY_URL + DASHCADDY_API_KEY env vars
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// AI Chat Panel — DashCaddy AI Intent Router frontend
|
||||
(function() {
|
||||
let chatMessages = [];
|
||||
let chatHistory = [];
|
||||
|
||||
function createChatPanel() {
|
||||
const existing = document.getElementById('ai-chat-panel');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'ai-chat-panel';
|
||||
panel.style.cssText = `
|
||||
position: fixed; bottom: 80px; right: 20px; width: 400px; max-width: 95vw;
|
||||
height: 500px; max-height: 70vh; background: var(--bg-card, #1a1a2e);
|
||||
border: 1px solid var(--border, #333); border-radius: 12px;
|
||||
display: none; flex-direction: column; z-index: 9999;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
`;
|
||||
|
||||
panel.innerHTML = `
|
||||
<div style="padding: 14px 18px; border-bottom: 1px solid var(--border,#333); display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 1.3rem;">🤖</span>
|
||||
<div style="flex:1;">
|
||||
<div style="font-weight: 700; font-size: 0.95rem;">AI Assistant</div>
|
||||
<div id="ai-status" style="font-size: 0.72rem; color: var(--text-muted,#666);">Pattern matching • No key needed</div>
|
||||
</div>
|
||||
<button onclick="document.getElementById('ai-chat-panel').style.display='none'" style="background:none;border:none;font-size:1.4rem;cursor:pointer;color:var(--text-muted,#888);">×</button>
|
||||
</div>
|
||||
<div id="ai-chat-messages" style="flex:1; overflow-y:auto; padding:14px; display:flex; flex-direction:column; gap:10px;">
|
||||
<div style="background:rgba(99,102,241,0.08); border-radius:10px; padding:12px; font-size:0.85rem; color:var(--text-muted,#aaa);">
|
||||
👋 Hi! I can help you deploy apps, diagnose issues, and manage your server. Try:
|
||||
<div style="margin-top:8px; display:flex; flex-wrap:wrap; gap:6px;">
|
||||
<button class="ai-suggestion" onclick="window.aiChat.send('"Deploy Plex"')" style="background:rgba(99,102,241,0.15); border:1px solid rgba(99,102,241,0.3); border-radius:16px; padding:4px 12px; font-size:0.78rem; cursor:pointer; color:#a5b4fc;">Deploy Plex</button>
|
||||
<button class="ai-suggestion" onclick="window.aiChat.send('"I want to stream movies"')" style="background:rgba(99,102,241,0.15); border:1px solid rgba(99,102,241,0.3); border-radius:16px; padding:4px 12px; font-size:0.78rem; cursor:pointer; color:#a5b4fc;">Stream movies</button>
|
||||
<button class="ai-suggestion" onclick="window.aiChat.send('"Block ads on my network"')" style="background:rgba(99,102,241,0.15); border:1px solid rgba(99,102,241,0.3); border-radius:16px; padding:4px 12px; font-size:0.78rem; cursor:pointer; color:#a5b4fc;">Block ads</button>
|
||||
<button class="ai-suggestion" onclick="window.aiChat.send('"Is everything OK?')" style="background:rgba(99,102,241,0.15); border:1px solid rgba(99,102,241,0.3); border-radius:16px; padding:4px 12px; font-size:0.78rem; cursor:pointer; color:#a5b4fc;">System health</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:12px; border-top:1px solid var(--border,#333);">
|
||||
<div style="display:flex; gap:8px;">
|
||||
<input id="ai-chat-input" type="text" placeholder="Ask me anything..."
|
||||
onkeypress="if(event.key==='Enter') window.aiChat.send()"
|
||||
style="flex:1; background:var(--bg-input,#0f0f1a); border:1px solid var(--border,#333); border-radius:8px; padding:10px 14px; color:var(--text,#fff); font-size:0.88rem; outline:none;">
|
||||
<button onclick="window.aiChat.send()" style="background:#6366f1; border:none; border-radius:8px; padding:0 16px; color:#fff; cursor:pointer; font-weight:600;">→</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
let panel = document.getElementById('ai-chat-panel');
|
||||
if (!panel) { createChatPanel(); panel = document.getElementById('ai-chat-panel'); }
|
||||
panel.style.display = panel.style.display === 'flex' ? 'none' : 'flex';
|
||||
if (panel.style.display === 'flex') document.getElementById('ai-chat-input').focus();
|
||||
}
|
||||
|
||||
function addMessage(role, text, extra) {
|
||||
const container = document.getElementById('ai-chat-messages');
|
||||
if (!container) return;
|
||||
const div = document.createElement('div');
|
||||
const isUser = role === 'user';
|
||||
div.style.cssText = isUser
|
||||
? 'align-self:flex-end; background:rgba(99,102,241,0.15); border-radius:10px 10px 2px 10px; padding:10px 14px; max-width:85%; font-size:0.85rem;'
|
||||
: 'align-self:flex-start; background:rgba(255,255,255,0.05); border-radius:10px 10px 10px 2px; padding:10px 14px; max-width:85%; font-size:0.85rem;';
|
||||
div.textContent = text;
|
||||
container.appendChild(div);
|
||||
|
||||
// Extra content (recommendations, action buttons)
|
||||
if (extra) {
|
||||
const extraDiv = document.createElement('div');
|
||||
extraDiv.style.cssText = 'align-self:flex-start; max-width:85%; margin-top:4px;';
|
||||
if (extra.recommendations) {
|
||||
extra.recommendations.forEach(rec => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = `📦 Deploy ${rec.app}`;
|
||||
btn.style.cssText = 'display:block; margin:4px 0; background:rgba(34,197,94,0.12); border:1px solid rgba(34,197,94,0.3); border-radius:8px; padding:8px 12px; cursor:pointer; font-size:0.8rem; color:#4ade80;';
|
||||
btn.onclick = () => window.aiChat.send('Deploy ' + rec.app);
|
||||
extraDiv.appendChild(btn);
|
||||
});
|
||||
}
|
||||
if (extra.appId) {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = `🚀 Deploy ${extra.appId}`;
|
||||
btn.style.cssText = 'display:block; margin:4px 0; background:rgba(34,197,94,0.12); border:1px solid rgba(34,197,94,0.3); border-radius:8px; padding:8px 12px; cursor:pointer; font-size:0.8rem; color:#4ade80;';
|
||||
btn.onclick = () => window.aiChat.deploy(extra.appId);
|
||||
extraDiv.appendChild(btn);
|
||||
}
|
||||
if (extra.suggestions) {
|
||||
const list = document.createElement('div');
|
||||
list.style.cssText = 'font-size:0.78rem; color:var(--text-muted,#888); padding:4px 0;';
|
||||
list.innerHTML = extra.suggestions.map(s => '• ' + s).join('<br>');
|
||||
extraDiv.appendChild(list);
|
||||
}
|
||||
if (extra.action === 'diagnose') {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = '🔍 Run Diagnostics';
|
||||
btn.style.cssText = 'display:block; margin:4px 0; background:rgba(251,191,36,0.12); border:1px solid rgba(251,191,36,0.3); border-radius:8px; padding:8px 12px; cursor:pointer; font-size:0.8rem; color:#fbbf24;';
|
||||
btn.onclick = () => window.location.href = '#health';
|
||||
extraDiv.appendChild(btn);
|
||||
}
|
||||
if (extra.action === 'backup') {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = '💾 Create Backup';
|
||||
btn.style.cssText = 'display:block; margin:4px 0; background:rgba(99,102,241,0.12); border:1px solid rgba(99,102,241,0.3); border-radius:8px; padding:8px 12px; cursor:pointer; font-size:0.8rem; color:#a5b4fc;';
|
||||
btn.onclick = () => { if (window.createBackup) window.createBackup(); };
|
||||
extraDiv.appendChild(btn);
|
||||
}
|
||||
container.appendChild(extraDiv);
|
||||
}
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
async function send(prefilled) {
|
||||
const input = document.getElementById('ai-chat-input');
|
||||
const message = prefilled || input.value.trim();
|
||||
if (!message) return;
|
||||
input.value = '';
|
||||
|
||||
addMessage('user', message);
|
||||
|
||||
// Typing indicator
|
||||
const container = document.getElementById('ai-chat-messages');
|
||||
const typing = document.createElement('div');
|
||||
typing.id = 'ai-typing';
|
||||
typing.style.cssText = 'align-self:flex-start; color:var(--text-muted,#666); font-size:0.8rem; padding:8px 14px;';
|
||||
typing.textContent = '🤖 Thinking...';
|
||||
container.appendChild(typing);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
try {
|
||||
const res = await secureFetch('/api/v1/ai/intent', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message, context: {} })
|
||||
});
|
||||
const data = await res.json();
|
||||
typing.remove();
|
||||
|
||||
if (data.success !== false && data.data) {
|
||||
const r = data.data;
|
||||
const response = r.response || {};
|
||||
addMessage('ai', response.message || r.response?.message || 'Action: ' + (r.action || r.intent), response);
|
||||
} else {
|
||||
addMessage('ai', data.error || 'Sorry, something went wrong.');
|
||||
}
|
||||
} catch (e) {
|
||||
typing.remove();
|
||||
addMessage('ai', 'Connection error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deploy(appId) {
|
||||
addMessage('user', 'Deploy ' + appId);
|
||||
addMessage('ai', `Launching ${appId} from the catalog...`);
|
||||
// Trigger the app selector with this app
|
||||
if (window.openAppSelector) {
|
||||
window.openAppSelector(appId);
|
||||
} else {
|
||||
addMessage('ai', `Search for ${appId} in the App Catalog to deploy it.`);
|
||||
}
|
||||
}
|
||||
|
||||
window.aiChat = { send, deploy, toggle };
|
||||
window.toggleAIChat = toggle;
|
||||
})();
|
||||
Reference in New Issue
Block a user