nodebb-plugin-search-agent 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +22 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/commitlint.config.js +26 -0
- package/eslint.config.mjs +10 -0
- package/languages/de/search-agent.json +15 -0
- package/languages/en-GB/search-agent.json +15 -0
- package/languages/en-US/search-agent.json +15 -0
- package/languages/he/search-agent.json +15 -0
- package/lib/controllers.js +69 -0
- package/lib/searchHandler.js +225 -0
- package/lib/similarity.js +125 -0
- package/library.js +98 -0
- package/nodebb-plugin-search-agent-0.0.1.tgz +0 -0
- package/package.json +51 -0
- package/plugin.json +29 -0
- package/plugins/quickstart/partials/sorted-list/form.tpl +10 -0
- package/plugins/quickstart/partials/sorted-list/item.tpl +12 -0
- package/plugins/search-agent.tpl +93 -0
- package/public/lib/acp-main.js +38 -0
- package/public/lib/admin.js +26 -0
- package/public/lib/main.js +281 -0
- package/renovate.json +5 -0
- package/scss/search-agent.scss +374 -0
- package/static/samplefile.html +5 -0
- package/templates/admin/plugins/search-agent.tpl +186 -0
- package/test/.eslintrc +9 -0
- package/test/index.js +128 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Common stop-words (English + Hebrew) to exclude from TF-IDF vectors
|
|
4
|
+
const STOP_WORDS = new Set([
|
|
5
|
+
// English
|
|
6
|
+
'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
|
7
|
+
'of', 'with', 'by', 'from', 'is', 'it', 'its', 'be', 'as', 'was',
|
|
8
|
+
'are', 'were', 'been', 'have', 'has', 'had', 'do', 'does', 'did',
|
|
9
|
+
'will', 'would', 'could', 'should', 'may', 'might', 'can', 'shall',
|
|
10
|
+
'not', 'no', 'so', 'if', 'this', 'that', 'these', 'those', 'i', 'we',
|
|
11
|
+
'you', 'he', 'she', 'they', 'my', 'your', 'his', 'her', 'our', 'their',
|
|
12
|
+
'how', 'what', 'when', 'where', 'why', 'who', 'which', 'am', 'up',
|
|
13
|
+
'out', 'about', 'into', 'than', 'more', 'also', 'me', 'him', 'us', 'them',
|
|
14
|
+
// Hebrew
|
|
15
|
+
'של', 'את', 'אל', 'על', 'עם', 'הם', 'הן', 'זה', 'זו', 'זאת',
|
|
16
|
+
'כי', 'לא', 'כן', 'יש', 'אם', 'רק', 'גם', 'אבל', 'אנחנו', 'אני',
|
|
17
|
+
'אתה', 'את', 'הוא', 'היא', 'אנו', 'אתם', 'אתן', 'הם', 'הן',
|
|
18
|
+
'זה', 'זו', 'אלה', 'אלו', 'כל', 'כך', 'כבר', 'עוד', 'רק', 'כן',
|
|
19
|
+
'אחד', 'יותר', 'פה', 'שם', 'מה', 'מי', 'איך', 'מתי', 'איפה',
|
|
20
|
+
'היה', 'הייתה', 'יהיה', 'תהיה', 'הוא', 'היא', 'הם', 'הן',
|
|
21
|
+
'אסור', 'מותר', 'צריך', 'רוצה', 'יכול', 'יכולה', 'לו', 'לה',
|
|
22
|
+
'בו', 'בה', 'עליו', 'עליה', 'בין', 'כבר', 'עכשיו', 'היום',
|
|
23
|
+
'כן', 'לכן', 'כדי', 'כדאי', 'שלי', 'שלך', 'שלו', 'שלה',
|
|
24
|
+
'שלנו', 'שלכם', 'שלהם', 'שלהן', 'להם', 'להן', 'לנו', 'לכם',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Strip HTML, lowercase, remove punctuation, drop stop-words and short tokens.
|
|
29
|
+
* @param {string|null|undefined} text
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
function tokenize(text) {
|
|
33
|
+
if (!text) return [];
|
|
34
|
+
return text
|
|
35
|
+
.replace(/<[^>]*>/g, ' ') // strip HTML tags
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^\p{L}\p{N}\s]/gu, ' ') // keep all Unicode letters & digits (Hebrew, Latin, etc.)
|
|
38
|
+
.split(/\s+/)
|
|
39
|
+
.filter(t => t.length >= 2 && !STOP_WORDS.has(t)); // min 2 chars to keep short Hebrew words
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a TF-IDF index from an array of topic objects.
|
|
44
|
+
* Each topic must have: tid, slug, title, mainPostContent (optional).
|
|
45
|
+
* @param {{ tid: number|string, slug: string, title: string, mainPostContent?: string }[]} topics
|
|
46
|
+
* @returns {{ tid: number|string, slug: string, vector: Map<string, number> }[]}
|
|
47
|
+
*/
|
|
48
|
+
function buildIndex(topics) {
|
|
49
|
+
if (!topics || topics.length === 0) return [];
|
|
50
|
+
|
|
51
|
+
// Step 1: term frequencies per document
|
|
52
|
+
const docs = topics.map((t) => {
|
|
53
|
+
const tokens = tokenize(`${t.title || ''} ${t.mainPostContent || ''}`);
|
|
54
|
+
const tf = new Map();
|
|
55
|
+
for (const token of tokens) {
|
|
56
|
+
tf.set(token, (tf.get(token) || 0) + 1);
|
|
57
|
+
}
|
|
58
|
+
return { tid: t.tid, slug: t.slug, tf, len: tokens.length };
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Step 2: document frequency (how many docs contain each term)
|
|
62
|
+
const df = new Map();
|
|
63
|
+
for (const doc of docs) {
|
|
64
|
+
for (const term of doc.tf.keys()) {
|
|
65
|
+
df.set(term, (df.get(term) || 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const N = docs.length;
|
|
70
|
+
|
|
71
|
+
// Step 3: compute TF-IDF vector per document
|
|
72
|
+
return docs.map((doc) => {
|
|
73
|
+
const vector = new Map();
|
|
74
|
+
for (const [term, freq] of doc.tf) {
|
|
75
|
+
const tf = doc.len > 0 ? freq / doc.len : 0;
|
|
76
|
+
// Smoothed IDF to avoid division by zero
|
|
77
|
+
const idf = Math.log((N + 1) / (df.get(term) + 1)) + 1;
|
|
78
|
+
vector.set(term, tf * idf);
|
|
79
|
+
}
|
|
80
|
+
return { tid: doc.tid, slug: doc.slug, vector };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rank indexed documents against a query using cosine similarity.
|
|
86
|
+
* @param {string} queryText
|
|
87
|
+
* @param {{ tid: number|string, slug: string, vector: Map<string, number> }[]} index
|
|
88
|
+
* @param {number} [topN=10]
|
|
89
|
+
* @returns {{ tid: number|string, slug: string, score: number }[]}
|
|
90
|
+
*/
|
|
91
|
+
function query(queryText, index, topN = 10) {
|
|
92
|
+
if (!index || index.length === 0) return [];
|
|
93
|
+
|
|
94
|
+
const qTokens = tokenize(queryText);
|
|
95
|
+
if (qTokens.length === 0) return [];
|
|
96
|
+
|
|
97
|
+
// Build raw term-count vector for the query
|
|
98
|
+
const qVec = new Map();
|
|
99
|
+
for (const token of qTokens) {
|
|
100
|
+
qVec.set(token, (qVec.get(token) || 0) + 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const doc of index) {
|
|
105
|
+
let dot = 0;
|
|
106
|
+
let docNormSq = 0;
|
|
107
|
+
let qNormSq = 0;
|
|
108
|
+
|
|
109
|
+
for (const [term, qVal] of qVec) {
|
|
110
|
+
dot += qVal * (doc.vector.get(term) || 0);
|
|
111
|
+
}
|
|
112
|
+
for (const val of doc.vector.values()) docNormSq += val * val;
|
|
113
|
+
for (const val of qVec.values()) qNormSq += val * val;
|
|
114
|
+
|
|
115
|
+
const norm = Math.sqrt(docNormSq) * Math.sqrt(qNormSq);
|
|
116
|
+
const score = norm > 0 ? dot / norm : 0;
|
|
117
|
+
if (score > 0) {
|
|
118
|
+
results.push({ tid: doc.tid, slug: doc.slug, score });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return results.sort((a, b) => b.score - a.score).slice(0, topN);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { tokenize, buildIndex, query };
|
package/library.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const winston = require.main.require('winston');
|
|
4
|
+
|
|
5
|
+
const controllers = require('./lib/controllers');
|
|
6
|
+
const { invalidateCache } = require('./lib/searchHandler');
|
|
7
|
+
|
|
8
|
+
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
9
|
+
|
|
10
|
+
const plugin = {};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* static:app.load
|
|
14
|
+
* Register admin page route and set up cache-invalidation on new topic creation.
|
|
15
|
+
*/
|
|
16
|
+
plugin.init = async (params) => {
|
|
17
|
+
console.log('[search-agent] plugin.init called');
|
|
18
|
+
const { router } = params;
|
|
19
|
+
|
|
20
|
+
// Admin settings page
|
|
21
|
+
routeHelpers.setupAdminPageRoute(
|
|
22
|
+
router,
|
|
23
|
+
'/admin/plugins/search-agent',
|
|
24
|
+
controllers.renderAdminPage
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
console.log('[search-agent] Admin route registered: /admin/plugins/search-agent');
|
|
28
|
+
winston.info('[plugins/search-agent] Initialised.');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* static:api.routes
|
|
33
|
+
* Register the REST endpoint consumed by the floating chat widget.
|
|
34
|
+
*
|
|
35
|
+
* POST /api/v3/plugins/search-agent/query
|
|
36
|
+
* Body: { "query": "how do I reset my password?" }
|
|
37
|
+
* Returns a ranked list of matching forum topics.
|
|
38
|
+
*
|
|
39
|
+
* Authentication: open to all logged-in users (uses ensureLoggedIn).
|
|
40
|
+
* To allow guests as well, remove the middleware array below.
|
|
41
|
+
*/
|
|
42
|
+
plugin.addRoutes = async ({ router, middleware, helpers }) => {
|
|
43
|
+
console.log('[search-agent] plugin.addRoutes called — registering API routes');
|
|
44
|
+
const middlewares = [
|
|
45
|
+
middleware.ensureLoggedIn,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
routeHelpers.setupApiRoute(
|
|
49
|
+
router,
|
|
50
|
+
'post',
|
|
51
|
+
'/search-agent/query',
|
|
52
|
+
middlewares,
|
|
53
|
+
(req, res) => controllers.handleQuery(req, res, helpers)
|
|
54
|
+
);
|
|
55
|
+
console.log('[search-agent] API route registered: POST /api/v3/plugins/search-agent/query');
|
|
56
|
+
|
|
57
|
+
// Public config endpoint — no auth required so guests can read the visibility setting
|
|
58
|
+
routeHelpers.setupApiRoute(
|
|
59
|
+
router,
|
|
60
|
+
'get',
|
|
61
|
+
'/search-agent/config',
|
|
62
|
+
[],
|
|
63
|
+
(req, res) => controllers.getConfig(req, res, helpers)
|
|
64
|
+
);
|
|
65
|
+
console.log('[search-agent] API route registered: GET /api/v3/plugins/search-agent/config');
|
|
66
|
+
|
|
67
|
+
// Lightweight cache-bust endpoint (admin only)
|
|
68
|
+
routeHelpers.setupApiRoute(
|
|
69
|
+
router,
|
|
70
|
+
'post',
|
|
71
|
+
'/search-agent/cache/invalidate',
|
|
72
|
+
[middleware.ensureLoggedIn, middleware.admin.checkPrivileges],
|
|
73
|
+
(req, res) => {
|
|
74
|
+
console.log('[search-agent] Cache invalidation requested by uid:', req.uid);
|
|
75
|
+
invalidateCache();
|
|
76
|
+
helpers.formatApiResponse(200, res, { message: 'Cache invalidated.' });
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
console.log('[search-agent] API route registered: POST /api/v3/plugins/search-agent/cache/invalidate');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* filter:admin.header.build
|
|
84
|
+
* Add the plugin entry to the ACP sidebar.
|
|
85
|
+
*/
|
|
86
|
+
plugin.addAdminNavigation = (header) => {
|
|
87
|
+
console.log('[search-agent] plugin.addAdminNavigation called — adding ACP sidebar entry');
|
|
88
|
+
header.plugins.push({
|
|
89
|
+
route: '/plugins/search-agent',
|
|
90
|
+
icon: 'fa-comments',
|
|
91
|
+
name: 'Search Agent',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return header;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
module.exports = plugin;
|
|
98
|
+
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodebb-plugin-search-agent",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "NodeBB plugin that adds a floating chat assistant to help users find relevant forum topics using TF-IDF text similarity",
|
|
5
|
+
"main": "library.js",
|
|
6
|
+
"author": "Racheli Bayfus",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/racheliK9201/nodebb-plugin-search-agent.git"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"lint": "eslint ."
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"nodebb",
|
|
16
|
+
"plugin",
|
|
17
|
+
"search",
|
|
18
|
+
"chat",
|
|
19
|
+
"assistant",
|
|
20
|
+
"tfidf",
|
|
21
|
+
"similarity"
|
|
22
|
+
],
|
|
23
|
+
"husky": {
|
|
24
|
+
"hooks": {
|
|
25
|
+
"pre-commit": "lint-staged",
|
|
26
|
+
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"lint-staged": {
|
|
30
|
+
"*.js": [
|
|
31
|
+
"eslint --fix",
|
|
32
|
+
"git add"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/nodebb/nodebb-plugin-search-agent/issues"
|
|
38
|
+
},
|
|
39
|
+
"readmeFilename": "README.md",
|
|
40
|
+
"nbbpm": {
|
|
41
|
+
"compatibility": "^3.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@commitlint/cli": "20.5.0",
|
|
45
|
+
"@commitlint/config-angular": "20.5.0",
|
|
46
|
+
"eslint": "10.0.3",
|
|
47
|
+
"eslint-config-nodebb": "2.0.1",
|
|
48
|
+
"husky": "9.1.7",
|
|
49
|
+
"lint-staged": "16.4.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-search-agent",
|
|
3
|
+
"name": "Search Agent",
|
|
4
|
+
"description": "Floating chat assistant that finds relevant forum topics using TF-IDF similarity",
|
|
5
|
+
"url": "https://github.com/NodeBB/nodebb-plugin-search-agent",
|
|
6
|
+
"library": "./library.js",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
9
|
+
{ "hook": "static:api.routes", "method": "addRoutes" },
|
|
10
|
+
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" }
|
|
11
|
+
],
|
|
12
|
+
"staticDirs": {
|
|
13
|
+
"static": "./static"
|
|
14
|
+
},
|
|
15
|
+
"scss": [
|
|
16
|
+
"scss/search-agent.scss"
|
|
17
|
+
],
|
|
18
|
+
"scripts": [
|
|
19
|
+
"public/lib/main.js"
|
|
20
|
+
],
|
|
21
|
+
"acpScripts": [
|
|
22
|
+
"public/lib/acp-main.js"
|
|
23
|
+
],
|
|
24
|
+
"modules": {
|
|
25
|
+
"../admin/plugins/search-agent.js": "./public/lib/admin.js"
|
|
26
|
+
},
|
|
27
|
+
"templates": "templates",
|
|
28
|
+
"languages": "languages"
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<form>
|
|
2
|
+
<div class="mb-3">
|
|
3
|
+
<label class="form-label" for="name">Name</label>
|
|
4
|
+
<input type="text" id="name" name="name" class="form-control" placeholder="Name" />
|
|
5
|
+
</div>
|
|
6
|
+
<div class="mb-3">
|
|
7
|
+
<label class="form-label" for="description">Description</label>
|
|
8
|
+
<input type="text" id="description" name="description" class="form-control" placeholder="Description" />
|
|
9
|
+
</div>
|
|
10
|
+
</form>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<li data-type="item" class="list-group-item">
|
|
2
|
+
<div class="d-flex gap-2 justify-content-between align-items-start">
|
|
3
|
+
<div class="flex-grow-1">
|
|
4
|
+
<strong>{name}</strong><br />
|
|
5
|
+
<small>{description}</small>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="d-flex gap-1 flex-nowrap">
|
|
8
|
+
<button type="button" data-type="edit" class="btn btn-sm btn-info">Edit</button>
|
|
9
|
+
<button type="button" data-type="remove" class="btn btn-sm btn-danger">Delete</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</li>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<div class="acp-page-container">
|
|
2
|
+
<!-- IMPORT admin/partials/settings/header.tpl -->
|
|
3
|
+
|
|
4
|
+
<div class="row m-0">
|
|
5
|
+
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
|
|
6
|
+
<form role="form" class="search-agent-settings">
|
|
7
|
+
|
|
8
|
+
<!-- ── General ─────────────────────────────────────────────── -->
|
|
9
|
+
<div class="mb-4">
|
|
10
|
+
<h5 class="fw-bold tracking-tight settings-header">General</h5>
|
|
11
|
+
<p class="lead">
|
|
12
|
+
Configure the Search Agent plugin behaviour.
|
|
13
|
+
Settings are persisted via <code>meta.settings.get('search-agent')</code>.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<div class="mb-3">
|
|
17
|
+
<label class="form-label" for="topicLimit">
|
|
18
|
+
Topics indexed (max topics fetched for similarity search)
|
|
19
|
+
</label>
|
|
20
|
+
<input
|
|
21
|
+
type="number"
|
|
22
|
+
min="50"
|
|
23
|
+
max="5000"
|
|
24
|
+
id="topicLimit"
|
|
25
|
+
name="topicLimit"
|
|
26
|
+
class="form-control"
|
|
27
|
+
placeholder="500"
|
|
28
|
+
value="500"
|
|
29
|
+
/>
|
|
30
|
+
<div class="form-text">
|
|
31
|
+
Higher values improve recall but increase memory usage.
|
|
32
|
+
The index is rebuilt every 5 minutes.
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="mb-3">
|
|
37
|
+
<label class="form-label" for="maxResults">
|
|
38
|
+
Max results returned per query
|
|
39
|
+
</label>
|
|
40
|
+
<input
|
|
41
|
+
type="number"
|
|
42
|
+
min="1"
|
|
43
|
+
max="20"
|
|
44
|
+
id="maxResults"
|
|
45
|
+
name="maxResults"
|
|
46
|
+
class="form-control"
|
|
47
|
+
placeholder="10"
|
|
48
|
+
value="10"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="form-check form-switch mb-3">
|
|
53
|
+
<input type="checkbox" class="form-check-input" id="guestsAllowed" name="guestsAllowed">
|
|
54
|
+
<label for="guestsAllowed" class="form-check-label">
|
|
55
|
+
Allow guests (non-logged-in users) to use the chat widget
|
|
56
|
+
</label>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- ── Appearance ──────────────────────────────────────────── -->
|
|
61
|
+
<div class="mb-4">
|
|
62
|
+
<h5 class="fw-bold tracking-tight settings-header">Appearance</h5>
|
|
63
|
+
|
|
64
|
+
<div class="mb-3 d-flex gap-2 align-items-center">
|
|
65
|
+
<label class="form-label mb-0" for="primaryColor">Primary colour</label>
|
|
66
|
+
<input
|
|
67
|
+
data-settings="colorpicker"
|
|
68
|
+
type="color"
|
|
69
|
+
id="primaryColor"
|
|
70
|
+
name="primaryColor"
|
|
71
|
+
class="form-control p-1"
|
|
72
|
+
value="#0f7ee8"
|
|
73
|
+
style="width:64px;"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
</form>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- ── Sticky save / table of contents ─────────────────────────────── -->
|
|
82
|
+
<div class="col-12 col-md-4 px-0 mb-4 acp-sidebar">
|
|
83
|
+
<div class="card sticky-top">
|
|
84
|
+
<div class="card-header">Save Changes</div>
|
|
85
|
+
<div class="card-body">
|
|
86
|
+
<button class="btn btn-primary btn-sm fw-semibold" data-action="save">
|
|
87
|
+
Save Settings
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
$(document).ready(function () {
|
|
4
|
+
/*
|
|
5
|
+
* Admin CP frontend for nodebb-plugin-search-agent.
|
|
6
|
+
* Handles saving settings via NodeBB's standard ACP settings API.
|
|
7
|
+
*/
|
|
8
|
+
console.log('[search-agent] acp-main.js: DOM ready');
|
|
9
|
+
|
|
10
|
+
$(window).on('action:ajaxify.end', function (ev, data) {
|
|
11
|
+
console.log('[search-agent] acp-main.js: ajaxify.end — url:', data.url);
|
|
12
|
+
if (data.url === 'admin/plugins/search-agent') {
|
|
13
|
+
console.log('[search-agent] acp-main.js: search-agent admin page detected — running setupAdminPage()');
|
|
14
|
+
setupAdminPage();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function setupAdminPage() {
|
|
19
|
+
console.log('[search-agent] setupAdminPage: binding save button and loading current settings');
|
|
20
|
+
// NodeBB's settings module is auto-loaded for the ACP.
|
|
21
|
+
// The [data-action="save"] button triggers NodeBB's built-in settings save.
|
|
22
|
+
$('[data-action="save"]').on('click', function () {
|
|
23
|
+
console.log('[search-agent] setupAdminPage: save button clicked');
|
|
24
|
+
require(['settings'], function (Settings) {
|
|
25
|
+
Settings.save('search-agent', $('form.search-agent-settings'), function () {
|
|
26
|
+
console.log('[search-agent] setupAdminPage: settings saved successfully');
|
|
27
|
+
app.alertSuccess('Settings saved!');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
require(['settings'], function (Settings) {
|
|
33
|
+
console.log('[search-agent] setupAdminPage: loading settings into form');
|
|
34
|
+
Settings.load('search-agent', $('form.search-agent-settings'));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This module is loaded when the user navigates to /admin/plugins/search-agent.
|
|
5
|
+
It handles loading and saving plugin settings via NodeBB's settings module.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { save, load } from 'settings';
|
|
9
|
+
|
|
10
|
+
export function init() {
|
|
11
|
+
console.log('[search-agent] admin.js: init() called');
|
|
12
|
+
handleSettingsForm();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function handleSettingsForm() {
|
|
16
|
+
console.log('[search-agent] admin.js: handleSettingsForm() — loading settings into form');
|
|
17
|
+
// Load current settings into the form fields
|
|
18
|
+
load('search-agent', $('.search-agent-settings'));
|
|
19
|
+
|
|
20
|
+
// Save settings when the button is clicked
|
|
21
|
+
$('[data-action="save"]').on('click', () => {
|
|
22
|
+
console.log('[search-agent] admin.js: save button clicked — persisting settings');
|
|
23
|
+
save('search-agent', $('.search-agent-settings'));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|