diff --git a/CHANGELOG.md b/CHANGELOG.md index be8d9f1..b3ca97b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Lumi changelog +## 0.2.7 + +- Reworked the Lumi AI Improvement Center into a live review queue with one-step apply, dismiss, and no-change outcomes. +- Added searchable finalized-feedback history with restore, edit, and permanent-delete controls while preserving existing feedback data. + ## 0.2.6 - Fixed plugin updates on Windows and network shares when a running plugin keeps its preserved `data` directory open. diff --git a/TODO.md b/TODO.md index c0dccb6..4e3eb7d 100644 --- a/TODO.md +++ b/TODO.md @@ -680,6 +680,7 @@ This section is for Lumi chat answer feedback and AI Improvement Center work, no - 2026-07-18: Fixed private reverse-proxy HTTPS recognition for production diagnostics in core 0.2.5 without globally trusting client forwarding headers, and taught the local client to consume the repository's env-style `.secrets` file and short diagnostic variable names directly. - 2026-07-18: Fixed Windows/network-share plugin updates in core 0.2.6 by leaving live preserved plugin data in place and transactionally replacing only code, including automatic code rollback coverage. - 2026-07-18: Retired the one-off core repair ZIP build scripts after repository updates proved reliable; core 0.2.6 is distributed through the normal immutable Git release flow. +- 2026-07-18: Reworked Lumi AI 0.8.3's Improvement Center into a live, non-technical review queue with one-step activation, direct final outcomes, automatic queue cleanup, and searchable/restorable/editable finalized-feedback history. - 2026-07-18: Updated production diagnostics examples in core 0.2.4 to derive the full endpoint and client base URL from the administrator's current Lumi request instead of showing a placeholder hostname. - 2026-07-18: Fixed the shared submit-action resolver in core 0.2.3: ordinary buttons now inherit their parent form endpoint unless they explicitly declare `formaction`, restoring timed diagnostics-key creation and preventing async update actions from posting back to the Updates page. - 2026-07-18: Added production-stage plugin update diagnostics in core 0.2.2: selected plugin source is verified before snapshotting, failures record their exact stage and target in update state, and the affected plugin row displays the server error directly. diff --git a/docs/lumi-ai-improvement-center.md b/docs/lumi-ai-improvement-center.md new file mode 100644 index 0000000..5a84fc5 --- /dev/null +++ b/docs/lumi-ai-improvement-center.md @@ -0,0 +1,34 @@ +# Lumi AI Improvement Center + +Open **Community > AI Improvement Center** to process feedback about Lumi's +answers. + +## Normal review + +1. Read the user's question, Lumi's answer, and any suggested improvement. +2. Choose **Apply improvement**, **No change needed**, or **Dismiss feedback**. +3. Applied improvements are activated immediately. Every finished record moves + out of the active queue automatically. + +**Apply improvement** asks for the answer or behavior Lumi should use for +similar requests. Audience and activation are the only normal settings. More +technical targets, route scopes, links, and evaluation options remain under +**Advanced options**. + +## Finished feedback + +Choose **Search finished feedback** to open the history dialog. It can search +questions, answers, tags, notes, implementation details, and outcomes. Admins +can restore a record to the active queue, edit its retained details, or +permanently delete it through Lumi's timed confirmation. + +The page subscribes to Lumi's authenticated event stream. New feedback and +review actions from another administrator or trusted reviewer update the queue +without a full-page refresh and reconnect automatically after a temporary +connection loss. + +## Advanced tools + +The collapsed **Advanced tools and access** section contains moderator access, +manual correction-bank maintenance, evaluation cases, and training exports. +These controls are not required for ordinary feedback processing. diff --git a/knowledge/core/lumi-core.md b/knowledge/core/lumi-core.md index 0a4ed36..64ffa32 100644 --- a/knowledge/core/lumi-core.md +++ b/knowledge/core/lumi-core.md @@ -14,7 +14,7 @@ editable: false Lumi is the core web UI and bot runtime. ## Runtime Package: lumi-bot -Version: 0.2.6 +Version: 0.2.7 ## Routes - GET /api/events - POST /api/destructive-confirmations diff --git a/knowledge/plugins/lumi-ai.md b/knowledge/plugins/lumi-ai.md index 52d387d..b9491eb 100644 --- a/knowledge/plugins/lumi-ai.md +++ b/knowledge/plugins/lumi-ai.md @@ -14,7 +14,7 @@ editable: false Managed local AI provider and scoped WebUI assistant for Lumi. ## Metadata Plugin ID: lumi_ai -Version: 0.8.2 +Version: 0.8.3 Default state: enabled ## Web Routes - /plugins/lumi_ai diff --git a/package-lock.json b/package-lock.json index bfa1406..595af37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lumi-bot", - "version": "0.2.6", + "version": "0.2.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumi-bot", - "version": "0.2.6", + "version": "0.2.7", "dependencies": { "adm-zip": "^0.5.12", "better-sqlite3": "^11.5.0", diff --git a/package.json b/package.json index cd2dacc..c717a4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumi-bot", - "version": "0.2.6", + "version": "0.2.7", "private": true, "type": "commonjs", "scripts": { diff --git a/plugins/lumi_ai/CHANGELOG.md b/plugins/lumi_ai/CHANGELOG.md index fa3e344..5bcf93a 100644 --- a/plugins/lumi_ai/CHANGELOG.md +++ b/plugins/lumi_ai/CHANGELOG.md @@ -1,5 +1,13 @@ # Lumi AI changelog +## 0.8.3 + +- Replaced the multi-step approve, promote, and save workflow with a guided one-step “Apply improvement” action that activates reviewed corrections immediately. +- Added direct dismiss and no-change decisions, automatically moving all finalized feedback out of the active queue. +- Added a searchable feedback-history modal with outcome filters, full record inspection, restoration, editing, and timed permanent deletion. +- Added live queue synchronization for new feedback and reviewer actions, including clean reconnect behavior without full-page refreshes. +- Moved correction-bank, evaluation, export, and reviewer-access controls into a clearly labeled advanced area. + ## 0.8.2 - Added OKF-first, role-aware retrieval with compact fast-model answers, conversational subject resolution, deterministic verified links, and main-model fallback when local evidence is insufficient. diff --git a/plugins/lumi_ai/backend/feedback.js b/plugins/lumi_ai/backend/feedback.js index 2aa645c..ac77b55 100644 --- a/plugins/lumi_ai/backend/feedback.js +++ b/plugins/lumi_ai/backend/feedback.js @@ -20,6 +20,7 @@ const FEEDBACK_TAGS = Object.freeze([ ]); const FEEDBACK_KINDS = Object.freeze(["instruction_based", "strict_correction"]); +const FINAL_FEEDBACK_STATUSES = Object.freeze(["implemented", "rejected", "reviewed", "archived"]); class FeedbackStore { constructor(options = {}) { @@ -65,13 +66,27 @@ class FeedbackStore { return entry; } - list({ page = 1, pageSize = 20, status = "", viewerRole = "admin" } = {}) { + list({ page = 1, pageSize = 20, status = "", viewerRole = "admin", scope = "all", query = "" } = {}) { + const search = clean(query, 300).toLowerCase(); const filtered = this.read().entries .filter((entry) => roleRank(viewerRole) >= roleRank(entry.role)) - .filter((entry) => !status || entry.status === status); + .filter((entry) => scope === "active" ? !isFinalized(entry) : scope === "history" ? isFinalized(entry) : true) + .filter((entry) => !status || entry.status === status) + .filter((entry) => !search || searchableFeedback(entry).includes(search)); return paginate(filtered, page, pageSize); } + counts(viewerRole = "admin") { + const visible = this.read().entries.filter((entry) => roleRank(viewerRole) >= roleRank(entry.role)); + const active = visible.filter((entry) => !isFinalized(entry)); + return { + active: active.length, + pending: active.filter((entry) => entry.status === "pending").length, + needs_attention: active.filter((entry) => ["flagged", "verified", "approved"].includes(entry.status)).length, + history: visible.length - active.length + }; + } + all() { return this.read().entries; } @@ -93,7 +108,7 @@ class FeedbackStore { } setStatus(id, status, actor, notes = "") { - if (!["pending", "flagged", "verified", "approved", "rejected", "reviewed", "archived"].includes(status)) { + if (!["pending", "flagged", "verified", "approved", "implemented", "rejected", "reviewed", "archived"].includes(status)) { throw new Error("Invalid review status."); } return this.mutate(id, (entry) => ({ @@ -105,6 +120,31 @@ class FeedbackStore { })); } + markImplemented(id, actor, details = {}) { + return this.mutate(id, (entry) => ({ + ...entry, + status: "implemented", + implementation_target: clean(details.target, 80) || "correction", + implementation_id: clean(details.implementation_id, 200), + implementation_summary: clean(details.summary, 1000), + implemented_by: String(actor.id), + implemented_at: new Date().toISOString(), + reviewed_by: String(actor.id), + reviewed_at: new Date().toISOString() + })); + } + + restoreToQueue(id, actor) { + return this.mutate(id, (entry) => ({ + ...entry, + status: "pending", + restored_by: String(actor.id), + restored_at: new Date().toISOString(), + reviewed_by: String(actor.id), + reviewed_at: new Date().toISOString() + })); + } + verify(id, actor, notes = "") { return this.mutate(id, (entry) => ({ ...entry, @@ -243,11 +283,35 @@ function roleRank(role) { return { user: 1, mod: 2, admin: 3 }[normalizeRole(role)] || 1; } +function isFinalized(entry) { + return FINAL_FEEDBACK_STATUSES.includes(entry?.status); +} + +function searchableFeedback(entry) { + return [ + entry.id, + entry.user_message, + entry.assistant_answer, + entry.optional_correction, + entry.review_notes, + entry.feedback_tag, + entry.feedback_kind, + entry.status, + entry.implementation_target, + entry.implementation_summary, + entry.linked_okf_correction, + entry.platform, + entry.route_used + ].map((value) => String(value || "").toLowerCase()).join("\n"); +} + module.exports = { FEEDBACK_KINDS, FEEDBACK_TAGS, + FINAL_FEEDBACK_STATUSES, FeedbackStore, improvementAccess, + isFinalized, normalizeContextSnapshot, paginate, atomicJson diff --git a/plugins/lumi_ai/backend/training_export.js b/plugins/lumi_ai/backend/training_export.js index 7628fd3..96d95bf 100644 --- a/plugins/lumi_ai/backend/training_export.js +++ b/plugins/lumi_ai/backend/training_export.js @@ -39,7 +39,7 @@ function approvedExamples(feedbackRows, correctionRows) { .map((entry) => [entry.source_feedback_id, entry]) ); return feedbackRows - .filter((entry) => entry.export_approved && entry.status === "approved") + .filter((entry) => entry.export_approved && ["approved", "implemented"].includes(entry.status)) .map((entry) => { const correction = byFeedback.get(entry.id); const preferred = correction?.corrected_answer || entry.optional_correction; diff --git a/plugins/lumi_ai/index.js b/plugins/lumi_ai/index.js index 4ae3a6e..3977428 100644 --- a/plugins/lumi_ai/index.js +++ b/plugins/lumi_ai/index.js @@ -26,7 +26,7 @@ const { AiRateLimiter, mergeLimits } = require("./backend/rate_limits"); const { buildOriginContext, formatPlatformReply, formatPlatformReplyDetails } = require("./backend/commands"); const { AssistantPanelDiagnostics } = require("./backend/assistant_panel_diagnostics"); const { formatAssistantResponse } = require("./backend/response_formatter"); -const { FeedbackStore, FEEDBACK_KINDS, FEEDBACK_TAGS, improvementAccess } = require("./backend/feedback"); +const { FeedbackStore, FEEDBACK_KINDS, FEEDBACK_TAGS, FINAL_FEEDBACK_STATUSES, improvementAccess, isFinalized } = require("./backend/feedback"); const { CorrectionStore, PROMOTION_TARGETS } = require("./backend/corrections"); const { EvalStore } = require("./backend/evals"); const { TrainingExporter } = require("./backend/training_export"); @@ -1053,6 +1053,12 @@ module.exports = { optional_correction: req.body.optional_correction, context_snapshot: req.body.context_snapshot }, req.session.user); + web.emitEvent?.("ai:improvement_changed", { + kind: "review", + review_id: entry.id, + status: entry.status, + message: "New AI feedback is waiting for review." + }, { role: "mod" }); return res.status(201).json({ success: true, id: entry.id }); } catch (error) { return res.status(400).json({ error: error.message }); @@ -1300,9 +1306,21 @@ module.exports = { } }); + const announceImprovementChange = (review, message, kind = "review") => { + web.emitEvent?.("ai:improvement_changed", { + kind, + review_id: review?.id || null, + status: review?.status || null, + message: message || "The AI feedback queue changed." + }, { role: "mod" }); + }; + router.get("/improvement_center", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.allowed) return deniedImprovement(res); + const historyStatus = FINAL_FEEDBACK_STATUSES.includes(cleanText(req.query.history_status, 30)) + ? cleanText(req.query.history_status, 30) + : ""; return res.render(path.join(__dirname, "views", "improvement-center.ejs"), { title: "Lumi AI Improvement Center", config, @@ -1313,9 +1331,20 @@ module.exports = { reviews: feedbackStore.list({ page: req.query.review_page, pageSize: 15, - status: cleanText(req.query.status, 30), + scope: "active", viewerRole: access.role }), + historyReviews: feedbackStore.list({ + page: req.query.history_page, + pageSize: 15, + scope: "history", + query: req.query.history_q, + status: historyStatus, + viewerRole: access.role + }), + reviewCounts: feedbackStore.counts(access.role), + historyQuery: cleanText(req.query.history_q, 300), + historyStatus, corrections: correctionStore.list({ page: req.query.correction_page, pageSize: 15 }), evalCases: evalStore.list({ page: req.query.eval_page, pageSize: 15 }), evalResults: evalStore.results(25), @@ -1336,6 +1365,7 @@ module.exports = { } }); ensureSidebarNavItem(settings); + announceImprovementChange(null, "Improvement Center settings changed.", "settings"); return improvementFlash(req, res, "success", "Improvement Center settings saved."); }); @@ -1346,24 +1376,38 @@ module.exports = { const review = feedbackStore.get(req.params.id); if (!canReviewAiFeedback(review, access)) return deniedImprovement(res); const action = cleanText(req.body.action, 30); + let updated = null; if (action === "flag" && access.can_flag) { - feedbackStore.setStatus(req.params.id, "flagged", req.session.user, req.body.review_notes); + updated = feedbackStore.setStatus(req.params.id, "flagged", req.session.user, req.body.review_notes); } else if (action === "verify" && access.can_verify) { - feedbackStore.verify(req.params.id, req.session.user, req.body.review_notes); + updated = feedbackStore.verify(req.params.id, req.session.user, req.body.review_notes); } else if (action === "approve" && access.can_approve) { - feedbackStore.setStatus(req.params.id, "approved", req.session.user, req.body.review_notes); + updated = feedbackStore.setStatus(req.params.id, "approved", req.session.user, req.body.review_notes); } else if (action === "reject" && access.can_approve) { - feedbackStore.setStatus(req.params.id, "rejected", req.session.user, req.body.review_notes); + updated = feedbackStore.setStatus(req.params.id, "rejected", req.session.user, req.body.review_notes); } else if (action === "reviewed" && access.can_approve) { - feedbackStore.setStatus(req.params.id, "reviewed", req.session.user, req.body.review_notes); + updated = feedbackStore.setStatus(req.params.id, "reviewed", req.session.user, req.body.review_notes); + } else if (action === "restore" && access.can_approve) { + updated = feedbackStore.restoreToQueue(req.params.id, req.session.user); } else if (action === "edit" && access.can_edit) { - feedbackStore.edit(req.params.id, req.body, req.session.user); + updated = feedbackStore.edit(req.params.id, req.body, req.session.user); } else if (action === "export" && access.can_export) { - feedbackStore.markExportApproved(req.params.id, req.session.user); + updated = feedbackStore.markExportApproved(req.params.id, req.session.user); } else { return deniedImprovement(res); } - return improvementFlash(req, res, "success", `Review ${action} completed.`); + const messages = { + flag: "Feedback marked for closer review.", + verify: "Feedback reviewed and sent to an administrator.", + approve: "Feedback approved and ready to apply.", + reject: "Feedback dismissed and moved to history.", + reviewed: "Feedback marked as requiring no change and moved to history.", + restore: "Feedback restored to the active queue.", + edit: "Feedback details saved.", + export: "Feedback approved for training export." + }; + announceImprovementChange(updated, messages[action]); + return improvementFlash(req, res, "success", messages[action] || "Feedback updated.", { review: updated }); } catch (error) { return improvementFlash(req, res, "error", error.message); } @@ -1373,8 +1417,10 @@ module.exports = { const access = improvementAccess(req.session.user, config); if (!access.can_delete) return deniedImprovement(res); try { + const deleted = feedbackStore.get(req.params.id); feedbackStore.delete(req.params.id); - return improvementFlash(req, res, "success", "Review delete completed."); + announceImprovementChange(deleted, "Feedback record permanently deleted."); + return improvementFlash(req, res, "success", "Feedback record permanently deleted."); } catch (error) { return improvementFlash(req, res, "error", error.message); } @@ -1384,8 +1430,9 @@ module.exports = { const access = improvementAccess(req.session.user, config); if (!access.can_delete) return deniedImprovement(res); try { - feedbackStore.setStatus(req.params.id, "archived", req.session.user, req.body.review_notes); - return improvementFlash(req, res, "success", "Feedback record archived."); + const updated = feedbackStore.setStatus(req.params.id, "archived", req.session.user, req.body.review_notes); + announceImprovementChange(updated, "Feedback archived and moved to history."); + return improvementFlash(req, res, "success", "Feedback archived and moved to history.", { review: updated }); } catch (error) { return improvementFlash(req, res, "error", error.message); } @@ -1395,11 +1442,16 @@ module.exports = { const access = improvementAccess(req.session.user, config); if (!access.can_implement) return deniedImprovement(res); try { - const review = feedbackStore.get(req.params.id); - if (!review || review.status !== "approved") throw new Error("Approve the review before implementing it."); + let review = feedbackStore.get(req.params.id); + if (!review || isFinalized(review)) throw new Error("This feedback is already finalized. Restore it from history before applying it again."); + if (review.status !== "approved") { + review = feedbackStore.setStatus(review.id, "approved", req.session.user, req.body.review_notes); + } const target = PROMOTION_TARGETS.includes(req.body.target) ? req.body.target : "correction"; + let implementationId = ""; + let implementationSummary = ""; if (target === "eval_case") { - evalStore.add({ + const evalCase = evalStore.add({ prompt: review.user_message, role: req.body.min_role || review.role, origin: req.body.permission_origin || review.origin, @@ -1408,8 +1460,11 @@ module.exports = { expected_link: req.body.expected_link, notes: req.body.notes }, req.session.user); + implementationId = evalCase.id; + implementationSummary = "Added as an evaluation case."; } else if (target === "training_export") { feedbackStore.markExportApproved(review.id, req.session.user); + implementationSummary = "Approved for the next manual training export."; } else { const correction = correctionStore.createFromFeedback(review, { ...req.body, @@ -1427,8 +1482,19 @@ module.exports = { throw error; } } + correctionStore.saveCorrections(req.session.user); + implementationId = correction.id; + implementationSummary = target === "correction" + ? "Created a searchable OKF correction and activated it for Lumi AI." + : `Created and activated a ${target.replaceAll("_", " ")}.`; } - return improvementFlash(req, res, "success", "Approved feedback was promoted. Save Corrections before it becomes active."); + const updated = feedbackStore.markImplemented(review.id, req.session.user, { + target, + implementation_id: implementationId, + summary: implementationSummary + }); + announceImprovementChange(updated, "Improvement applied and moved to history."); + return improvementFlash(req, res, "success", "Improvement applied and moved to history.", { review: updated }); } catch (error) { return improvementFlash(req, res, "error", error.message); } @@ -1438,6 +1504,7 @@ module.exports = { const access = improvementAccess(req.session.user, config); if (!access.can_implement) return deniedImprovement(res); const result = correctionStore.saveCorrections(req.session.user); + announceImprovementChange(null, "AI corrections were activated.", "correction"); return improvementFlash(req, res, "success", `Corrections saved. ${result.active} of ${result.total} are active.`); }); @@ -1462,6 +1529,7 @@ module.exports = { } else { return deniedImprovement(res); } + announceImprovementChange(null, `Correction ${action} completed.`, "correction"); return improvementFlash(req, res, "success", `Correction ${action} completed. Save Corrections to activate changes.`); } catch (error) { return improvementFlash(req, res, "error", error.message); @@ -1473,6 +1541,7 @@ module.exports = { if (!access.can_delete) return deniedImprovement(res); try { correctionStore.delete(req.params.id); + announceImprovementChange(null, "Correction deleted.", "correction"); return improvementFlash(req, res, "success", "Correction delete completed. Save Corrections to activate changes."); } catch (error) { return improvementFlash(req, res, "error", error.message); @@ -1487,6 +1556,7 @@ module.exports = { throw new Error("Expected links must match a verified Lumi route."); } evalStore.add(req.body, req.session.user); + announceImprovementChange(null, "Evaluation case added.", "eval"); return improvementFlash(req, res, "success", "Eval case added."); } catch (error) { return improvementFlash(req, res, "error", error.message); @@ -1497,6 +1567,7 @@ module.exports = { const access = improvementAccess(req.session.user, config); if (!access.can_run_evals) return deniedImprovement(res); evalStore.delete(req.params.id); + announceImprovementChange(null, "Evaluation case deleted.", "eval"); return improvementFlash(req, res, "success", "Eval case deleted."); }); @@ -1505,6 +1576,7 @@ module.exports = { if (!access.can_run_evals) return deniedImprovement(res); try { const results = await evalStore.runAll({ provider, actor: req.session.user }); + announceImprovementChange(null, "Evaluation run completed.", "eval"); return improvementFlash(req, res, "success", `Eval run completed with ${results.length} result(s).`); } catch (error) { return improvementFlash(req, res, "error", error.message); @@ -1697,7 +1769,14 @@ function toolDenied(req, res) { if (req.accepts(["json", "html"]) === "json") return res.status(403).json({ error: "Access denied." }); return denied(res); } -function improvementFlash(req, res, type, message) { +function improvementFlash(req, res, type, message, result = {}) { + if (req.accepts(["json", "html"]) === "json") { + return res.status(type === "error" ? 400 : 200).json({ + success: type !== "error", + message, + result + }); + } req.session.flash = { type, message }; return res.redirect(`/plugins/${PLUGIN_ID}/improvement_center`); } diff --git a/plugins/lumi_ai/plugin.json b/plugins/lumi_ai/plugin.json index d830498..d9086fc 100644 --- a/plugins/lumi_ai/plugin.json +++ b/plugins/lumi_ai/plugin.json @@ -1,11 +1,11 @@ { "id": "lumi_ai", "name": "Lumi AI", - "version": "0.8.2", + "version": "0.8.3", "description": "Managed local AI provider and scoped WebUI assistant for Lumi.", "main": "index.js", "channel": "stable", - "compatible_from": "0.8.1", - "migration_notes": "Existing models, runtimes, settings, feedback, metrics, and tool data are retained. No manual migration is required.", + "compatible_from": "0.8.2", + "migration_notes": "Existing models, runtimes, settings, feedback, corrections, metrics, and tool data are retained. Existing finalized feedback appears in searchable history; no manual migration is required.", "rollback_safe": true } diff --git a/plugins/lumi_ai/public/improvement-center.css b/plugins/lumi_ai/public/improvement-center.css index 9a3240c..a1de87e 100644 --- a/plugins/lumi_ai/public/improvement-center.css +++ b/plugins/lumi_ai/public/improvement-center.css @@ -1,21 +1,66 @@ .improvement-titlebar { align-items: center; } -.improvement-filters, .improvement-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; } -.improvement-actions form { margin: 0; } -.improvement-list { display: grid; gap: 12px; } -.improvement-card { padding: 13px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); } -.improvement-card > header { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; margin-bottom: 10px; color: var(--ink-soft); font-size: 12px; } -.improvement-card > header strong { color: var(--ink); font-size: 14px; } +.improvement-live-status { position: sticky; top: 10px; z-index: 15; margin: 0 0 12px; padding: 10px 13px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); box-shadow: 0 8px 24px rgba(0,0,0,.18); } +.improvement-live-status[hidden] { display: none; } +.improvement-live-status[data-tone="success"] { border-color: var(--sea); } +.improvement-live-status[data-tone="error"] { border-color: var(--danger); } +.improvement-guide { background: linear-gradient(135deg, color-mix(in srgb, var(--sea) 9%, var(--card)), var(--card)); } +.improvement-steps { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 12px; margin: 16px 0; padding: 0; list-style: none; } +.improvement-steps li { display: flex; gap: 10px; min-width: 0; padding: 12px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface-2); } +.improvement-steps li > span { display: grid; place-items: center; flex: 0 0 28px; width: 28px; height: 28px; border-radius: 50%; background: var(--sea); color: var(--button-text, #081317); font-weight: 800; } +.improvement-steps strong, .improvement-steps small { display: block; } +.improvement-steps small { margin-top: 4px; color: var(--ink-soft); line-height: 1.4; } +.improvement-counts { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 10px; } +.improvement-counts > div { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); } +.improvement-counts span { color: var(--ink-soft); } +.improvement-counts strong { font-size: 1.35rem; } +.improvement-filters, .improvement-actions, .improvement-decision-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } +.improvement-actions form, .improvement-decision-row form { margin: 0; } +.improvement-decision-row { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); } +.improvement-decision-row > .button:first-child { min-width: 150px; } +.improvement-list { display: grid; gap: 14px; } +.improvement-card { min-width: 0; padding: 15px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); box-shadow: 0 4px 14px rgba(0,0,0,.08); } +.improvement-card > header { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; margin-bottom: 12px; color: var(--ink-soft); font-size: 12px; } +.improvement-card > header strong { color: var(--ink); font-size: 15px; } +.improvement-card-labels { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; } .improvement-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; } -.improvement-pair > div, .improvement-correction { min-width: 0; padding: 9px; border-radius: 6px; background: var(--surface-2); } -.improvement-pair span { display: block; margin-bottom: 5px; color: var(--ink-soft); font-size: 11px; font-weight: 700; text-transform: uppercase; } -.improvement-card pre, .table pre { max-height: 240px; margin: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); font: inherit; } +.improvement-pair > div, .improvement-correction, .improvement-review-note { min-width: 0; padding: 10px; border-radius: 7px; background: var(--surface-2); } +.improvement-pair span { display: block; margin-bottom: 5px; color: var(--ink-soft); font-size: 11px; font-weight: 700; letter-spacing: .025em; text-transform: uppercase; } +.improvement-card pre, .table pre { max-height: 260px; margin: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); font: inherit; } .improvement-correction { margin-bottom: 10px; border-left: 3px solid var(--sea); } -.improvement-correction strong { display: block; margin-bottom: 5px; } -.improvement-dialog { width: min(760px, calc(100vw - 28px)); max-height: calc(100vh - 28px); padding: 18px; overflow: auto; border: 1px solid var(--border); border-radius: 9px; background: var(--card); color: var(--ink); box-shadow: 0 20px 60px rgba(0, 0, 0, .35); } -.improvement-dialog::backdrop { background: rgba(0, 0, 0, .55); } +.improvement-correction strong, .improvement-review-note strong { display: block; margin-bottom: 5px; } +.improvement-review-note { margin-bottom: 10px; border-left: 3px solid var(--warning, #d1a43a); } +.improvement-review-note p { margin: 0; } +.improvement-card .button, .improvement-actions .button { min-height: 38px; white-space: nowrap; } +.improvement-empty { padding: 28px 18px; border: 1px dashed var(--border); border-radius: 10px; text-align: center; color: var(--ink-soft); } +.improvement-empty strong { display: block; color: var(--ink); font-size: 1.1rem; } +.improvement-dialog { width: min(780px, calc(100vw - 28px)); max-height: calc(100vh - 28px); padding: 18px; overflow: auto; border: 1px solid var(--border); border-radius: 10px; background: var(--card); color: var(--ink); box-shadow: 0 20px 60px rgba(0,0,0,.4); } +.improvement-history-dialog { width: min(1100px, calc(100vw - 28px)); } +.improvement-dialog::backdrop { background: rgba(0,0,0,.62); } .improvement-dialog .ai-form { margin: 0; } -.improvement-card .button, .improvement-actions .button { white-space: nowrap; } +.improvement-dialog-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 16px; } +.improvement-dialog-heading h2, .improvement-dialog-heading h3, .improvement-dialog-heading p { margin-top: 0; } +.improvement-dialog-heading p { margin-bottom: 0; color: var(--ink-soft); } +.improvement-advanced-fields { margin-top: 12px; } +.improvement-history-search { display: grid; grid-template-columns: minmax(220px,1fr) minmax(180px,auto) auto auto; align-items: end; gap: 10px; margin-bottom: 15px; } +.improvement-history-list { display: grid; gap: 10px; } +.history-card > details > summary { cursor: pointer; color: var(--ink); font-weight: 650; } +.history-card > details[open] > summary { margin-bottom: 12px; } +.improvement-advanced { padding: 0; overflow: clip; } +.improvement-advanced > summary { cursor: pointer; padding: 16px; list-style-position: inside; } +.improvement-advanced > summary span { display: inline-flex; flex-direction: column; gap: 3px; margin-left: 5px; } +.improvement-advanced > summary small { color: var(--ink-soft); font-weight: 400; } +.improvement-advanced-body { display: grid; gap: 24px; padding: 0 16px 18px; } +.improvement-advanced-body > section { padding-top: 20px; border-top: 1px solid var(--border); } +.ai-tag.status-implemented { border-color: var(--sea); } +.ai-tag.status-rejected, .ai-tag.status-archived { opacity: .82; } +@media (max-width: 850px) { + .improvement-steps, .improvement-counts { grid-template-columns: 1fr; } + .improvement-history-search { grid-template-columns: 1fr 1fr; } +} @media (max-width: 760px) { .improvement-pair { grid-template-columns: 1fr; } .improvement-titlebar { align-items: flex-start; } + .improvement-history-search { grid-template-columns: 1fr; } + .improvement-dialog { width: calc(100vw - 16px); max-height: calc(100vh - 16px); padding: 14px; } + .improvement-decision-row > *, .improvement-decision-row form, .improvement-decision-row .button { width: 100%; } } diff --git a/plugins/lumi_ai/public/improvement-center.js b/plugins/lumi_ai/public/improvement-center.js index cc1efaf..c985da8 100644 --- a/plugins/lumi_ai/public/improvement-center.js +++ b/plugins/lumi_ai/public/improvement-center.js @@ -1,12 +1,167 @@ (() => { + let stream = null; + let refreshTimer = null; + let refreshing = false; + let queuedRefresh = false; + + const status = () => document.querySelector("[data-improvement-status]"); + const workspace = () => document.querySelector("[data-improvement-workspace]"); + const historyDialog = () => document.getElementById("improvement-history"); + + function showStatus(message, tone = "info") { + const target = status(); + if (!target) return; + target.textContent = message || ""; + target.dataset.tone = tone; + target.hidden = !message; + if (message && tone !== "error") window.setTimeout(() => { + if (target.textContent === message) target.hidden = true; + }, 5000); + } + + function historyParams() { + const dialog = historyDialog(); + const form = dialog?.querySelector("[data-improvement-history-search]"); + const params = new URLSearchParams(); + const query = form?.elements?.history_q?.value?.trim(); + const outcome = form?.elements?.history_status?.value; + if (query) params.set("history_q", query); + if (outcome) params.set("history_status", outcome); + return params; + } + + async function refreshWorkspace(url = null, options = {}) { + if (refreshing) { + queuedRefresh = true; + return; + } + refreshing = true; + const wasHistoryOpen = options.openHistory ?? Boolean(historyDialog()?.open); + const activeElementId = document.activeElement?.id || ""; + try { + const params = url ? new URL(url, window.location.origin).searchParams : historyParams(); + const response = await fetch(`/plugins/lumi_ai/improvement_center?${params.toString()}`, { + headers: { Accept: "text/html", "X-Lumi-Live-Refresh": "1" }, + cache: "no-store" + }); + if (!response.ok) throw new Error("The latest feedback queue could not be loaded."); + const html = await response.text(); + const documentCopy = new DOMParser().parseFromString(html, "text/html"); + const next = documentCopy.querySelector("[data-improvement-workspace]"); + const current = workspace(); + if (!next || !current) throw new Error("The refreshed feedback view was incomplete."); + current.replaceWith(next); + window.LumiInteractions?.init?.(next); + if (wasHistoryOpen) historyDialog()?.showModal(); + if (activeElementId) document.getElementById(activeElementId)?.focus?.(); + } catch (error) { + showStatus(error.message || "The feedback queue could not be refreshed.", "error"); + } finally { + refreshing = false; + if (queuedRefresh) { + queuedRefresh = false; + refreshWorkspace(null, { openHistory: wasHistoryOpen }); + } + } + } + + function scheduleRefresh(message) { + if (message) showStatus(message); + window.clearTimeout(refreshTimer); + refreshTimer = window.setTimeout(() => refreshWorkspace(), 120); + } + document.addEventListener("click", (event) => { const opener = event.target.closest("[data-open-dialog]"); if (opener) { document.getElementById(opener.dataset.openDialog)?.showModal(); return; } + if (event.target.closest("[data-open-history]")) { + historyDialog()?.showModal(); + historyDialog()?.querySelector("input[type='search']")?.focus(); + return; + } const closer = event.target.closest("[data-close-dialog]"); - if (closer) closer.closest("dialog")?.close(); + if (closer) { + closer.closest("dialog")?.close(); + return; + } + if (event.target.closest("[data-history-clear]")) { + const form = historyDialog()?.querySelector("[data-improvement-history-search]"); + form?.reset(); + refreshWorkspace("/plugins/lumi_ai/improvement_center", { openHistory: true }); + return; + } + const pageLink = event.target.closest("[data-improvement-page], [data-improvement-history-page]"); + if (pageLink && !pageLink.classList.contains("disabled")) { + event.preventDefault(); + refreshWorkspace(pageLink.href, { openHistory: pageLink.matches("[data-improvement-history-page]") }); + } }); + document.addEventListener("submit", async (event) => { + const form = event.target; + if (!(form instanceof HTMLFormElement)) return; + if (form.matches("[data-improvement-history-search]")) { + event.preventDefault(); + const params = new URLSearchParams(new FormData(form)); + params.delete("history_page"); + return refreshWorkspace(`/plugins/lumi_ai/improvement_center?${params.toString()}`, { openHistory: true }); + } + if (!form.matches("[data-improvement-action]")) return; + // The shared timed-confirmation handler runs in the capture phase. Let it + // issue a token before AJAX handles permanent deletes. + if (event.defaultPrevented && !form.elements.confirmation_token?.value) return; + event.preventDefault(); + const submitter = event.submitter || form.querySelector("button[type='submit']"); + const originalLabel = submitter?.textContent || ""; + if (submitter) { + submitter.disabled = true; + submitter.textContent = "Saving…"; + } + try { + const body = new URLSearchParams(); + for (const [key, value] of new FormData(form).entries()) { + if (typeof value === "string") body.append(key, value); + } + const response = await fetch(form.action, { + method: form.method || "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" + }, + body + }); + const result = await response.json().catch(() => ({})); + if (!response.ok || result.success === false) throw new Error(result.message || result.error || "The feedback could not be updated."); + form.closest("dialog")?.close(); + showStatus(result.message || "Feedback updated.", "success"); + await refreshWorkspace(null, { openHistory: Boolean(historyDialog()?.open) }); + } catch (error) { + showStatus(error.message || "The feedback could not be updated.", "error"); + if (submitter) { + submitter.disabled = false; + submitter.textContent = originalLabel; + } + } + }); + + function connectLiveUpdates() { + if (!window.EventSource || stream) return; + stream = new EventSource("/api/events"); + stream.addEventListener("ai:improvement_changed", (event) => { + try { + const data = JSON.parse(event.data || "{}"); + scheduleRefresh(data.message || "The feedback queue was updated by another reviewer."); + } catch { + scheduleRefresh("The feedback queue was updated by another reviewer."); + } + }); + stream.onerror = () => { + showStatus("Live updates are reconnecting. Your saved actions are still safe.", "warning"); + }; + } + + connectLiveUpdates(); })(); diff --git a/plugins/lumi_ai/tests/verify.js b/plugins/lumi_ai/tests/verify.js index 65272a3..0d6e20d 100644 --- a/plugins/lumi_ai/tests/verify.js +++ b/plugins/lumi_ai/tests/verify.js @@ -46,7 +46,7 @@ const { AiRateLimiter, mergeLimits } = require("../backend/rate_limits"); const { PLATFORM_DEFAULTS, buildOriginContext, formatPlatformReply, formatPlatformReplyDetails } = require("../backend/commands"); const { AssistantPanelDiagnostics } = require("../backend/assistant_panel_diagnostics"); const { formatAssistantResponse, normalizeLink, normalizeCodeFences } = require("../backend/response_formatter"); -const { FeedbackStore, FEEDBACK_TAGS, improvementAccess } = require("../backend/feedback"); +const { FeedbackStore, FEEDBACK_TAGS, FINAL_FEEDBACK_STATUSES, improvementAccess, isFinalized } = require("../backend/feedback"); const { CorrectionStore } = require("../backend/corrections"); const { EvalStore, evaluateCase } = require("../backend/evals"); const { TrainingExporter, approvedExamples } = require("../backend/training_export"); @@ -234,12 +234,16 @@ async function run() { assert(settingsTemplate.includes("Export for Codex")); assert(settingsTemplate.includes("raw diagnostics, and tool payloads removed")); const improvementTemplate = fs.readFileSync(path.join(PLUGIN_ROOT, "views", "improvement-center.ejs"), "utf8"); - for (const control of ["Review queue", "Save Corrections", "Run all evals", "Export instruction JSONL", "Export DPO JSONL"]) { + for (const control of ["Feedback waiting for review", "Apply improvement", "No change needed", "Dismiss feedback", "Finished feedback history", "Activate staged changes", "Run all tests", "Export instruction data"]) { assert(improvementTemplate.includes(control)); } assert(improvementTemplate.includes('data-confirm-title="Delete eval case"')); - assert(improvementTemplate.includes("Sources used for this answer")); - assert(improvementTemplate.includes("Searchable OKF title")); + assert(improvementTemplate.includes("What Lumi used to answer")); + assert(improvementTemplate.includes("Searchable knowledge title")); + assert(improvementTemplate.includes("data-improvement-workspace")); + const improvementFrontend = fs.readFileSync(path.join(PLUGIN_ROOT, "public", "improvement-center.js"), "utf8"); + assert(improvementFrontend.includes('ai:improvement_changed')); + assert(improvementFrontend.includes("data-improvement-history-search")); const assistantFeedbackScript = fs.readFileSync(path.join(PLUGIN_ROOT, "public", "assistant.js"), "utf8"); for (const tag of FEEDBACK_TAGS) assert(assistantFeedbackScript.includes(`"${tag}"`)); assert(assistantFeedbackScript.includes("/assistant/feedback")); @@ -357,6 +361,17 @@ async function run() { platform: "webui" }).length, 0); correctionConfig.improvement.corrections_enabled = true; + const implemented = feedbackStore.markImplemented(review.id, { id: "admin" }, { + target: "predefined_answer", + implementation_id: correction.id, + summary: "Activated a verified settings answer." + }); + assert.equal(implemented.status, "implemented"); + assert.equal(isFinalized(implemented), true); + assert(FINAL_FEEDBACK_STATUSES.includes("implemented")); + assert.equal(feedbackStore.list({ scope: "active", viewerRole: "admin" }).total, 0); + assert.equal(feedbackStore.list({ scope: "history", query: "verified settings", viewerRole: "admin" }).total, 1); + assert.deepEqual(feedbackStore.counts("admin"), { active: 0, pending: 0, needs_attention: 0, history: 1 }); const evalStore = new EvalStore({ casesFile: path.join(improvementTemp, "eval-cases.json"), diff --git a/plugins/lumi_ai/views/improvement-center.ejs b/plugins/lumi_ai/views/improvement-center.ejs index 210073c..c9ce41d 100644 --- a/plugins/lumi_ai/views/improvement-center.ejs +++ b/plugins/lumi_ai/views/improvement-center.ejs @@ -1,247 +1,192 @@ <%- include("../../../src/web/views/partials/layout-top", { title }) %> - - + + +<% const statusLabel = (status) => ({ pending: "New", flagged: "Needs attention", verified: "Reviewed by moderator", approved: "Ready to apply", implemented: "Applied", rejected: "Dismissed", reviewed: "No change needed", archived: "Archived" }[status] || status); %>
-

Lumi AI Improvement Center

-

Review assistant feedback, stage approved corrections, run evals, and create manual training exports.

+

Improve Lumi AI

+

Review feedback and either apply an improvement or close it. Finished items move to searchable history automatically.

<%= access.role %><%= access.trusted ? " · trusted reviewer" : "" %> - <% if (access.can_approve) { %>Lumi AI settings<% } %> + <% if (access.can_approve) { %>AI settings<% } %>
- +
-<% if (access.can_approve) { %> -
-

Access and activation

Corrections remain inactive until an administrator selects Save Corrections.

-
-
-
-
-
-
-
-<% } %> - -
-
-

Review queue

Review ratings, the answer, and a privacy-limited snapshot of the knowledge sources that informed it. Only admins can turn approved feedback into searchable OKF corrections.

-
- <% ["", "pending", "flagged", "verified", "approved", "reviewed", "rejected", "archived"].forEach((status) => { %> - <%= status || "All" %> - <% }) %> +
+
+
+

A simpler review process

The usual path takes one decision and no manual activation step.

+
-
-
- <% reviews.entries.forEach((review) => { %> -
-
-
<%= review.rating === "up" ? "Helpful" : "Needs work" %> <%= review.feedback_tag.replaceAll("_", " ") %> <%= review.feedback_kind || "strict_correction" %> <%= review.status %>
- <%= formatDate(review.timestamp) %> · <%= review.role %> · <%= review.platform %> · <%= review.route_used || "unknown route" %> -
-
-
User message
<%= review.user_message %>
-
Assistant answer
<%= review.assistant_answer %>
-
- <% if (review.optional_correction) { %>
<%= review.feedback_kind === "instruction_based" ? "Instruction guidance" : "Suggested correction" %>
<%= review.optional_correction %>
<% } %> - <% const snapshot = review.context_snapshot || {}; const retrieval = snapshot.retrieval || {}; const snapshotRows = [...(snapshot.okf || []), ...(snapshot.corrections || []), ...(snapshot.repository || [])]; %> - <% if (snapshotRows.length || retrieval.query || (snapshot.tools || []).length) { %> -
- Sources used for this answer<%= retrieval.selected_count || snapshotRows.length %> selected · <%= retrieval.candidate_count || 0 %> considered -
- <% if (retrieval.query) { %>

Lookup: <%= retrieval.query %> · depth <%= retrieval.depth || "unknown" %>

<% } %> - <% snapshotRows.forEach((source) => { %>
<%= source %>
<% }) %> - <% if ((snapshot.tools || []).length) { %>

Available/selected tools: <%= snapshot.tools.join(", ") %>

<% } %> +
    +
  1. 1
    Read the reportCompare the question, Lumi's answer, and the user's suggestion.
  2. +
  3. 2
    Choose an outcomeApply the improvement, dismiss it, or mark that no change is needed.
  4. +
  5. 3
    Done automaticallyApplied corrections become active immediately. Finished reports move to history.
  6. +
+
+
Active queue<%= reviewCounts.active %>
+
New<%= reviewCounts.pending %>
+
Needs attention<%= reviewCounts.needs_attention %>
+
+
+ +
+
+

Feedback waiting for review

Finished feedback is hidden from this queue and remains available in history.

+ +
+
+ <% reviews.entries.forEach((review) => { const snapshot = review.context_snapshot || {}; const retrieval = snapshot.retrieval || {}; const snapshotRows = [...(snapshot.okf || []), ...(snapshot.corrections || []), ...(snapshot.repository || [])]; %> +
+
+
<%= review.rating === "up" ? "Helpful response" : "Response needs work" %><%= statusLabel(review.status) %><%= review.feedback_tag.replaceAll("_", " ") %>
+ <%= formatDate(review.timestamp) %> · <%= review.role %> · <%= review.platform %> +
+
+
User asked
<%= review.user_message %>
+
Lumi answered
<%= review.assistant_answer %>
- - <% } %> - <% if (review.linked_okf_correction) { %>

Searchable correction: <%= review.linked_okf_correction %>

<% } %> - <% if (review.review_notes) { %>

Review notes: <%= review.review_notes %>

<% } %> -
- <% if (access.can_flag) { %> -
- - -
+ <% if (review.optional_correction) { %>
User's suggested improvement
<%= review.optional_correction %>
<% } %> + <% if (review.review_notes) { %>
Reviewer note

<%= review.review_notes %>

<% } %> + <% if (snapshotRows.length || retrieval.query || (snapshot.tools || []).length) { %> +
+ What Lumi used to answer<%= retrieval.selected_count || snapshotRows.length %> source(s) selected +
+ <% if (retrieval.query) { %>

Knowledge lookup: <%= retrieval.query %>

<% } %> + <% snapshotRows.forEach((source) => { %>
<%= source %>
<% }) %> + <% if ((snapshot.tools || []).length) { %>

Tools available: <%= snapshot.tools.join(", ") %>

<% } %> +
+
<% } %> - <% if (access.can_verify && !["approved", "rejected"].includes(review.status)) { %> -
- - -
- <% } %> - <% if (access.can_approve) { %> -
- - -
-
- - -
-
- - -
- - <% if (review.status === "approved") { %><% } %> -
- - -
-
- -
-
- -
- <% } %> -
-
- - <% if (access.can_edit) { %> - -
- -
-
-
-
-
-
-
- <% } %> - - <% if (access.can_implement && review.status === "approved") { %> - -
-
-
-
-
When the target is Correction, Lumi saves an editable, searchable OKF file after approval.
-
" />
-
" />
-
-
-
-
-
-
-
-
-
- <% } %> - <% }) %> - <% if (!reviews.entries.length) { %>
No feedback matches this filter.
<% } %> -
-
- " href="?review_page=<%= Math.max(1, reviews.page - 1) %>#reviews">Previous - Page <%= reviews.page %> of <%= reviews.pages %> (<%= reviews.total %> reviews) - " href="?review_page=<%= Math.min(reviews.pages, reviews.page + 1) %>#reviews">Next -
-
- -
-
-

Correction bank

Edits and toggles are staged. Save Corrections is required before they become active.

- <% if (access.can_implement) { %>
<% } %> -
-
- - - - <% corrections.entries.forEach((entry) => { %> - - - - - - - - <% if (access.can_edit) { %> - -
- -
-
-
-
-
-
-
-
-
+
+ <% if (access.can_implement) { %> + + + + -
- <% } %> - <% }) %> - <% if (!corrections.entries.length) { %><% } %> - -
TargetPrompt / answerPermissionStateActions
<%= entry.target.replaceAll("_", " ") %>
<%= entry.prompt.slice(0, 100) %>
<%= entry.corrected_answer %>
<%= entry.min_role %> · <%= entry.permission_scope.origin %>/<%= entry.permission_scope.platform %><%= entry.active ? "active" : entry.enabled ? "staged" : "disabled" %> - <% if (access.can_verify) { %>
<% } %> - <% if (entry.linked_okf_path) { %>Open OKF<% } %> - <% if (access.can_edit) { %>
" />
<% } %> -
No corrections have been promoted.
-
-
- " href="?correction_page=<%= Math.max(1, corrections.page - 1) %>#corrections">Previous - Page <%= corrections.page %> of <%= corrections.pages %> (<%= corrections.total %> corrections) - " href="?correction_page=<%= Math.min(corrections.pages, corrections.page + 1) %>#corrections">Next -
-
+
+ + +
+ + <% } else { %> + <% if (access.can_verify) { %>
<% } %> + <% if (access.can_flag) { %>
<% } %> + <% } %> + + -
-
-

Evals

Stored cases can be run manually against the current Lumi AI configuration.

- <% if (access.can_run_evals) { %>
<% } %> -
- <% if (access.can_run_evals) { %> -
Add eval case -
-
-
-
-
-
-
-
-
+ <% if (access.can_edit) { %> + +

Edit feedback details

Clarify the report without changing its outcome.

+ + +
+
+
+
+
+ +
+ <% } %> + + <% if (access.can_implement) { %> + +

Apply this improvement

Tell Lumi what it should do instead. Saving here activates the change and finishes the feedback.

+
+
Use clear, user-facing wording. This becomes the reviewed guidance for similar requests.
+
+
+
+ Advanced options +
+
+
+
" />
+
" />
+
+
+
+
+
+
+
+
+
+
+ <% } %> + <% }) %> + <% if (!reviews.entries.length) { %>
The queue is clear.

New AI feedback will appear here automatically.

<% } %> + + <% if (reviews.pages > 1) { %><% } %> +
+ + +

Finished feedback history

Search, inspect, restore, edit, or permanently delete earlier decisions.

+ +
+ <% historyReviews.entries.forEach((review) => { %> +
+
<%= statusLabel(review.status) %><%= statusLabel(review.status) %><%= review.feedback_tag.replaceAll("_", " ") %>
<%= formatDate(review.reviewed_at || review.timestamp) %>
+
+ <%= review.user_message.slice(0, 180) %><%= review.user_message.length > 180 ? "…" : "" %> +
User asked
<%= review.user_message %>
Lumi answered
<%= review.assistant_answer %>
+ <% if (review.optional_correction) { %>
Suggested improvement
<%= review.optional_correction %>
<% } %> + <% if (review.implementation_summary) { %>
What was applied

<%= review.implementation_summary %>

<% } %> + <% if (review.review_notes) { %>

Reviewer note: <%= review.review_notes %>

<% } %> + <% if (review.linked_okf_correction) { %>

Open searchable knowledge entry

<% } %> +
+ <% if (access.can_edit) { %>
<% } %> +
+ <% if (access.can_edit) { %>

Edit finished feedback

" />
<% } %> + <% }) %> + <% if (!historyReviews.entries.length) { %>
No finished feedback found.

Try a different search or status.

<% } %> +
+ <% if (historyReviews.pages > 1) { %>
" href="?history_q=<%= encodeURIComponent(historyQuery) %>&history_status=<%= historyStatus %>&history_page=<%= Math.max(1, historyReviews.page - 1) %>" data-improvement-history-page>PreviousPage <%= historyReviews.page %> of <%= historyReviews.pages %> (<%= historyReviews.total %> records)" href="?history_q=<%= encodeURIComponent(historyQuery) %>&history_status=<%= historyStatus %>&history_page=<%= Math.min(historyReviews.pages, historyReviews.page + 1) %>" data-improvement-history-page>Next
<% } %> +
+ + <% if (access.can_approve) { %> +
+ Advanced tools and accessCorrection bank, moderator access, evaluation cases, and training exports +
+
+

Review access

Choose whether selected moderators may help triage feedback.

+
+
+
+
+
+
+
+ +
+

Correction bank

Normal “Apply improvement” actions are saved immediately. Use this table for manual maintenance.

+
+ <% corrections.entries.forEach((entry) => { %><% }) %> + <% if (!corrections.entries.length) { %><% } %> +
TypePrompt / answerAudienceStateActions
<%= entry.target.replaceAll("_", " ") %>
<%= entry.prompt.slice(0, 100) %>
<%= entry.corrected_answer %>
<%= entry.min_role %> · <%= entry.permission_scope.origin %>/<%= entry.permission_scope.platform %><%= entry.active ? "active" : entry.enabled ? "staged" : "disabled" %><% if (entry.linked_okf_path) { %>Open knowledge<% } %>
" />
No corrections have been created.
+
+ +
+

Evaluation cases

Optional technical tests for checking the current AI configuration.

+
Add a test case
+
<% evalCases.entries.forEach((entry) => { %><% }) %><% if (!evalCases.entries.length) { %><% } %>
PromptRole / originExpectedMust avoidActions
<%= entry.prompt %><%= entry.role %> / <%= entry.origin %><%= entry.expected_behavior || "-" %><%= entry.forbidden_behavior || "-" %>
No evaluation cases.
+
Recent test results
<% evalResults.forEach((result) => { %><% }) %><% if (!evalResults.length) { %><% } %>
TimeCaseResultNotes
<%= formatDate(result.run_at) %><%= result.case_id %><%= result.status %><%= result.notes || "-" %>
No test results.
+
+ + <% if (access.can_export) { %>

Training exports

Manual exports include only approved examples. Lumi does not start training.

<% } %> +
<% } %> -
- <% evalCases.entries.forEach((entry) => { %><% }) %> - <% if (!evalCases.entries.length) { %><% } %> -
PromptRole / originExpectedForbiddenExpected linkActions
<%= entry.prompt %><%= entry.role %> / <%= entry.origin %><%= entry.expected_behavior || "-" %><%= entry.forbidden_behavior || "-" %><%= entry.expected_link || "-" %><% if (access.can_run_evals) { %>
<% } %>
No eval cases.
-
- " href="?eval_page=<%= Math.max(1, evalCases.page - 1) %>#evals">Previous - Page <%= evalCases.page %> of <%= evalCases.pages %> (<%= evalCases.total %> cases) - " href="?eval_page=<%= Math.min(evalCases.pages, evalCases.page + 1) %>#evals">Next -
-
Recent eval results -
- <% evalResults.forEach((result) => { %><% }) %> - <% if (!evalResults.length) { %><% } %> -
TimeCaseResultNotes
<%= formatDate(result.run_at) %><%= result.case_id %><%= result.status %><%= result.notes || "-" %>
No eval results.
-
- + -<% if (access.can_export) { %> -
-

Training exports

Manual JSONL exports include approved examples only. Lumi does not start training.

-
-
-
-
-
-<% } %> - - + <%- include("../../../src/web/views/partials/layout-bottom") %> diff --git a/release-index.json b/release-index.json index 275312b..8a29576 100644 --- a/release-index.json +++ b/release-index.json @@ -2,6 +2,36 @@ "schema_version": 1, "channel": "stable", "releases": [ + { + "version": "0.2.7", + "ref": "refs/tags/v0.2.7", + "released_at": "2026-07-18", + "installable": true, + "rollback_safe": true, + "replaces_versions": [ + "1.2.0" + ], + "data_policy": "preserve", + "dependency_policy": "sync_on_restart", + "migration_notes": "Adds a live one-step Lumi AI feedback workflow and searchable finalized-feedback history while preserving existing review and correction data.", + "plugins": { + "auto-vc": "0.1.6", + "birthday": "0.1.3", + "economy-framework": "0.2.10", + "economy-games": "0.1.7", + "expression-interaction": "0.2.1", + "lumi_ai": "0.8.3", + "moderation": "0.1.5", + "okf": "0.1.1", + "quotes": "0.1.2", + "sample-plugin": "0.1.0", + "throne_wishlist": "0.1.2", + "welcome_messages": "0.1.1" + }, + "tools": { + "lumi_ai_web_search": "0.1.1" + } + }, { "version": "0.2.6", "ref": "refs/tags/v0.2.6", diff --git a/scripts/verify-release-metadata.js b/scripts/verify-release-metadata.js index 6425a94..d3fc3bf 100644 --- a/scripts/verify-release-metadata.js +++ b/scripts/verify-release-metadata.js @@ -4,15 +4,15 @@ const path = require("path"); const { findSafeTarget } = require("../src/services/versioning"); const root = path.join(__dirname, ".."); -const releaseVersion = "0.2.6"; -const previousCoreVersion = "0.2.5"; +const releaseVersion = "0.2.7"; +const previousCoreVersion = "0.2.6"; const earliestCompatibleCoreVersion = "0.1.9"; const changedPlugins = { "auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" }, birthday: { from: "0.1.2", to: "0.1.3", knowledge: "birthday" }, "economy-framework": { from: "0.2.9", to: "0.2.10", knowledge: "economy-framework" }, "expression-interaction": { from: "0.2.0", to: "0.2.1", knowledge: "expression-interaction" }, - lumi_ai: { from: "0.8.1", to: "0.8.2", knowledge: "lumi-ai" }, + lumi_ai: { from: "0.8.2", to: "0.8.3", knowledge: "lumi-ai" }, moderation: { from: "0.1.4", to: "0.1.5", knowledge: "moderation" }, okf: { from: "0.1.0", to: "0.1.1", knowledge: "okf" }, quotes: { from: "0.1.1", to: "0.1.2", knowledge: "quotes" }, @@ -84,7 +84,7 @@ for (const [pluginId, expected] of Object.entries(changedPlugins)) { const webSearch = readJson("plugins/lumi_ai_web_search/tool_info.json"); assert.equal(webSearch.version, "0.1.1"); assert.equal(webSearch.minimum_lumi_version, "0.2.0"); -assert.equal(webSearch.minimum_lumi_ai_version, changedPlugins.lumi_ai.to); +assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2"); assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true); -console.log("Release metadata verification passed: core 0.2.6 and 11 changed plugin/tool packages."); +console.log("Release metadata verification passed: core 0.2.7, Lumi AI 0.8.3, and synchronized package metadata."); diff --git a/scripts/verify-update-system.js b/scripts/verify-update-system.js index 9084d05..1eec41f 100644 --- a/scripts/verify-update-system.js +++ b/scripts/verify-update-system.js @@ -16,7 +16,7 @@ function readJson(relativePath) { const releaseIndex = readJson("release-index.json"); const releaseVersions = releaseIndex.releases.map((release) => release.version); -assert.deepEqual(releaseVersions, ["0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]); +assert.deepEqual(releaseVersions, ["0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]); assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique"); for (const release of releaseIndex.releases) { assert.equal(normalizeRepositoryRef(release.ref), release.ref); @@ -37,6 +37,7 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) { const baseTarget = { current_version: "0.2.4", available_versions: [ + { version: "0.2.7", ref: "refs/tags/v0.2.7", rollback_safe: true }, { version: "0.2.6", ref: "refs/tags/v0.2.6", rollback_safe: true }, { version: "0.2.5", ref: "refs/tags/v0.2.5", rollback_safe: true }, { version: "0.2.4", ref: "refs/tags/v0.2.4", rollback_safe: true }, @@ -66,7 +67,7 @@ const corrected = buildStatus({ channel: "stable" }); assert.equal(corrected.version_correction, true); -assert.equal(corrected.safe_target_version, "0.2.6"); +assert.equal(corrected.safe_target_version, "0.2.7"); assert.equal(corrected.update_available, true); assert.equal(corrected.blocked, false); diff --git a/update-manifest.json b/update-manifest.json index 60a8871..7ffdc7f 100644 --- a/update-manifest.json +++ b/update-manifest.json @@ -1,6 +1,6 @@ { "name": "Lumi Core", - "version": "0.2.6", + "version": "0.2.7", "channel": "stable", "released_at": "2026-07-18", "compatible_from": "0.1.9", @@ -8,7 +8,7 @@ "replaces_versions": [ "1.2.0" ], - "migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics with private-proxy HTTPS support, the shared form-action fix, and Windows/network-share-safe plugin code replacement. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, and secrets.", + "migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics, Windows/network-share-safe plugin code replacement, and the live streamlined Lumi AI feedback workflow. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, feedback, and secrets.", "rollback_safe": true, "requirements": [ "Node.js 18 or newer" @@ -97,6 +97,18 @@ ], "rollback_safe": true, "migration_notes": "Updates plugin code without renaming a running plugin's preserved data directory, avoiding Windows and network-share locks; preserved local data is not replaced." + }, + { + "version": "0.2.7", + "channel": "stable", + "released_at": "2026-07-18", + "compatible_from": "0.1.9", + "migration_kind": "patch", + "replaces_versions": [ + "1.2.0" + ], + "rollback_safe": true, + "migration_notes": "Adds the live streamlined Lumi AI feedback queue and searchable finalized-feedback history; existing feedback and plugin data are preserved." } ] }