Streamline Lumi AI feedback reviews
This commit is contained in:
parent
9647f11bf9
commit
e710469492
@ -1,5 +1,10 @@
|
|||||||
# Lumi changelog
|
# 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
|
## 0.2.6
|
||||||
|
|
||||||
- Fixed plugin updates on Windows and network shares when a running plugin keeps its preserved `data` directory open.
|
- Fixed plugin updates on Windows and network shares when a running plugin keeps its preserved `data` directory open.
|
||||||
|
|||||||
1
TODO.md
1
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 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: 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: 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: 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: 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.
|
- 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.
|
||||||
|
|||||||
34
docs/lumi-ai-improvement-center.md
Normal file
34
docs/lumi-ai-improvement-center.md
Normal file
@ -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.
|
||||||
@ -14,7 +14,7 @@ editable: false
|
|||||||
Lumi is the core web UI and bot runtime.
|
Lumi is the core web UI and bot runtime.
|
||||||
## Runtime
|
## Runtime
|
||||||
Package: lumi-bot
|
Package: lumi-bot
|
||||||
Version: 0.2.6
|
Version: 0.2.7
|
||||||
## Routes
|
## Routes
|
||||||
- GET /api/events
|
- GET /api/events
|
||||||
- POST /api/destructive-confirmations
|
- POST /api/destructive-confirmations
|
||||||
|
|||||||
@ -14,7 +14,7 @@ editable: false
|
|||||||
Managed local AI provider and scoped WebUI assistant for Lumi.
|
Managed local AI provider and scoped WebUI assistant for Lumi.
|
||||||
## Metadata
|
## Metadata
|
||||||
Plugin ID: lumi_ai
|
Plugin ID: lumi_ai
|
||||||
Version: 0.8.2
|
Version: 0.8.3
|
||||||
Default state: enabled
|
Default state: enabled
|
||||||
## Web Routes
|
## Web Routes
|
||||||
- /plugins/lumi_ai
|
- /plugins/lumi_ai
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.12",
|
"adm-zip": "^0.5.12",
|
||||||
"better-sqlite3": "^11.5.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
# Lumi AI changelog
|
# 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
|
## 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.
|
- 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.
|
||||||
|
|||||||
@ -20,6 +20,7 @@ const FEEDBACK_TAGS = Object.freeze([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const FEEDBACK_KINDS = Object.freeze(["instruction_based", "strict_correction"]);
|
const FEEDBACK_KINDS = Object.freeze(["instruction_based", "strict_correction"]);
|
||||||
|
const FINAL_FEEDBACK_STATUSES = Object.freeze(["implemented", "rejected", "reviewed", "archived"]);
|
||||||
|
|
||||||
class FeedbackStore {
|
class FeedbackStore {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
@ -65,13 +66,27 @@ class FeedbackStore {
|
|||||||
return entry;
|
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
|
const filtered = this.read().entries
|
||||||
.filter((entry) => roleRank(viewerRole) >= roleRank(entry.role))
|
.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);
|
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() {
|
all() {
|
||||||
return this.read().entries;
|
return this.read().entries;
|
||||||
}
|
}
|
||||||
@ -93,7 +108,7 @@ class FeedbackStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setStatus(id, status, actor, notes = "") {
|
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.");
|
throw new Error("Invalid review status.");
|
||||||
}
|
}
|
||||||
return this.mutate(id, (entry) => ({
|
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 = "") {
|
verify(id, actor, notes = "") {
|
||||||
return this.mutate(id, (entry) => ({
|
return this.mutate(id, (entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
@ -243,11 +283,35 @@ function roleRank(role) {
|
|||||||
return { user: 1, mod: 2, admin: 3 }[normalizeRole(role)] || 1;
|
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 = {
|
module.exports = {
|
||||||
FEEDBACK_KINDS,
|
FEEDBACK_KINDS,
|
||||||
FEEDBACK_TAGS,
|
FEEDBACK_TAGS,
|
||||||
|
FINAL_FEEDBACK_STATUSES,
|
||||||
FeedbackStore,
|
FeedbackStore,
|
||||||
improvementAccess,
|
improvementAccess,
|
||||||
|
isFinalized,
|
||||||
normalizeContextSnapshot,
|
normalizeContextSnapshot,
|
||||||
paginate,
|
paginate,
|
||||||
atomicJson
|
atomicJson
|
||||||
|
|||||||
@ -39,7 +39,7 @@ function approvedExamples(feedbackRows, correctionRows) {
|
|||||||
.map((entry) => [entry.source_feedback_id, entry])
|
.map((entry) => [entry.source_feedback_id, entry])
|
||||||
);
|
);
|
||||||
return feedbackRows
|
return feedbackRows
|
||||||
.filter((entry) => entry.export_approved && entry.status === "approved")
|
.filter((entry) => entry.export_approved && ["approved", "implemented"].includes(entry.status))
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const correction = byFeedback.get(entry.id);
|
const correction = byFeedback.get(entry.id);
|
||||||
const preferred = correction?.corrected_answer || entry.optional_correction;
|
const preferred = correction?.corrected_answer || entry.optional_correction;
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const { AiRateLimiter, mergeLimits } = require("./backend/rate_limits");
|
|||||||
const { buildOriginContext, formatPlatformReply, formatPlatformReplyDetails } = require("./backend/commands");
|
const { buildOriginContext, formatPlatformReply, formatPlatformReplyDetails } = require("./backend/commands");
|
||||||
const { AssistantPanelDiagnostics } = require("./backend/assistant_panel_diagnostics");
|
const { AssistantPanelDiagnostics } = require("./backend/assistant_panel_diagnostics");
|
||||||
const { formatAssistantResponse } = require("./backend/response_formatter");
|
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 { CorrectionStore, PROMOTION_TARGETS } = require("./backend/corrections");
|
||||||
const { EvalStore } = require("./backend/evals");
|
const { EvalStore } = require("./backend/evals");
|
||||||
const { TrainingExporter } = require("./backend/training_export");
|
const { TrainingExporter } = require("./backend/training_export");
|
||||||
@ -1053,6 +1053,12 @@ module.exports = {
|
|||||||
optional_correction: req.body.optional_correction,
|
optional_correction: req.body.optional_correction,
|
||||||
context_snapshot: req.body.context_snapshot
|
context_snapshot: req.body.context_snapshot
|
||||||
}, req.session.user);
|
}, 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 });
|
return res.status(201).json({ success: true, id: entry.id });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({ error: error.message });
|
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) => {
|
router.get("/improvement_center", (req, res) => {
|
||||||
const access = improvementAccess(req.session.user, config);
|
const access = improvementAccess(req.session.user, config);
|
||||||
if (!access.allowed) return deniedImprovement(res);
|
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"), {
|
return res.render(path.join(__dirname, "views", "improvement-center.ejs"), {
|
||||||
title: "Lumi AI Improvement Center",
|
title: "Lumi AI Improvement Center",
|
||||||
config,
|
config,
|
||||||
@ -1313,9 +1331,20 @@ module.exports = {
|
|||||||
reviews: feedbackStore.list({
|
reviews: feedbackStore.list({
|
||||||
page: req.query.review_page,
|
page: req.query.review_page,
|
||||||
pageSize: 15,
|
pageSize: 15,
|
||||||
status: cleanText(req.query.status, 30),
|
scope: "active",
|
||||||
viewerRole: access.role
|
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 }),
|
corrections: correctionStore.list({ page: req.query.correction_page, pageSize: 15 }),
|
||||||
evalCases: evalStore.list({ page: req.query.eval_page, pageSize: 15 }),
|
evalCases: evalStore.list({ page: req.query.eval_page, pageSize: 15 }),
|
||||||
evalResults: evalStore.results(25),
|
evalResults: evalStore.results(25),
|
||||||
@ -1336,6 +1365,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
ensureSidebarNavItem(settings);
|
ensureSidebarNavItem(settings);
|
||||||
|
announceImprovementChange(null, "Improvement Center settings changed.", "settings");
|
||||||
return improvementFlash(req, res, "success", "Improvement Center settings saved.");
|
return improvementFlash(req, res, "success", "Improvement Center settings saved.");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1346,24 +1376,38 @@ module.exports = {
|
|||||||
const review = feedbackStore.get(req.params.id);
|
const review = feedbackStore.get(req.params.id);
|
||||||
if (!canReviewAiFeedback(review, access)) return deniedImprovement(res);
|
if (!canReviewAiFeedback(review, access)) return deniedImprovement(res);
|
||||||
const action = cleanText(req.body.action, 30);
|
const action = cleanText(req.body.action, 30);
|
||||||
|
let updated = null;
|
||||||
if (action === "flag" && access.can_flag) {
|
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) {
|
} 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) {
|
} 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) {
|
} 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) {
|
} 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) {
|
} 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) {
|
} 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 {
|
} else {
|
||||||
return deniedImprovement(res);
|
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) {
|
} catch (error) {
|
||||||
return improvementFlash(req, res, "error", error.message);
|
return improvementFlash(req, res, "error", error.message);
|
||||||
}
|
}
|
||||||
@ -1373,8 +1417,10 @@ module.exports = {
|
|||||||
const access = improvementAccess(req.session.user, config);
|
const access = improvementAccess(req.session.user, config);
|
||||||
if (!access.can_delete) return deniedImprovement(res);
|
if (!access.can_delete) return deniedImprovement(res);
|
||||||
try {
|
try {
|
||||||
|
const deleted = feedbackStore.get(req.params.id);
|
||||||
feedbackStore.delete(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) {
|
} catch (error) {
|
||||||
return improvementFlash(req, res, "error", error.message);
|
return improvementFlash(req, res, "error", error.message);
|
||||||
}
|
}
|
||||||
@ -1384,8 +1430,9 @@ module.exports = {
|
|||||||
const access = improvementAccess(req.session.user, config);
|
const access = improvementAccess(req.session.user, config);
|
||||||
if (!access.can_delete) return deniedImprovement(res);
|
if (!access.can_delete) return deniedImprovement(res);
|
||||||
try {
|
try {
|
||||||
feedbackStore.setStatus(req.params.id, "archived", req.session.user, req.body.review_notes);
|
const updated = feedbackStore.setStatus(req.params.id, "archived", req.session.user, req.body.review_notes);
|
||||||
return improvementFlash(req, res, "success", "Feedback record archived.");
|
announceImprovementChange(updated, "Feedback archived and moved to history.");
|
||||||
|
return improvementFlash(req, res, "success", "Feedback archived and moved to history.", { review: updated });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return improvementFlash(req, res, "error", error.message);
|
return improvementFlash(req, res, "error", error.message);
|
||||||
}
|
}
|
||||||
@ -1395,11 +1442,16 @@ module.exports = {
|
|||||||
const access = improvementAccess(req.session.user, config);
|
const access = improvementAccess(req.session.user, config);
|
||||||
if (!access.can_implement) return deniedImprovement(res);
|
if (!access.can_implement) return deniedImprovement(res);
|
||||||
try {
|
try {
|
||||||
const review = feedbackStore.get(req.params.id);
|
let review = feedbackStore.get(req.params.id);
|
||||||
if (!review || review.status !== "approved") throw new Error("Approve the review before implementing it.");
|
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";
|
const target = PROMOTION_TARGETS.includes(req.body.target) ? req.body.target : "correction";
|
||||||
|
let implementationId = "";
|
||||||
|
let implementationSummary = "";
|
||||||
if (target === "eval_case") {
|
if (target === "eval_case") {
|
||||||
evalStore.add({
|
const evalCase = evalStore.add({
|
||||||
prompt: review.user_message,
|
prompt: review.user_message,
|
||||||
role: req.body.min_role || review.role,
|
role: req.body.min_role || review.role,
|
||||||
origin: req.body.permission_origin || review.origin,
|
origin: req.body.permission_origin || review.origin,
|
||||||
@ -1408,8 +1460,11 @@ module.exports = {
|
|||||||
expected_link: req.body.expected_link,
|
expected_link: req.body.expected_link,
|
||||||
notes: req.body.notes
|
notes: req.body.notes
|
||||||
}, req.session.user);
|
}, req.session.user);
|
||||||
|
implementationId = evalCase.id;
|
||||||
|
implementationSummary = "Added as an evaluation case.";
|
||||||
} else if (target === "training_export") {
|
} else if (target === "training_export") {
|
||||||
feedbackStore.markExportApproved(review.id, req.session.user);
|
feedbackStore.markExportApproved(review.id, req.session.user);
|
||||||
|
implementationSummary = "Approved for the next manual training export.";
|
||||||
} else {
|
} else {
|
||||||
const correction = correctionStore.createFromFeedback(review, {
|
const correction = correctionStore.createFromFeedback(review, {
|
||||||
...req.body,
|
...req.body,
|
||||||
@ -1427,8 +1482,19 @@ module.exports = {
|
|||||||
throw error;
|
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) {
|
} catch (error) {
|
||||||
return improvementFlash(req, res, "error", error.message);
|
return improvementFlash(req, res, "error", error.message);
|
||||||
}
|
}
|
||||||
@ -1438,6 +1504,7 @@ module.exports = {
|
|||||||
const access = improvementAccess(req.session.user, config);
|
const access = improvementAccess(req.session.user, config);
|
||||||
if (!access.can_implement) return deniedImprovement(res);
|
if (!access.can_implement) return deniedImprovement(res);
|
||||||
const result = correctionStore.saveCorrections(req.session.user);
|
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.`);
|
return improvementFlash(req, res, "success", `Corrections saved. ${result.active} of ${result.total} are active.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1462,6 +1529,7 @@ module.exports = {
|
|||||||
} else {
|
} else {
|
||||||
return deniedImprovement(res);
|
return deniedImprovement(res);
|
||||||
}
|
}
|
||||||
|
announceImprovementChange(null, `Correction ${action} completed.`, "correction");
|
||||||
return improvementFlash(req, res, "success", `Correction ${action} completed. Save Corrections to activate changes.`);
|
return improvementFlash(req, res, "success", `Correction ${action} completed. Save Corrections to activate changes.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return improvementFlash(req, res, "error", error.message);
|
return improvementFlash(req, res, "error", error.message);
|
||||||
@ -1473,6 +1541,7 @@ module.exports = {
|
|||||||
if (!access.can_delete) return deniedImprovement(res);
|
if (!access.can_delete) return deniedImprovement(res);
|
||||||
try {
|
try {
|
||||||
correctionStore.delete(req.params.id);
|
correctionStore.delete(req.params.id);
|
||||||
|
announceImprovementChange(null, "Correction deleted.", "correction");
|
||||||
return improvementFlash(req, res, "success", "Correction delete completed. Save Corrections to activate changes.");
|
return improvementFlash(req, res, "success", "Correction delete completed. Save Corrections to activate changes.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return improvementFlash(req, res, "error", error.message);
|
return improvementFlash(req, res, "error", error.message);
|
||||||
@ -1487,6 +1556,7 @@ module.exports = {
|
|||||||
throw new Error("Expected links must match a verified Lumi route.");
|
throw new Error("Expected links must match a verified Lumi route.");
|
||||||
}
|
}
|
||||||
evalStore.add(req.body, req.session.user);
|
evalStore.add(req.body, req.session.user);
|
||||||
|
announceImprovementChange(null, "Evaluation case added.", "eval");
|
||||||
return improvementFlash(req, res, "success", "Eval case added.");
|
return improvementFlash(req, res, "success", "Eval case added.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return improvementFlash(req, res, "error", error.message);
|
return improvementFlash(req, res, "error", error.message);
|
||||||
@ -1497,6 +1567,7 @@ module.exports = {
|
|||||||
const access = improvementAccess(req.session.user, config);
|
const access = improvementAccess(req.session.user, config);
|
||||||
if (!access.can_run_evals) return deniedImprovement(res);
|
if (!access.can_run_evals) return deniedImprovement(res);
|
||||||
evalStore.delete(req.params.id);
|
evalStore.delete(req.params.id);
|
||||||
|
announceImprovementChange(null, "Evaluation case deleted.", "eval");
|
||||||
return improvementFlash(req, res, "success", "Eval case deleted.");
|
return improvementFlash(req, res, "success", "Eval case deleted.");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1505,6 +1576,7 @@ module.exports = {
|
|||||||
if (!access.can_run_evals) return deniedImprovement(res);
|
if (!access.can_run_evals) return deniedImprovement(res);
|
||||||
try {
|
try {
|
||||||
const results = await evalStore.runAll({ provider, actor: req.session.user });
|
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).`);
|
return improvementFlash(req, res, "success", `Eval run completed with ${results.length} result(s).`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return improvementFlash(req, res, "error", error.message);
|
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." });
|
if (req.accepts(["json", "html"]) === "json") return res.status(403).json({ error: "Access denied." });
|
||||||
return denied(res);
|
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 };
|
req.session.flash = { type, message };
|
||||||
return res.redirect(`/plugins/${PLUGIN_ID}/improvement_center`);
|
return res.redirect(`/plugins/${PLUGIN_ID}/improvement_center`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"id": "lumi_ai",
|
"id": "lumi_ai",
|
||||||
"name": "Lumi AI",
|
"name": "Lumi AI",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"description": "Managed local AI provider and scoped WebUI assistant for Lumi.",
|
"description": "Managed local AI provider and scoped WebUI assistant for Lumi.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"compatible_from": "0.8.1",
|
"compatible_from": "0.8.2",
|
||||||
"migration_notes": "Existing models, runtimes, settings, feedback, metrics, and tool data are retained. No manual migration is required.",
|
"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
|
"rollback_safe": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,66 @@
|
|||||||
.improvement-titlebar { align-items: center; }
|
.improvement-titlebar { align-items: center; }
|
||||||
.improvement-filters, .improvement-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
|
.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-actions form { margin: 0; }
|
.improvement-live-status[hidden] { display: none; }
|
||||||
.improvement-list { display: grid; gap: 12px; }
|
.improvement-live-status[data-tone="success"] { border-color: var(--sea); }
|
||||||
.improvement-card { padding: 13px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); }
|
.improvement-live-status[data-tone="error"] { border-color: var(--danger); }
|
||||||
.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-guide { background: linear-gradient(135deg, color-mix(in srgb, var(--sea) 9%, var(--card)), var(--card)); }
|
||||||
.improvement-card > header strong { color: var(--ink); font-size: 14px; }
|
.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 { 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 > 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; text-transform: uppercase; }
|
.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: 240px; margin: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); font: inherit; }
|
.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 { margin-bottom: 10px; border-left: 3px solid var(--sea); }
|
||||||
.improvement-correction strong { display: block; margin-bottom: 5px; }
|
.improvement-correction strong, .improvement-review-note 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-review-note { margin-bottom: 10px; border-left: 3px solid var(--warning, #d1a43a); }
|
||||||
.improvement-dialog::backdrop { background: rgba(0, 0, 0, .55); }
|
.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-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) {
|
@media (max-width: 760px) {
|
||||||
.improvement-pair { grid-template-columns: 1fr; }
|
.improvement-pair { grid-template-columns: 1fr; }
|
||||||
.improvement-titlebar { align-items: flex-start; }
|
.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%; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) => {
|
document.addEventListener("click", (event) => {
|
||||||
const opener = event.target.closest("[data-open-dialog]");
|
const opener = event.target.closest("[data-open-dialog]");
|
||||||
if (opener) {
|
if (opener) {
|
||||||
document.getElementById(opener.dataset.openDialog)?.showModal();
|
document.getElementById(opener.dataset.openDialog)?.showModal();
|
||||||
return;
|
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]");
|
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();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -46,7 +46,7 @@ const { AiRateLimiter, mergeLimits } = require("../backend/rate_limits");
|
|||||||
const { PLATFORM_DEFAULTS, buildOriginContext, formatPlatformReply, formatPlatformReplyDetails } = require("../backend/commands");
|
const { PLATFORM_DEFAULTS, buildOriginContext, formatPlatformReply, formatPlatformReplyDetails } = require("../backend/commands");
|
||||||
const { AssistantPanelDiagnostics } = require("../backend/assistant_panel_diagnostics");
|
const { AssistantPanelDiagnostics } = require("../backend/assistant_panel_diagnostics");
|
||||||
const { formatAssistantResponse, normalizeLink, normalizeCodeFences } = require("../backend/response_formatter");
|
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 { CorrectionStore } = require("../backend/corrections");
|
||||||
const { EvalStore, evaluateCase } = require("../backend/evals");
|
const { EvalStore, evaluateCase } = require("../backend/evals");
|
||||||
const { TrainingExporter, approvedExamples } = require("../backend/training_export");
|
const { TrainingExporter, approvedExamples } = require("../backend/training_export");
|
||||||
@ -234,12 +234,16 @@ async function run() {
|
|||||||
assert(settingsTemplate.includes("Export for Codex"));
|
assert(settingsTemplate.includes("Export for Codex"));
|
||||||
assert(settingsTemplate.includes("raw diagnostics, and tool payloads removed"));
|
assert(settingsTemplate.includes("raw diagnostics, and tool payloads removed"));
|
||||||
const improvementTemplate = fs.readFileSync(path.join(PLUGIN_ROOT, "views", "improvement-center.ejs"), "utf8");
|
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(control));
|
||||||
}
|
}
|
||||||
assert(improvementTemplate.includes('data-confirm-title="Delete eval case"'));
|
assert(improvementTemplate.includes('data-confirm-title="Delete eval case"'));
|
||||||
assert(improvementTemplate.includes("Sources used for this answer"));
|
assert(improvementTemplate.includes("What Lumi used to answer"));
|
||||||
assert(improvementTemplate.includes("Searchable OKF title"));
|
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");
|
const assistantFeedbackScript = fs.readFileSync(path.join(PLUGIN_ROOT, "public", "assistant.js"), "utf8");
|
||||||
for (const tag of FEEDBACK_TAGS) assert(assistantFeedbackScript.includes(`"${tag}"`));
|
for (const tag of FEEDBACK_TAGS) assert(assistantFeedbackScript.includes(`"${tag}"`));
|
||||||
assert(assistantFeedbackScript.includes("/assistant/feedback"));
|
assert(assistantFeedbackScript.includes("/assistant/feedback"));
|
||||||
@ -357,6 +361,17 @@ async function run() {
|
|||||||
platform: "webui"
|
platform: "webui"
|
||||||
}).length, 0);
|
}).length, 0);
|
||||||
correctionConfig.improvement.corrections_enabled = true;
|
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({
|
const evalStore = new EvalStore({
|
||||||
casesFile: path.join(improvementTemp, "eval-cases.json"),
|
casesFile: path.join(improvementTemp, "eval-cases.json"),
|
||||||
|
|||||||
@ -1,247 +1,192 @@
|
|||||||
<%- include("../../../src/web/views/partials/layout-top", { title }) %>
|
<%- include("../../../src/web/views/partials/layout-top", { title }) %>
|
||||||
<link rel="stylesheet" href="/plugins/lumi_ai/assets/settings.css?v=0.6.0" />
|
<link rel="stylesheet" href="/plugins/lumi_ai/assets/settings.css?v=0.8.3" />
|
||||||
<link rel="stylesheet" href="/plugins/lumi_ai/assets/improvement-center.css?v=0.6.0" />
|
<link rel="stylesheet" href="/plugins/lumi_ai/assets/improvement-center.css?v=0.8.3" />
|
||||||
|
|
||||||
|
<% 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); %>
|
||||||
<section class="ai-titlebar improvement-titlebar">
|
<section class="ai-titlebar improvement-titlebar">
|
||||||
<div>
|
<div>
|
||||||
<h1>Lumi AI Improvement Center</h1>
|
<h1>Improve Lumi AI</h1>
|
||||||
<p>Review assistant feedback, stage approved corrections, run evals, and create manual training exports.</p>
|
<p>Review feedback and either apply an improvement or close it. Finished items move to searchable history automatically.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ai-tag"><%= access.role %><%= access.trusted ? " · trusted reviewer" : "" %></span>
|
<span class="ai-tag"><%= access.role %><%= access.trusted ? " · trusted reviewer" : "" %></span>
|
||||||
<% if (access.can_approve) { %><a class="button subtle" href="/plugins/lumi_ai">Lumi AI settings</a><% } %>
|
<% if (access.can_approve) { %><a class="button subtle" href="/plugins/lumi_ai">AI settings</a><% } %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<nav class="ai-tabs" aria-label="Improvement Center sections">
|
<div class="improvement-live-status" data-improvement-status role="status" aria-live="polite"></div>
|
||||||
<a href="#reviews">Review queue</a>
|
|
||||||
<a href="#corrections">Corrections</a>
|
|
||||||
<a href="#evals">Evals</a>
|
|
||||||
<% if (access.can_export) { %><a href="#exports">Exports</a><% } %>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<% if (access.can_approve) { %>
|
<div data-improvement-workspace>
|
||||||
<section class="ai-band">
|
<section class="ai-band improvement-guide" aria-labelledby="improvement-guide-title">
|
||||||
<div class="ai-section-heading"><div><h2>Access and activation</h2><p>Corrections remain inactive until an administrator selects Save Corrections.</p></div></div>
|
<div class="ai-section-heading">
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/settings" class="form-grid ai-form">
|
<div><h2 id="improvement-guide-title">A simpler review process</h2><p>The usual path takes one decision and no manual activation step.</p></div>
|
||||||
<div class="field"><label><input type="checkbox" name="allow_moderators_to_review_responses" <%= config.improvement.allow_moderators_to_review_responses ? "checked" : "" %> /> Allow moderators to review responses</label></div>
|
<button class="button subtle" type="button" data-open-history>Search finished feedback (<%= reviewCounts.history %>)</button>
|
||||||
<div class="field"><label><input type="checkbox" name="corrections_enabled" <%= config.improvement.corrections_enabled ? "checked" : "" %> /> Use active approved corrections</label></div>
|
</div>
|
||||||
<div class="field full"><label>Trusted moderator reviewer IDs</label><textarea name="trusted_moderator_reviewers" rows="2"><%= config.improvement.trusted_moderator_reviewers.join("\n") %></textarea></div>
|
<ol class="improvement-steps">
|
||||||
<div class="field full"><button class="button" type="submit">Save access settings</button></div>
|
<li><span>1</span><div><strong>Read the report</strong><small>Compare the question, Lumi's answer, and the user's suggestion.</small></div></li>
|
||||||
</form>
|
<li><span>2</span><div><strong>Choose an outcome</strong><small>Apply the improvement, dismiss it, or mark that no change is needed.</small></div></li>
|
||||||
|
<li><span>3</span><div><strong>Done automatically</strong><small>Applied corrections become active immediately. Finished reports move to history.</small></div></li>
|
||||||
|
</ol>
|
||||||
|
<div class="improvement-counts" aria-label="Feedback queue summary">
|
||||||
|
<div><span>Active queue</span><strong><%= reviewCounts.active %></strong></div>
|
||||||
|
<div><span>New</span><strong><%= reviewCounts.pending %></strong></div>
|
||||||
|
<div><span>Needs attention</span><strong><%= reviewCounts.needs_attention %></strong></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<section class="ai-band" id="reviews">
|
<section class="ai-band" id="reviews">
|
||||||
<div class="ai-section-heading">
|
<div class="ai-section-heading">
|
||||||
<div><h2>Review queue</h2><p>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.</p></div>
|
<div><h2>Feedback waiting for review</h2><p>Finished feedback is hidden from this queue and remains available in history.</p></div>
|
||||||
<div class="improvement-filters">
|
<button class="button subtle" type="button" data-open-history>Open history</button>
|
||||||
<% ["", "pending", "flagged", "verified", "approved", "reviewed", "rejected", "archived"].forEach((status) => { %>
|
|
||||||
<a class="button subtle" href="?status=<%= status %>#reviews"><%= status || "All" %></a>
|
|
||||||
<% }) %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="improvement-list">
|
<div class="improvement-list">
|
||||||
<% reviews.entries.forEach((review) => { %>
|
<% reviews.entries.forEach((review) => { const snapshot = review.context_snapshot || {}; const retrieval = snapshot.retrieval || {}; const snapshotRows = [...(snapshot.okf || []), ...(snapshot.corrections || []), ...(snapshot.repository || [])]; %>
|
||||||
<article class="improvement-card" id="review-<%= review.id %>">
|
<article class="improvement-card" id="review-<%= review.id %>" data-review-id="<%= review.id %>">
|
||||||
<header>
|
<header>
|
||||||
<div><strong><%= review.rating === "up" ? "Helpful" : "Needs work" %></strong> <span class="ai-tag"><%= review.feedback_tag.replaceAll("_", " ") %></span> <span class="ai-tag"><%= review.feedback_kind || "strict_correction" %></span> <span class="ai-tag"><%= review.status %></span></div>
|
<div class="improvement-card-labels"><strong><%= review.rating === "up" ? "Helpful response" : "Response needs work" %></strong><span class="ai-tag status-<%= review.status %>"><%= statusLabel(review.status) %></span><span class="ai-tag"><%= review.feedback_tag.replaceAll("_", " ") %></span></div>
|
||||||
<span><%= formatDate(review.timestamp) %> · <%= review.role %> · <%= review.platform %> · <%= review.route_used || "unknown route" %></span>
|
<span><%= formatDate(review.timestamp) %> · <%= review.role %> · <%= review.platform %></span>
|
||||||
</header>
|
</header>
|
||||||
<div class="improvement-pair">
|
<div class="improvement-pair">
|
||||||
<div><span>User message</span><pre><%= review.user_message %></pre></div>
|
<div><span>User asked</span><pre><%= review.user_message %></pre></div>
|
||||||
<div><span>Assistant answer</span><pre><%= review.assistant_answer %></pre></div>
|
<div><span>Lumi answered</span><pre><%= review.assistant_answer %></pre></div>
|
||||||
</div>
|
</div>
|
||||||
<% if (review.optional_correction) { %><div class="improvement-correction"><strong><%= review.feedback_kind === "instruction_based" ? "Instruction guidance" : "Suggested correction" %></strong><pre><%= review.optional_correction %></pre></div><% } %>
|
<% if (review.optional_correction) { %><div class="improvement-correction"><strong>User's suggested improvement</strong><pre><%= review.optional_correction %></pre></div><% } %>
|
||||||
<% const snapshot = review.context_snapshot || {}; const retrieval = snapshot.retrieval || {}; const snapshotRows = [...(snapshot.okf || []), ...(snapshot.corrections || []), ...(snapshot.repository || [])]; %>
|
<% if (review.review_notes) { %><div class="improvement-review-note"><strong>Reviewer note</strong><p><%= review.review_notes %></p></div><% } %>
|
||||||
<% if (snapshotRows.length || retrieval.query || (snapshot.tools || []).length) { %>
|
<% if (snapshotRows.length || retrieval.query || (snapshot.tools || []).length) { %>
|
||||||
<details class="lumi-expandable-settings">
|
<details class="lumi-expandable-settings">
|
||||||
<summary><strong>Sources used for this answer</strong><span class="hint"><%= retrieval.selected_count || snapshotRows.length %> selected · <%= retrieval.candidate_count || 0 %> considered</span></summary>
|
<summary><strong>What Lumi used to answer</strong><span class="hint"><%= retrieval.selected_count || snapshotRows.length %> source(s) selected</span></summary>
|
||||||
<div class="lumi-expandable-body">
|
<div class="lumi-expandable-body">
|
||||||
<% if (retrieval.query) { %><p class="hint"><strong>Lookup:</strong> <%= retrieval.query %> · depth <%= retrieval.depth || "unknown" %></p><% } %>
|
<% if (retrieval.query) { %><p class="hint"><strong>Knowledge lookup:</strong> <%= retrieval.query %></p><% } %>
|
||||||
<% snapshotRows.forEach((source) => { %><pre><%= source %></pre><% }) %>
|
<% snapshotRows.forEach((source) => { %><pre><%= source %></pre><% }) %>
|
||||||
<% if ((snapshot.tools || []).length) { %><p class="hint"><strong>Available/selected tools:</strong> <%= snapshot.tools.join(", ") %></p><% } %>
|
<% if ((snapshot.tools || []).length) { %><p class="hint"><strong>Tools available:</strong> <%= snapshot.tools.join(", ") %></p><% } %>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% if (review.linked_okf_correction) { %><p class="hint"><strong>Searchable correction:</strong> <a href="/plugins/okf?q=<%= encodeURIComponent(review.user_message.slice(0, 80)) %>"><%= review.linked_okf_correction %></a></p><% } %>
|
<div class="improvement-decision-row">
|
||||||
<% if (review.review_notes) { %><p class="hint"><strong>Review notes:</strong> <%= review.review_notes %></p><% } %>
|
<% if (access.can_implement) { %>
|
||||||
<div class="improvement-actions">
|
<button class="button" type="button" data-open-dialog="implement-<%= review.id %>">Apply improvement</button>
|
||||||
<% if (access.can_flag) { %>
|
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>" data-improvement-action>
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>">
|
|
||||||
<input type="hidden" name="action" value="flag" />
|
|
||||||
<button class="button subtle" type="submit">Flag</button>
|
|
||||||
</form>
|
|
||||||
<% } %>
|
|
||||||
<% if (access.can_verify && !["approved", "rejected"].includes(review.status)) { %>
|
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>">
|
|
||||||
<input type="hidden" name="action" value="verify" />
|
|
||||||
<button class="button subtle" type="submit">Verify</button>
|
|
||||||
</form>
|
|
||||||
<% } %>
|
|
||||||
<% if (access.can_approve) { %>
|
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>">
|
|
||||||
<input type="hidden" name="action" value="reviewed" />
|
<input type="hidden" name="action" value="reviewed" />
|
||||||
<button class="button subtle" type="submit">No action needed</button>
|
<button class="button subtle" type="submit">No change needed</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>">
|
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>" data-improvement-action>
|
||||||
<input type="hidden" name="action" value="approve" />
|
|
||||||
<button class="button" type="submit">Approve</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>">
|
|
||||||
<input type="hidden" name="action" value="reject" />
|
<input type="hidden" name="action" value="reject" />
|
||||||
<button class="button subtle" type="submit">Reject</button>
|
<button class="button subtle" type="submit">Dismiss feedback</button>
|
||||||
</form>
|
|
||||||
<button class="button subtle" type="button" data-open-dialog="edit-<%= review.id %>">Edit</button>
|
|
||||||
<% if (review.status === "approved") { %><button class="button" type="button" data-open-dialog="implement-<%= review.id %>">Implement</button><% } %>
|
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>">
|
|
||||||
<input type="hidden" name="action" value="export" />
|
|
||||||
<button class="button subtle" type="submit"><%= review.export_approved ? "Export approved" : "Approve for export" %></button>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>/delete" data-confirm-mode="modal" data-confirm-title="Delete feedback record" data-confirm-text="Delete this feedback record permanently?" data-confirm-label="Delete feedback">
|
|
||||||
<button class="button danger" type="submit">Delete</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>/archive" data-confirm-mode="modal" data-confirm-title="Archive feedback record" data-confirm-text="Archive this feedback record and hide it from the active review queue?" data-confirm-label="Archive feedback">
|
|
||||||
<button class="button danger" type="submit">Archive</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
<button class="button subtle" type="button" data-open-dialog="edit-<%= review.id %>">Edit details</button>
|
||||||
|
<% } else { %>
|
||||||
|
<% if (access.can_verify) { %><form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>" data-improvement-action><input type="hidden" name="action" value="verify" /><button class="button" type="submit">Reviewed — send to admin</button></form><% } %>
|
||||||
|
<% if (access.can_flag) { %><form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>" data-improvement-action><input type="hidden" name="action" value="flag" /><button class="button subtle" type="submit">Needs closer review</button></form><% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<% if (access.can_edit) { %>
|
<% if (access.can_edit) { %>
|
||||||
<dialog class="improvement-dialog" id="edit-<%= review.id %>">
|
<dialog class="improvement-dialog" id="edit-<%= review.id %>">
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>" class="form-grid ai-form">
|
<div class="improvement-dialog-heading"><div><h3>Edit feedback details</h3><p>Clarify the report without changing its outcome.</p></div><button class="icon-button" type="button" data-close-dialog aria-label="Close">×</button></div>
|
||||||
|
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>" class="form-grid ai-form" data-improvement-action>
|
||||||
<input type="hidden" name="action" value="edit" />
|
<input type="hidden" name="action" value="edit" />
|
||||||
<div class="field"><label>Feedback tag</label><select name="feedback_tag"><% feedbackTags.forEach((tag) => { %><option value="<%= tag %>" <%= tag === review.feedback_tag ? "selected" : "" %>><%= tag %></option><% }) %></select></div>
|
<div class="field"><label>Problem type</label><select name="feedback_tag"><% feedbackTags.forEach((tag) => { %><option value="<%= tag %>" <%= tag === review.feedback_tag ? "selected" : "" %>><%= tag.replaceAll("_", " ") %></option><% }) %></select></div>
|
||||||
<div class="field"><label>Feedback type</label><select name="feedback_kind"><% feedbackKinds.forEach((kind) => { %><option value="<%= kind %>" <%= kind === (review.feedback_kind || "instruction_based") ? "selected" : "" %>><%= kind.replaceAll("_", " ") %></option><% }) %></select></div>
|
<div class="field"><label>Improvement style</label><select name="feedback_kind"><% feedbackKinds.forEach((kind) => { %><option value="<%= kind %>" <%= kind === (review.feedback_kind || "instruction_based") ? "selected" : "" %>><%= kind === "instruction_based" ? "Behavior guidance" : "Exact correction" %></option><% }) %></select></div>
|
||||||
<div class="field full"><label>Correction or instruction</label><textarea name="optional_correction" rows="7"><%= review.optional_correction %></textarea></div>
|
<div class="field full"><label>Suggested improvement</label><textarea name="optional_correction" rows="6"><%= review.optional_correction %></textarea></div>
|
||||||
<div class="field full"><label>Review notes</label><textarea name="review_notes" rows="3"><%= review.review_notes %></textarea></div>
|
<div class="field full"><label>Private reviewer note</label><textarea name="review_notes" rows="3"><%= review.review_notes %></textarea></div>
|
||||||
<div class="field full improvement-actions"><button class="button" type="submit">Save review</button><button class="button subtle" type="button" data-close-dialog>Cancel</button></div>
|
<div class="field full improvement-actions"><button class="button" type="submit">Save details</button><button class="button subtle" type="button" data-close-dialog>Cancel</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (access.can_implement && review.status === "approved") { %>
|
<% if (access.can_implement) { %>
|
||||||
<dialog class="improvement-dialog" id="implement-<%= review.id %>">
|
<dialog class="improvement-dialog" id="implement-<%= review.id %>">
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>/implement" class="form-grid ai-form">
|
<div class="improvement-dialog-heading"><div><h3>Apply this improvement</h3><p>Tell Lumi what it should do instead. Saving here activates the change and finishes the feedback.</p></div><button class="icon-button" type="button" data-close-dialog aria-label="Close">×</button></div>
|
||||||
<div class="field"><label>Promotion target</label><select name="target"><% promotionTargets.forEach((target) => { %><option value="<%= target %>"><%= target.replaceAll("_", " ") %></option><% }) %></select></div>
|
<form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>/implement" class="form-grid ai-form" data-improvement-action>
|
||||||
<div class="field"><label>Minimum role</label><select name="min_role"><% ["user", "mod", "admin"].forEach((role) => { %><option value="<%= role %>" <%= role === review.role ? "selected" : "" %>><%= role %></option><% }) %></select></div>
|
<div class="field full"><label>What should Lumi do instead?</label><textarea name="corrected_answer" rows="8" required placeholder="Write the answer or behavior Lumi should use for similar requests."><%= review.optional_correction %></textarea><span class="hint">Use clear, user-facing wording. This becomes the reviewed guidance for similar requests.</span></div>
|
||||||
<div class="field full"><label><%= review.feedback_kind === "instruction_based" ? "Instruction to apply" : "Corrected / expected answer" %></label><textarea name="corrected_answer" rows="7" required><%= review.optional_correction %></textarea></div>
|
<div class="field"><label>Who may receive this improvement?</label><select name="min_role"><% [["user", "Everyone"], ["mod", "Moderators and admins"], ["admin", "Admins only"]].forEach(([role, label]) => { %><option value="<%= role %>" <%= role === review.role ? "selected" : "" %>><%= label %></option><% }) %></select></div>
|
||||||
<div class="field full"><label>Searchable OKF title</label><input name="okf_title" placeholder="Short title for this correction" /><span class="hint">When the target is Correction, Lumi saves an editable, searchable OKF file after approval.</span></div>
|
<div class="field"><label class="checkbox-inline"><input type="checkbox" name="enabled" checked /><span>Start using it immediately</span></label></div>
|
||||||
<div class="field"><label>Origin scope</label><input name="permission_origin" value="<%= review.origin || "any" %>" /></div>
|
<details class="field full ai-settings-group">
|
||||||
<div class="field"><label>Platform scope</label><input name="permission_platform" value="<%= review.platform || "any" %>" /></div>
|
<summary>Advanced options</summary>
|
||||||
|
<div class="form-grid ai-form improvement-advanced-fields">
|
||||||
|
<div class="field"><label>Apply as</label><select name="target"><option value="correction">Answer or behavior guidance</option><option value="predefined_answer">Exact predefined answer</option><option value="route_alias">Route/link answer</option><option value="eval_case">Test case only</option><option value="training_export">Training export only</option></select></div>
|
||||||
|
<div class="field"><label>Searchable knowledge title</label><input name="okf_title" placeholder="Short title for this improvement" /></div>
|
||||||
|
<div class="field"><label>Origin</label><input name="permission_origin" value="<%= review.origin || "any" %>" /></div>
|
||||||
|
<div class="field"><label>Platform</label><input name="permission_platform" value="<%= review.platform || "any" %>" /></div>
|
||||||
<div class="field"><label>Route alias</label><input name="route_alias" /></div>
|
<div class="field"><label>Route alias</label><input name="route_alias" /></div>
|
||||||
<div class="field"><label>Verified expected link</label><input name="expected_link" placeholder="/verified/path" /></div>
|
<div class="field"><label>Verified Lumi link</label><input name="expected_link" placeholder="/verified/path" /></div>
|
||||||
<div class="field full"><label>Forbidden eval behavior</label><textarea name="forbidden_behavior" rows="2"></textarea></div>
|
<div class="field full"><label>Behavior Lumi must avoid</label><textarea name="forbidden_behavior" rows="2"></textarea></div>
|
||||||
<div class="field full"><label>Notes</label><textarea name="notes" rows="2"></textarea></div>
|
<div class="field full"><label>Implementation note</label><textarea name="notes" rows="2"></textarea></div>
|
||||||
<div class="field"><label><input type="checkbox" name="enabled" checked /> Enabled in staged bank</label></div>
|
<div class="field"><label class="checkbox-inline"><input type="checkbox" name="explicitly_safe" /><span>Safe as an exact predefined answer</span></label></div>
|
||||||
<div class="field"><label><input type="checkbox" name="explicitly_safe" /> Safe for predefined answer</label></div>
|
</div>
|
||||||
<div class="field full improvement-actions"><button class="button" type="submit">Promote review</button><button class="button subtle" type="button" data-close-dialog>Cancel</button></div>
|
</details>
|
||||||
|
<div class="field full improvement-actions"><button class="button" type="submit">Apply and finish</button><button class="button subtle" type="button" data-close-dialog>Cancel</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% if (!reviews.entries.length) { %><div class="callout">No feedback matches this filter.</div><% } %>
|
<% if (!reviews.entries.length) { %><div class="improvement-empty"><strong>The queue is clear.</strong><p>New AI feedback will appear here automatically.</p><button class="button subtle" type="button" data-open-history>Search finished feedback</button></div><% } %>
|
||||||
</div>
|
|
||||||
<div class="table-pagination">
|
|
||||||
<a class="button subtle <%= reviews.page <= 1 ? "disabled" : "" %>" href="?review_page=<%= Math.max(1, reviews.page - 1) %>#reviews">Previous</a>
|
|
||||||
<span class="table-page-label">Page <%= reviews.page %> of <%= reviews.pages %> (<%= reviews.total %> reviews)</span>
|
|
||||||
<a class="button subtle <%= reviews.page >= reviews.pages ? "disabled" : "" %>" href="?review_page=<%= Math.min(reviews.pages, reviews.page + 1) %>#reviews">Next</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<% if (reviews.pages > 1) { %><div class="table-pagination"><a class="button subtle <%= reviews.page <= 1 ? "disabled" : "" %>" href="?review_page=<%= Math.max(1, reviews.page - 1) %>#reviews" data-improvement-page>Previous</a><span class="table-page-label">Page <%= reviews.page %> of <%= reviews.pages %></span><a class="button subtle <%= reviews.page >= reviews.pages ? "disabled" : "" %>" href="?review_page=<%= Math.min(reviews.pages, reviews.page + 1) %>#reviews" data-improvement-page>Next</a></div><% } %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="ai-band" id="corrections">
|
<dialog class="improvement-dialog improvement-history-dialog" id="improvement-history">
|
||||||
<div class="ai-section-heading">
|
<div class="improvement-dialog-heading"><div><h2>Finished feedback history</h2><p>Search, inspect, restore, edit, or permanently delete earlier decisions.</p></div><button class="icon-button" type="button" data-close-dialog aria-label="Close history">×</button></div>
|
||||||
<div><h2>Correction bank</h2><p>Edits and toggles are staged. Save Corrections is required before they become active.</p></div>
|
<form class="improvement-history-search" method="get" action="/plugins/lumi_ai/improvement_center" data-improvement-history-search>
|
||||||
<% if (access.can_implement) { %><form method="post" action="/plugins/lumi_ai/improvement_center/corrections/save"><button class="button" type="submit">Save Corrections</button></form><% } %>
|
<label class="field"><span>Search feedback</span><input type="search" name="history_q" value="<%= historyQuery %>" placeholder="Question, answer, tag, note, or status" /></label>
|
||||||
</div>
|
<label class="field"><span>Outcome</span><select name="history_status"><option value="">All finished feedback</option><% [["implemented", "Applied"], ["rejected", "Dismissed"], ["reviewed", "No change needed"], ["archived", "Archived"]].forEach(([value, label]) => { %><option value="<%= value %>" <%= historyStatus === value ? "selected" : "" %>><%= label %></option><% }) %></select></label>
|
||||||
<div class="table-wrap">
|
<button class="button" type="submit">Search</button>
|
||||||
<table class="table">
|
<button class="button subtle" type="button" data-history-clear>Clear</button>
|
||||||
<thead><tr><th>Target</th><th>Prompt / answer</th><th>Permission</th><th>State</th><th>Actions</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<% corrections.entries.forEach((entry) => { %>
|
|
||||||
<tr>
|
|
||||||
<td><%= entry.target.replaceAll("_", " ") %></td>
|
|
||||||
<td><details><summary><%= entry.prompt.slice(0, 100) %></summary><pre><%= entry.corrected_answer %></pre></details></td>
|
|
||||||
<td><%= entry.min_role %> · <%= entry.permission_scope.origin %>/<%= entry.permission_scope.platform %></td>
|
|
||||||
<td><span class="ai-tag"><%= entry.active ? "active" : entry.enabled ? "staged" : "disabled" %></span></td>
|
|
||||||
<td class="improvement-actions">
|
|
||||||
<% if (access.can_verify) { %><form method="post" action="/plugins/lumi_ai/improvement_center/corrections/<%= entry.id %>"><input type="hidden" name="action" value="verify" /><button class="button subtle" type="submit">Verify</button></form><% } %>
|
|
||||||
<% if (entry.linked_okf_path) { %><a class="button subtle" href="/plugins/okf?q=<%= encodeURIComponent(entry.prompt.slice(0, 80)) %>">Open OKF</a><% } %>
|
|
||||||
<% if (access.can_edit) { %><form method="post" action="/plugins/lumi_ai/improvement_center/corrections/<%= entry.id %>"><input type="hidden" name="action" value="toggle" /><input type="hidden" name="enabled" value="<%= entry.enabled ? "off" : "on" %>" /><button class="button subtle" type="submit"><%= entry.enabled ? "Disable" : "Enable" %></button></form><button class="button subtle" type="button" data-open-dialog="correction-<%= entry.id %>">Edit</button><form method="post" action="/plugins/lumi_ai/improvement_center/corrections/<%= entry.id %>/delete" data-confirm-mode="modal" data-confirm-title="Delete correction" data-confirm-text="Delete this correction permanently?" data-confirm-label="Delete correction"><button class="button danger" type="submit">Delete</button></form><% } %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% if (access.can_edit) { %>
|
|
||||||
<dialog class="improvement-dialog" id="correction-<%= entry.id %>">
|
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/corrections/<%= entry.id %>" class="form-grid ai-form">
|
|
||||||
<input type="hidden" name="action" value="edit" />
|
|
||||||
<div class="field full"><label>Corrected answer</label><textarea name="corrected_answer" rows="8"><%= entry.corrected_answer %></textarea></div>
|
|
||||||
<div class="field"><label>Minimum role</label><select name="min_role"><% ["user", "mod", "admin"].forEach((role) => { %><option value="<%= role %>" <%= role === entry.min_role ? "selected" : "" %>><%= role %></option><% }) %></select></div>
|
|
||||||
<div class="field"><label>Verified expected link</label><input name="expected_link" value="<%= entry.expected_link %>" /></div>
|
|
||||||
<div class="field"><label>Origin scope</label><input name="permission_origin" value="<%= entry.permission_scope.origin %>" /></div>
|
|
||||||
<div class="field"><label>Platform scope</label><input name="permission_platform" value="<%= entry.permission_scope.platform %>" /></div>
|
|
||||||
<div class="field"><label>Route alias</label><input name="route_alias" value="<%= entry.route_alias %>" /></div>
|
|
||||||
<div class="field"><label><input type="checkbox" name="enabled" <%= entry.enabled ? "checked" : "" %> /> Enabled</label></div>
|
|
||||||
<div class="field"><label><input type="checkbox" name="explicitly_safe" <%= entry.explicitly_safe ? "checked" : "" %> /> Safe for predefined answer</label></div>
|
|
||||||
<div class="field full improvement-actions"><button class="button" type="submit">Stage changes</button><button class="button subtle" type="button" data-close-dialog>Cancel</button></div>
|
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
<div class="improvement-history-list">
|
||||||
<% } %>
|
<% historyReviews.entries.forEach((review) => { %>
|
||||||
|
<article class="improvement-card history-card">
|
||||||
|
<header><div class="improvement-card-labels"><strong><%= statusLabel(review.status) %></strong><span class="ai-tag status-<%= review.status %>"><%= statusLabel(review.status) %></span><span class="ai-tag"><%= review.feedback_tag.replaceAll("_", " ") %></span></div><span><%= formatDate(review.reviewed_at || review.timestamp) %></span></header>
|
||||||
|
<details>
|
||||||
|
<summary><%= review.user_message.slice(0, 180) %><%= review.user_message.length > 180 ? "…" : "" %></summary>
|
||||||
|
<div class="improvement-pair"><div><span>User asked</span><pre><%= review.user_message %></pre></div><div><span>Lumi answered</span><pre><%= review.assistant_answer %></pre></div></div>
|
||||||
|
<% if (review.optional_correction) { %><div class="improvement-correction"><strong>Suggested improvement</strong><pre><%= review.optional_correction %></pre></div><% } %>
|
||||||
|
<% if (review.implementation_summary) { %><div class="callout success"><strong>What was applied</strong><p><%= review.implementation_summary %></p></div><% } %>
|
||||||
|
<% if (review.review_notes) { %><p><strong>Reviewer note:</strong> <%= review.review_notes %></p><% } %>
|
||||||
|
<% if (review.linked_okf_correction) { %><p><a href="/plugins/okf?q=<%= encodeURIComponent(review.user_message.slice(0, 80)) %>">Open searchable knowledge entry</a></p><% } %>
|
||||||
|
</details>
|
||||||
|
<% if (access.can_edit) { %><div class="improvement-actions"><form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>" data-improvement-action><input type="hidden" name="action" value="restore" /><button class="button subtle" type="submit">Restore to queue</button></form><button class="button subtle" type="button" data-open-dialog="history-edit-<%= review.id %>">Edit details</button><form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>/delete" data-improvement-action data-confirm-mode="modal" data-confirm-title="Delete feedback record" data-confirm-text="Permanently delete this finished feedback record?" data-confirm-label="Delete feedback"><button class="button danger" type="submit">Delete permanently</button></form></div><% } %>
|
||||||
|
</article>
|
||||||
|
<% if (access.can_edit) { %><dialog class="improvement-dialog" id="history-edit-<%= review.id %>"><div class="improvement-dialog-heading"><h3>Edit finished feedback</h3><button class="icon-button" type="button" data-close-dialog aria-label="Close">×</button></div><form method="post" action="/plugins/lumi_ai/improvement_center/reviews/<%= review.id %>" class="form-grid ai-form" data-improvement-action><input type="hidden" name="action" value="edit" /><div class="field"><label>Problem type</label><select name="feedback_tag"><% feedbackTags.forEach((tag) => { %><option value="<%= tag %>" <%= tag === review.feedback_tag ? "selected" : "" %>><%= tag.replaceAll("_", " ") %></option><% }) %></select></div><input type="hidden" name="feedback_kind" value="<%= review.feedback_kind || "instruction_based" %>" /><div class="field full"><label>Suggested improvement</label><textarea name="optional_correction" rows="6"><%= review.optional_correction %></textarea></div><div class="field full"><label>Reviewer note</label><textarea name="review_notes" rows="3"><%= review.review_notes %></textarea></div><div class="field full improvement-actions"><button class="button" type="submit">Save details</button><button class="button subtle" type="button" data-close-dialog>Cancel</button></div></form></dialog><% } %>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% if (!corrections.entries.length) { %><tr><td colspan="5">No corrections have been promoted.</td></tr><% } %>
|
<% if (!historyReviews.entries.length) { %><div class="improvement-empty"><strong>No finished feedback found.</strong><p>Try a different search or status.</p></div><% } %>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="table-pagination">
|
<% if (historyReviews.pages > 1) { %><div class="table-pagination"><a class="button subtle <%= historyReviews.page <= 1 ? "disabled" : "" %>" href="?history_q=<%= encodeURIComponent(historyQuery) %>&history_status=<%= historyStatus %>&history_page=<%= Math.max(1, historyReviews.page - 1) %>" data-improvement-history-page>Previous</a><span class="table-page-label">Page <%= historyReviews.page %> of <%= historyReviews.pages %> (<%= historyReviews.total %> records)</span><a class="button subtle <%= historyReviews.page >= historyReviews.pages ? "disabled" : "" %>" href="?history_q=<%= encodeURIComponent(historyQuery) %>&history_status=<%= historyStatus %>&history_page=<%= Math.min(historyReviews.pages, historyReviews.page + 1) %>" data-improvement-history-page>Next</a></div><% } %>
|
||||||
<a class="button subtle <%= corrections.page <= 1 ? "disabled" : "" %>" href="?correction_page=<%= Math.max(1, corrections.page - 1) %>#corrections">Previous</a>
|
</dialog>
|
||||||
<span class="table-page-label">Page <%= corrections.page %> of <%= corrections.pages %> (<%= corrections.total %> corrections)</span>
|
|
||||||
<a class="button subtle <%= corrections.page >= corrections.pages ? "disabled" : "" %>" href="?correction_page=<%= Math.min(corrections.pages, corrections.page + 1) %>#corrections">Next</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="ai-band" id="evals">
|
<% if (access.can_approve) { %>
|
||||||
<div class="ai-section-heading">
|
<details class="ai-band improvement-advanced" id="advanced-tools">
|
||||||
<div><h2>Evals</h2><p>Stored cases can be run manually against the current Lumi AI configuration.</p></div>
|
<summary><span><strong>Advanced tools and access</strong><small>Correction bank, moderator access, evaluation cases, and training exports</small></span></summary>
|
||||||
<% if (access.can_run_evals) { %><form method="post" action="/plugins/lumi_ai/improvement_center/evals/run"><button class="button" type="submit">Run all evals</button></form><% } %>
|
<div class="improvement-advanced-body">
|
||||||
</div>
|
<section>
|
||||||
<% if (access.can_run_evals) { %>
|
<div class="ai-section-heading"><div><h2>Review access</h2><p>Choose whether selected moderators may help triage feedback.</p></div></div>
|
||||||
<details class="ai-settings-group"><summary>Add eval case</summary>
|
<form method="post" action="/plugins/lumi_ai/improvement_center/settings" class="form-grid ai-form" data-improvement-action>
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/evals" class="form-grid ai-form">
|
<div class="field"><label class="checkbox-inline"><input type="checkbox" name="allow_moderators_to_review_responses" <%= config.improvement.allow_moderators_to_review_responses ? "checked" : "" %> /><span>Allow moderators to review responses</span></label></div>
|
||||||
<div class="field full"><label>Prompt</label><textarea name="prompt" rows="3" required></textarea></div>
|
<div class="field"><label class="checkbox-inline"><input type="checkbox" name="corrections_enabled" <%= config.improvement.corrections_enabled ? "checked" : "" %> /><span>Use active reviewed corrections</span></label></div>
|
||||||
<div class="field"><label>Role</label><select name="role"><option>user</option><option>mod</option><option>admin</option></select></div>
|
<div class="field full"><label>Trusted moderator reviewer IDs</label><textarea name="trusted_moderator_reviewers" rows="2"><%= config.improvement.trusted_moderator_reviewers.join("\n") %></textarea></div>
|
||||||
<div class="field"><label>Origin</label><input name="origin" value="webui" /></div>
|
<div class="field full"><button class="button" type="submit">Save access</button></div>
|
||||||
<div class="field full"><label>Expected behavior</label><textarea name="expected_behavior" rows="3"></textarea></div>
|
|
||||||
<div class="field full"><label>Forbidden behavior</label><textarea name="forbidden_behavior" rows="3"></textarea></div>
|
|
||||||
<div class="field"><label>Expected verified link</label><input name="expected_link" /></div>
|
|
||||||
<div class="field"><label>Notes</label><input name="notes" /></div>
|
|
||||||
<div class="field full"><button class="button" type="submit">Add eval</button></div>
|
|
||||||
</form>
|
</form>
|
||||||
</details>
|
|
||||||
<% } %>
|
|
||||||
<div class="table-wrap"><table class="table"><thead><tr><th>Prompt</th><th>Role / origin</th><th>Expected</th><th>Forbidden</th><th>Expected link</th><th>Actions</th></tr></thead><tbody>
|
|
||||||
<% evalCases.entries.forEach((entry) => { %><tr><td><%= entry.prompt %></td><td><%= entry.role %> / <%= entry.origin %></td><td><%= entry.expected_behavior || "-" %></td><td><%= entry.forbidden_behavior || "-" %></td><td><%= entry.expected_link || "-" %></td><td><% if (access.can_run_evals) { %><form method="post" action="/plugins/lumi_ai/improvement_center/evals/<%= entry.id %>/delete" data-confirm-mode="modal" data-confirm-title="Delete eval case" data-confirm-text="Delete this eval case?" data-confirm-label="Delete eval"><button class="button danger" type="submit">Delete</button></form><% } %></td></tr><% }) %>
|
|
||||||
<% if (!evalCases.entries.length) { %><tr><td colspan="6">No eval cases.</td></tr><% } %>
|
|
||||||
</tbody></table></div>
|
|
||||||
<div class="table-pagination">
|
|
||||||
<a class="button subtle <%= evalCases.page <= 1 ? "disabled" : "" %>" href="?eval_page=<%= Math.max(1, evalCases.page - 1) %>#evals">Previous</a>
|
|
||||||
<span class="table-page-label">Page <%= evalCases.page %> of <%= evalCases.pages %> (<%= evalCases.total %> cases)</span>
|
|
||||||
<a class="button subtle <%= evalCases.page >= evalCases.pages ? "disabled" : "" %>" href="?eval_page=<%= Math.min(evalCases.pages, evalCases.page + 1) %>#evals">Next</a>
|
|
||||||
</div>
|
|
||||||
<details class="ai-settings-group"><summary>Recent eval results</summary>
|
|
||||||
<div class="table-wrap"><table class="table"><thead><tr><th>Time</th><th>Case</th><th>Result</th><th>Notes</th></tr></thead><tbody>
|
|
||||||
<% evalResults.forEach((result) => { %><tr><td><%= formatDate(result.run_at) %></td><td><%= result.case_id %></td><td><span class="ai-tag"><%= result.status %></span></td><td><%= result.notes || "-" %></td></tr><% }) %>
|
|
||||||
<% if (!evalResults.length) { %><tr><td colspan="4">No eval results.</td></tr><% } %>
|
|
||||||
</tbody></table></div>
|
|
||||||
</details>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<% if (access.can_export) { %>
|
<section id="corrections">
|
||||||
<section class="ai-band" id="exports">
|
<div class="ai-section-heading"><div><h2>Correction bank</h2><p>Normal “Apply improvement” actions are saved immediately. Use this table for manual maintenance.</p></div><form method="post" action="/plugins/lumi_ai/improvement_center/corrections/save" data-improvement-action><button class="button subtle" type="submit">Activate staged changes</button></form></div>
|
||||||
<div class="ai-section-heading"><div><h2>Training exports</h2><p>Manual JSONL exports include approved examples only. Lumi does not start training.</p></div></div>
|
<div class="table-wrap"><table class="table"><thead><tr><th>Type</th><th>Prompt / answer</th><th>Audience</th><th>State</th><th>Actions</th></tr></thead><tbody>
|
||||||
<div class="improvement-actions">
|
<% corrections.entries.forEach((entry) => { %><tr><td><%= entry.target.replaceAll("_", " ") %></td><td><details><summary><%= entry.prompt.slice(0, 100) %></summary><pre><%= entry.corrected_answer %></pre></details></td><td><%= entry.min_role %> · <%= entry.permission_scope.origin %>/<%= entry.permission_scope.platform %></td><td><span class="ai-tag"><%= entry.active ? "active" : entry.enabled ? "staged" : "disabled" %></span></td><td class="improvement-actions"><% if (entry.linked_okf_path) { %><a class="button subtle" href="/plugins/okf?q=<%= encodeURIComponent(entry.prompt.slice(0, 80)) %>">Open knowledge</a><% } %><form method="post" action="/plugins/lumi_ai/improvement_center/corrections/<%= entry.id %>" data-improvement-action><input type="hidden" name="action" value="toggle" /><input type="hidden" name="enabled" value="<%= entry.enabled ? "off" : "on" %>" /><button class="button subtle" type="submit"><%= entry.enabled ? "Disable" : "Enable" %></button></form><form method="post" action="/plugins/lumi_ai/improvement_center/corrections/<%= entry.id %>/delete" data-improvement-action data-confirm-mode="modal" data-confirm-title="Delete correction" data-confirm-text="Delete this correction permanently?" data-confirm-label="Delete correction"><button class="button danger" type="submit">Delete</button></form></td></tr><% }) %>
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/exports/instruction"><button class="button" type="submit">Export instruction JSONL</button></form>
|
<% if (!corrections.entries.length) { %><tr><td colspan="5">No corrections have been created.</td></tr><% } %>
|
||||||
<form method="post" action="/plugins/lumi_ai/improvement_center/exports/dpo"><button class="button subtle" type="submit">Export DPO JSONL</button></form>
|
</tbody></table></div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<script src="/plugins/lumi_ai/assets/improvement-center.js?v=0.6.0" defer></script>
|
<section id="evals">
|
||||||
|
<div class="ai-section-heading"><div><h2>Evaluation cases</h2><p>Optional technical tests for checking the current AI configuration.</p></div><form method="post" action="/plugins/lumi_ai/improvement_center/evals/run" data-improvement-action><button class="button subtle" type="submit">Run all tests</button></form></div>
|
||||||
|
<details class="ai-settings-group"><summary>Add a test case</summary><form method="post" action="/plugins/lumi_ai/improvement_center/evals" class="form-grid ai-form" data-improvement-action><div class="field full"><label>Prompt</label><textarea name="prompt" rows="3" required></textarea></div><div class="field"><label>Role</label><select name="role"><option>user</option><option>mod</option><option>admin</option></select></div><div class="field"><label>Origin</label><input name="origin" value="webui" /></div><div class="field full"><label>Expected behavior</label><textarea name="expected_behavior" rows="3"></textarea></div><div class="field full"><label>Forbidden behavior</label><textarea name="forbidden_behavior" rows="3"></textarea></div><div class="field"><label>Expected verified link</label><input name="expected_link" /></div><div class="field"><label>Notes</label><input name="notes" /></div><div class="field full"><button class="button" type="submit">Add test</button></div></form></details>
|
||||||
|
<div class="table-wrap"><table class="table"><thead><tr><th>Prompt</th><th>Role / origin</th><th>Expected</th><th>Must avoid</th><th>Actions</th></tr></thead><tbody><% evalCases.entries.forEach((entry) => { %><tr><td><%= entry.prompt %></td><td><%= entry.role %> / <%= entry.origin %></td><td><%= entry.expected_behavior || "-" %></td><td><%= entry.forbidden_behavior || "-" %></td><td><form method="post" action="/plugins/lumi_ai/improvement_center/evals/<%= entry.id %>/delete" data-improvement-action data-confirm-mode="modal" data-confirm-title="Delete eval case" data-confirm-text="Delete this evaluation case?" data-confirm-label="Delete test"><button class="button danger" type="submit">Delete</button></form></td></tr><% }) %><% if (!evalCases.entries.length) { %><tr><td colspan="5">No evaluation cases.</td></tr><% } %></tbody></table></div>
|
||||||
|
<details class="ai-settings-group"><summary>Recent test results</summary><div class="table-wrap"><table class="table"><thead><tr><th>Time</th><th>Case</th><th>Result</th><th>Notes</th></tr></thead><tbody><% evalResults.forEach((result) => { %><tr><td><%= formatDate(result.run_at) %></td><td><%= result.case_id %></td><td><span class="ai-tag"><%= result.status %></span></td><td><%= result.notes || "-" %></td></tr><% }) %><% if (!evalResults.length) { %><tr><td colspan="4">No test results.</td></tr><% } %></tbody></table></div></details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<% if (access.can_export) { %><section id="exports"><div class="ai-section-heading"><div><h2>Training exports</h2><p>Manual exports include only approved examples. Lumi does not start training.</p></div></div><div class="improvement-actions"><form method="post" action="/plugins/lumi_ai/improvement_center/exports/instruction"><button class="button subtle" type="submit">Export instruction data</button></form><form method="post" action="/plugins/lumi_ai/improvement_center/exports/dpo"><button class="button subtle" type="submit">Export preference data</button></form></div></section><% } %>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/plugins/lumi_ai/assets/improvement-center.js?v=0.8.3" defer></script>
|
||||||
<%- include("../../../src/web/views/partials/layout-bottom") %>
|
<%- include("../../../src/web/views/partials/layout-bottom") %>
|
||||||
|
|||||||
@ -2,6 +2,36 @@
|
|||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"releases": [
|
"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",
|
"version": "0.2.6",
|
||||||
"ref": "refs/tags/v0.2.6",
|
"ref": "refs/tags/v0.2.6",
|
||||||
|
|||||||
@ -4,15 +4,15 @@ const path = require("path");
|
|||||||
const { findSafeTarget } = require("../src/services/versioning");
|
const { findSafeTarget } = require("../src/services/versioning");
|
||||||
|
|
||||||
const root = path.join(__dirname, "..");
|
const root = path.join(__dirname, "..");
|
||||||
const releaseVersion = "0.2.6";
|
const releaseVersion = "0.2.7";
|
||||||
const previousCoreVersion = "0.2.5";
|
const previousCoreVersion = "0.2.6";
|
||||||
const earliestCompatibleCoreVersion = "0.1.9";
|
const earliestCompatibleCoreVersion = "0.1.9";
|
||||||
const changedPlugins = {
|
const changedPlugins = {
|
||||||
"auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" },
|
"auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" },
|
||||||
birthday: { from: "0.1.2", to: "0.1.3", knowledge: "birthday" },
|
birthday: { from: "0.1.2", to: "0.1.3", knowledge: "birthday" },
|
||||||
"economy-framework": { from: "0.2.9", to: "0.2.10", knowledge: "economy-framework" },
|
"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" },
|
"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" },
|
moderation: { from: "0.1.4", to: "0.1.5", knowledge: "moderation" },
|
||||||
okf: { from: "0.1.0", to: "0.1.1", knowledge: "okf" },
|
okf: { from: "0.1.0", to: "0.1.1", knowledge: "okf" },
|
||||||
quotes: { from: "0.1.1", to: "0.1.2", knowledge: "quotes" },
|
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");
|
const webSearch = readJson("plugins/lumi_ai_web_search/tool_info.json");
|
||||||
assert.equal(webSearch.version, "0.1.1");
|
assert.equal(webSearch.version, "0.1.1");
|
||||||
assert.equal(webSearch.minimum_lumi_version, "0.2.0");
|
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);
|
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.");
|
||||||
|
|||||||
@ -16,7 +16,7 @@ function readJson(relativePath) {
|
|||||||
|
|
||||||
const releaseIndex = readJson("release-index.json");
|
const releaseIndex = readJson("release-index.json");
|
||||||
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
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");
|
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
|
||||||
for (const release of releaseIndex.releases) {
|
for (const release of releaseIndex.releases) {
|
||||||
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
||||||
@ -37,6 +37,7 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) {
|
|||||||
const baseTarget = {
|
const baseTarget = {
|
||||||
current_version: "0.2.4",
|
current_version: "0.2.4",
|
||||||
available_versions: [
|
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.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.5", ref: "refs/tags/v0.2.5", rollback_safe: true },
|
||||||
{ version: "0.2.4", ref: "refs/tags/v0.2.4", 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"
|
channel: "stable"
|
||||||
});
|
});
|
||||||
assert.equal(corrected.version_correction, true);
|
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.update_available, true);
|
||||||
assert.equal(corrected.blocked, false);
|
assert.equal(corrected.blocked, false);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Lumi Core",
|
"name": "Lumi Core",
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"released_at": "2026-07-18",
|
"released_at": "2026-07-18",
|
||||||
"compatible_from": "0.1.9",
|
"compatible_from": "0.1.9",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"replaces_versions": [
|
"replaces_versions": [
|
||||||
"1.2.0"
|
"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,
|
"rollback_safe": true,
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"Node.js 18 or newer"
|
"Node.js 18 or newer"
|
||||||
@ -97,6 +97,18 @@
|
|||||||
],
|
],
|
||||||
"rollback_safe": true,
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user