[pending-judge] feat(DC-163): AI assistant that works — LLM intent engine + fixed chat panel
The floating assistant was broken end-to-end; Sami said "Do it that works".
Three user-visible bugs fixed + the promised LLM delegation implemented:
FRONTEND (status/js/ai-chat.js — rewritten):
- Response parsing: old code read data.data; the API returns the FLAT ok()
envelope — EVERY reply rendered "Sorry, something went wrong". Now reads
the real shape (contract asserted by route test).
- Suggestion chips: onclick="...send('"Deploy Plex"')" — nested quotes,
invalid JS, dead buttons (shipped like this). Now data-driven buttons with
addEventListener.
- Deploy button: called window.openAppSelector which NEVER existed anywhere
in the tree (grep-verified). Now opens the REAL deploy modal via new
window.openDeployConfigFor(templateId) exported by app-selector.js —
resolves against the live API template cache, reuses showDeployConfig
(same path a manual catalog click takes). Widgets route to the grid.
- Conversation history (last 6 turns) now rides along per request, engine
badge in header, ⚙ settings sub-panel (baseUrl/model/API key + Test +
Remove), all server text via textContent (no HTML injection), CSP-safe
(no inline handlers).
BACKEND (routes/ai-intent.js):
- TWO engines, one contract. LLM classifier (OpenAI-compatible chat
completions) with system prompt embedding the LIVE 78-app catalog + the
six wizard categories; regex patterns kept as the zero-config default and
the automatic failover (HTTP error, timeout via AbortController,
unparseable output -> pattern answers; response carries engine +
llmError so the UI can say which served — no silent degradation).
- Closed-vocabulary schema validation: intent MUST be in the known set else
'unknown'; appId MUST exist in APP_TEMPLATES else nulled (hallucinated
ids cannot leak into deploy plans); categories filtered to the wizard's
real keys; reply stripped of control chars + bounded to 600; history
role-filtered (only user/assistant pass — client-injected system turns
dropped) + capped; input capped to 1000 chars.
- enrich() rebuilds the response contract per intent; pattern-canned
response is dropped when the LLM rerouted the intent (no stale shape);
deployPlan body.port now fills from the template's defaultPort (was
hardcoded null); deployPlan shape asserted against wizard usage.
- Key storage: PUT /ai/settings stores {baseUrl,model,apiKey} in the
ENCRYPTED credential store (key 'ai:llm') via ctx.credentialManager —
NOT config.json (GET /config echoes the raw file to the browser; that
route's shape was verified before this decision). Blank apiKey on PUT
keeps the stored one. GET /ai/settings reports configured/source/model/
hasApiKey — the key value NEVER crosses the wire (asserted). Env
fallback AI_LLM_BASE_URL/AI_LLM_API_KEY/AI_LLM_MODEL + legacy
LLM_PROXY_URL for headless installs. POST /ai/settings/test = one-shot
real classify probe returning ok/sample or ok:false/reason (never
throws). DELETE clears.
- GET /ai/capabilities now reports engine + llm status (pattern was honest
marketing before; now it can be true).
MOUNT (src/app.js): aiIntentRoutes receives siteConfig, log, fetchImpl
(ctx.fetchT — test-injectable) and credentialManager.
Docs: OpenAPI +4 AI path entries (intent, capabilities, settings, settings/test); CHANGELOG entry; dist bundle rebuilt
(openDeployConfigFor present in features.js; SW cache c62511c40d).
Tests: 20 NEW in __tests__/routes/ai-intent-engine.test.js (engine
selection w/ and w/o store, off-catalog phrasing via mock LLM,
hallucinated-id nulling, illegal intent coercion, HTTP-500 failover,
AbortError timeout failover, markdown-fence repair, reply sanitization,
history cap+role filter, env config path, intent-disagreement rebuild w/
real jellyfin port 8096, flat envelope contract, 400s, 1000-char cap,
settings GET/PUT/DELETE/test incl. key-never-echoed assertions,
capabilities engine reporting) + the 18 original pattern tests unchanged +
composition-root e2e 8 pass. Full suite: 3643 passed; only the pre-existing
pdf-parse billing family red (6 failed in documented band [5,7], patch
touches no billing file).
This commit is contained in:
9 files changed
+1313
-293
No files matched your search
@@ -917,6 +917,28 @@
|
||||
modal.classList.add('show');
|
||||
});
|
||||
|
||||
// DC-163: programmatic deploy-config opener for the AI chat panel (and
|
||||
// any other in-page consumer). Resolves the template id against the
|
||||
// cached API templates (fetching them if needed), then opens the same
|
||||
// deploy modal a manual click would. Returns false for unknown ids.
|
||||
window.openDeployConfigFor = async function (templateId) {
|
||||
if (!apiTemplates) {
|
||||
const okFetch = await fetchAppTemplates();
|
||||
if (!okFetch) return false;
|
||||
}
|
||||
const key = String(templateId || '').toLowerCase();
|
||||
const entry = Object.entries(apiTemplates).find(([id]) => id.toLowerCase() === key);
|
||||
if (!entry) return false;
|
||||
const app = { id: entry[0], ...entry[1] };
|
||||
if (app.isDashboardWidget) {
|
||||
await buildAppSelector();
|
||||
modal.classList.add('show');
|
||||
return 'selector'; // widget — point at the selector grid instead
|
||||
}
|
||||
await showDeployConfig(app);
|
||||
return true;
|
||||
};
|
||||
|
||||
wireModal(modal, document.getElementById('app-selector-cancel'));
|
||||
|
||||
// Deploy modal event listeners
|
||||
|
||||
Reference in new issue
Block a user