termsearch 0.3.1 → 0.3.2
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/README.md +8 -3
- package/frontend/dist/app.js +212 -12
- package/frontend/dist/style.css +126 -3
- package/package.json +1 -1
- package/src/api/routes.js +112 -20
- package/src/search/engine.js +404 -73
- package/src/search/providers/github.js +91 -0
- package/src/search/providers/searxng.js +15 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TermSearch - Personal Search Engine
|
|
2
2
|
|
|
3
|
-
[](#project-status)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](https://termux.dev)
|
|
@@ -12,7 +12,9 @@ Zero external dependencies, no Docker, no Python. AI is optional and configured
|
|
|
12
12
|
Core capabilities:
|
|
13
13
|
|
|
14
14
|
- Zero-config search via DuckDuckGo and Wikipedia — works immediately after install
|
|
15
|
+
- Built-in GitHub Search API fallback (`github-api`) and selectable engine mix from UI
|
|
15
16
|
- Progressive enhancement: add Brave/Mojeek API keys, AI endpoints, or SearXNG when needed
|
|
17
|
+
- Search history persistence (opt-in toggle in Settings)
|
|
16
18
|
- Social profile scanner: GitHub, Bluesky, Reddit, Twitter/X, Instagram, YouTube, LinkedIn, TikTok, Telegram, Facebook
|
|
17
19
|
- Torrent search: The Pirate Bay + 1337x with direct magnet extraction
|
|
18
20
|
- Social search: Bluesky posts/actors + GDELT news
|
|
@@ -22,7 +24,7 @@ Core capabilities:
|
|
|
22
24
|
|
|
23
25
|
## Project Status
|
|
24
26
|
|
|
25
|
-
- Current line: `0.3.
|
|
27
|
+
- Current line: `0.3.2`
|
|
26
28
|
- Core is MIT — zero required API keys
|
|
27
29
|
- AI features are optional, configured via Settings page in browser
|
|
28
30
|
- Tested on: Ubuntu 24.04, Termux (Android 15/16)
|
|
@@ -89,8 +91,11 @@ Configure at **Settings → AI** in the browser. Supported endpoints:
|
|
|
89
91
|
|----------|----------|-------|-----|
|
|
90
92
|
| **Localhost** (Ollama) | `http://localhost:11434/v1` | `qwen3.5:4b` or any | not required |
|
|
91
93
|
| **Localhost** (LM Studio) | `http://localhost:1234/v1` | your loaded model | not required |
|
|
94
|
+
| **Localhost** (llama.cpp) | `http://localhost:8080/v1` | your loaded model | not required |
|
|
92
95
|
| **Chutes.ai TEE** | `https://llm.chutes.ai/v1` | `deepseek-ai/DeepSeek-V3.2-TEE` | required |
|
|
96
|
+
| **Anthropic** | `https://api.anthropic.com/v1` | `claude-3-5-haiku-latest` | required |
|
|
93
97
|
| **OpenAI** | `https://api.openai.com/v1` | `gpt-4o-mini` | required |
|
|
98
|
+
| **OpenRouter** | `https://openrouter.ai/api/v1` | any listed model | required |
|
|
94
99
|
| **API custom** | any OpenAI-compatible URL | your model | optional |
|
|
95
100
|
|
|
96
101
|
All providers use the OpenAI-compatible `/chat/completions` format. Leave API key empty for local models.
|
|
@@ -107,7 +112,7 @@ All providers use the OpenAI-compatible `/chat/completions` format. Leave API ke
|
|
|
107
112
|
src/
|
|
108
113
|
config/ config manager — load/save/defaults/env overrides
|
|
109
114
|
search/
|
|
110
|
-
providers/ DuckDuckGo, Wikipedia, Brave, Mojeek, SearXNG
|
|
115
|
+
providers/ DuckDuckGo, Wikipedia, Brave, Mojeek, SearXNG, GitHub API
|
|
111
116
|
engine.js fan-out, merge, rank, cache
|
|
112
117
|
ranking.js source diversity ranking
|
|
113
118
|
cache.js tiered cache (L1 Map + L2 disk JSON)
|
package/frontend/dist/app.js
CHANGED
|
@@ -17,6 +17,14 @@ const state = {
|
|
|
17
17
|
providers: [],
|
|
18
18
|
config: null,
|
|
19
19
|
historyEnabled: localStorage.getItem('ts-save-history') !== '0',
|
|
20
|
+
selectedEngines: (() => {
|
|
21
|
+
try {
|
|
22
|
+
const raw = JSON.parse(localStorage.getItem('ts-engines') || '[]');
|
|
23
|
+
return Array.isArray(raw) ? raw.slice(0, 20).map((v) => String(v || '').trim().toLowerCase()).filter(Boolean) : [];
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
})(),
|
|
20
28
|
searchHistory: (() => {
|
|
21
29
|
try {
|
|
22
30
|
const raw = JSON.parse(localStorage.getItem('ts-history') || '[]');
|
|
@@ -95,6 +103,43 @@ function addSearchToHistory(query) {
|
|
|
95
103
|
persistHistory();
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
const LANG_CANONICAL = new Map([
|
|
107
|
+
['it', 'it-IT'], ['it-it', 'it-IT'],
|
|
108
|
+
['en', 'en-US'], ['en-us', 'en-US'],
|
|
109
|
+
['es', 'es-ES'], ['es-es', 'es-ES'],
|
|
110
|
+
['fr', 'fr-FR'], ['fr-fr', 'fr-FR'],
|
|
111
|
+
['de', 'de-DE'], ['de-de', 'de-DE'],
|
|
112
|
+
['pt', 'pt-PT'], ['pt-pt', 'pt-PT'],
|
|
113
|
+
['ru', 'ru-RU'], ['ru-ru', 'ru-RU'],
|
|
114
|
+
['zh', 'zh-CN'], ['zh-cn', 'zh-CN'],
|
|
115
|
+
['ja', 'ja-JP'], ['ja-jp', 'ja-JP'],
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
function normalizeLangCode(raw) {
|
|
119
|
+
const key = String(raw || '').trim().toLowerCase();
|
|
120
|
+
return LANG_CANONICAL.get(key) || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getResolvedLang() {
|
|
124
|
+
const selected = getLang();
|
|
125
|
+
if (selected && selected !== 'auto') return selected;
|
|
126
|
+
const browser = normalizeLangCode(navigator.language || navigator.languages?.[0] || '');
|
|
127
|
+
return browser || 'en-US';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function persistSelectedEngines() {
|
|
131
|
+
localStorage.setItem('ts-engines', JSON.stringify(state.selectedEngines.slice(0, 20)));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function setSelectedEngines(engines) {
|
|
135
|
+
state.selectedEngines = [...new Set(
|
|
136
|
+
(Array.isArray(engines) ? engines : [])
|
|
137
|
+
.map((engine) => String(engine || '').trim().toLowerCase())
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
)].slice(0, 20);
|
|
140
|
+
persistSelectedEngines();
|
|
141
|
+
}
|
|
142
|
+
|
|
98
143
|
// ─── SVG Icons ────────────────────────────────────────────────────────────
|
|
99
144
|
function svg(paths, size = 16, extra = '') {
|
|
100
145
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ${extra}>${paths}</svg>`;
|
|
@@ -164,6 +209,21 @@ const AI_PRESETS = [
|
|
|
164
209
|
{ id: 'llamacpp', label: 'llama.cpp server', api_base: 'http://127.0.0.1:8080/v1', keyRequired: false, defaultModel: '' },
|
|
165
210
|
];
|
|
166
211
|
|
|
212
|
+
const ENGINE_GROUPS = [
|
|
213
|
+
{ label: 'Web Core', items: ['duckduckgo', 'wikipedia', 'brave', 'startpage', 'qwant', 'mojeek', 'bing', 'google', 'yahoo'] },
|
|
214
|
+
{ label: 'Code & Dev', items: ['github', 'github-api', 'hackernews', 'reddit'] },
|
|
215
|
+
{ label: 'Media', items: ['youtube', 'sepiasearch'] },
|
|
216
|
+
{ label: 'Research', items: ['wikidata', 'crossref', 'openalex', 'openlibrary'] },
|
|
217
|
+
{ label: 'Federated', items: ['mastodon users', 'mastodon hashtags', 'tootfinder', 'lemmy communities', 'lemmy posts'] },
|
|
218
|
+
{ label: 'Torrent', items: ['piratebay', '1337x', 'nyaa'] },
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const ENGINE_PRESETS = [
|
|
222
|
+
{ id: 'all', label: 'All', engines: [] },
|
|
223
|
+
{ id: 'balanced', label: 'Balanced', engines: ['duckduckgo', 'wikipedia', 'bing', 'startpage', 'github', 'reddit', 'youtube'] },
|
|
224
|
+
{ id: 'github', label: 'GitHub Focus', engines: ['github-api', 'github', 'duckduckgo', 'wikipedia'] },
|
|
225
|
+
];
|
|
226
|
+
|
|
167
227
|
function detectPresetFromBase(base) {
|
|
168
228
|
const raw = String(base || '').toLowerCase();
|
|
169
229
|
if (!raw) return 'custom';
|
|
@@ -179,12 +239,88 @@ function LangPicker() {
|
|
|
179
239
|
if (l.code === getLang()) opt.selected = true;
|
|
180
240
|
sel.append(opt);
|
|
181
241
|
}
|
|
182
|
-
sel.addEventListener('change', () => {
|
|
242
|
+
sel.addEventListener('change', () => {
|
|
243
|
+
setLang(sel.value);
|
|
244
|
+
if (state.query) {
|
|
245
|
+
doSearch(state.query, state.category);
|
|
246
|
+
} else {
|
|
247
|
+
renderApp();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
183
250
|
const arrow = el('span', { className: 'lang-arrow', html: svg('<polyline points="6 9 12 15 18 9"/>', 12) });
|
|
184
251
|
wrap.append(sel, arrow);
|
|
185
252
|
return wrap;
|
|
186
253
|
}
|
|
187
254
|
|
|
255
|
+
function EnginePicker() {
|
|
256
|
+
const details = el('details', { className: 'engine-picker' });
|
|
257
|
+
const selectedCount = state.selectedEngines.length;
|
|
258
|
+
const summary = el('summary', { className: 'engine-picker-summary' },
|
|
259
|
+
el('span', { className: 'engine-picker-title' }, selectedCount ? `Engines (${selectedCount})` : 'Engines (all)'),
|
|
260
|
+
iconEl('chevron', 'engine-chevron'),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const body = el('div', { className: 'engine-picker-body' });
|
|
264
|
+
const presetRow = el('div', { className: 'engine-preset-row' });
|
|
265
|
+
ENGINE_PRESETS.forEach((preset) => {
|
|
266
|
+
presetRow.append(el('button', {
|
|
267
|
+
className: `btn ${preset.id === 'balanced' ? 'btn-primary' : ''}`,
|
|
268
|
+
type: 'button',
|
|
269
|
+
onClick: () => {
|
|
270
|
+
setSelectedEngines(preset.engines);
|
|
271
|
+
details.open = false;
|
|
272
|
+
if (state.query) doSearch(state.query, state.category);
|
|
273
|
+
else renderApp();
|
|
274
|
+
},
|
|
275
|
+
}, preset.label));
|
|
276
|
+
});
|
|
277
|
+
body.append(presetRow);
|
|
278
|
+
|
|
279
|
+
ENGINE_GROUPS.forEach((group) => {
|
|
280
|
+
const card = el('div', { className: 'engine-group' });
|
|
281
|
+
card.append(el('div', { className: 'engine-group-title' }, group.label));
|
|
282
|
+
const list = el('div', { className: 'engine-chip-wrap' });
|
|
283
|
+
group.items.forEach((engine) => {
|
|
284
|
+
const checked = state.selectedEngines.includes(engine);
|
|
285
|
+
const id = `engine-${engine.replace(/[^a-z0-9]+/g, '-')}-${Math.random().toString(36).slice(2, 6)}`;
|
|
286
|
+
const input = el('input', { id, type: 'checkbox', ...(checked ? { checked: '' } : {}) });
|
|
287
|
+
const label = el('label', { className: 'engine-chip', for: id }, input, el('span', {}, engine));
|
|
288
|
+
list.append(label);
|
|
289
|
+
});
|
|
290
|
+
card.append(list);
|
|
291
|
+
body.append(card);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
body.append(el('div', { className: 'engine-actions' },
|
|
295
|
+
el('button', {
|
|
296
|
+
className: 'btn btn-primary',
|
|
297
|
+
type: 'button',
|
|
298
|
+
onClick: () => {
|
|
299
|
+
const selected = [...details.querySelectorAll('.engine-chip input:checked')]
|
|
300
|
+
.map((node) => node.parentElement?.textContent?.trim().toLowerCase())
|
|
301
|
+
.filter(Boolean);
|
|
302
|
+
setSelectedEngines(selected);
|
|
303
|
+
details.open = false;
|
|
304
|
+
if (state.query) doSearch(state.query, state.category);
|
|
305
|
+
else renderApp();
|
|
306
|
+
},
|
|
307
|
+
}, 'Apply'),
|
|
308
|
+
el('button', {
|
|
309
|
+
className: 'btn',
|
|
310
|
+
type: 'button',
|
|
311
|
+
onClick: () => {
|
|
312
|
+
setSelectedEngines([]);
|
|
313
|
+
details.open = false;
|
|
314
|
+
if (state.query) doSearch(state.query, state.category);
|
|
315
|
+
else renderApp();
|
|
316
|
+
},
|
|
317
|
+
}, 'Reset'),
|
|
318
|
+
));
|
|
319
|
+
|
|
320
|
+
details.append(summary, body);
|
|
321
|
+
return details;
|
|
322
|
+
}
|
|
323
|
+
|
|
188
324
|
// ─── Search form ──────────────────────────────────────────────────────────
|
|
189
325
|
function SearchForm(value, onSearch) {
|
|
190
326
|
const form = el('form', { className: 'search-form' });
|
|
@@ -526,8 +662,11 @@ function flattenSocialResults(payload) {
|
|
|
526
662
|
].filter((item) => item.url);
|
|
527
663
|
}
|
|
528
664
|
|
|
529
|
-
async function runSearchProgressive(q, lang, category) {
|
|
665
|
+
async function runSearchProgressive(q, lang, category, engines = []) {
|
|
530
666
|
const params = new URLSearchParams({ q, lang, cat: category });
|
|
667
|
+
if (Array.isArray(engines) && engines.length > 0) {
|
|
668
|
+
params.set('engines', engines.join(','));
|
|
669
|
+
}
|
|
531
670
|
const response = await fetch(`/api/search-stream?${params.toString()}`);
|
|
532
671
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
533
672
|
|
|
@@ -599,11 +738,14 @@ async function doSearch(q, category = state.category) {
|
|
|
599
738
|
state.socialData = [];
|
|
600
739
|
renderApp();
|
|
601
740
|
|
|
602
|
-
const lang =
|
|
741
|
+
const lang = getResolvedLang();
|
|
742
|
+
const engines = state.selectedEngines.slice();
|
|
603
743
|
|
|
604
744
|
try {
|
|
605
|
-
const searchPromise = runSearchProgressive(q, lang, state.category).catch(async () => {
|
|
606
|
-
|
|
745
|
+
const searchPromise = runSearchProgressive(q, lang, state.category, engines).catch(async () => {
|
|
746
|
+
const p = new URLSearchParams({ q, lang, cat: state.category });
|
|
747
|
+
if (engines.length > 0) p.set('engines', engines.join(','));
|
|
748
|
+
return api(`/api/search?${p.toString()}`);
|
|
607
749
|
});
|
|
608
750
|
const promises = [
|
|
609
751
|
searchPromise,
|
|
@@ -732,6 +874,7 @@ function renderApp() {
|
|
|
732
874
|
type: 'button',
|
|
733
875
|
}, cat.label));
|
|
734
876
|
});
|
|
877
|
+
categoryBar.append(EnginePicker());
|
|
735
878
|
|
|
736
879
|
const main = el('div', { className: 'main' });
|
|
737
880
|
|
|
@@ -751,6 +894,7 @@ function renderApp() {
|
|
|
751
894
|
const meta = el('div', { className: 'results-meta' });
|
|
752
895
|
meta.append(document.createTextNode(`${state.results.length} results`));
|
|
753
896
|
if (state.providers.length) meta.append(document.createTextNode(' · ' + state.providers.join(', ')));
|
|
897
|
+
if (state.selectedEngines.length) meta.append(document.createTextNode(' · engines: ' + state.selectedEngines.join(', ')));
|
|
754
898
|
main.append(meta);
|
|
755
899
|
}
|
|
756
900
|
|
|
@@ -785,7 +929,7 @@ function renderHome(app) {
|
|
|
785
929
|
el('div', { className: 'home-logo' }, 'Term', el('strong', {}, 'Search')),
|
|
786
930
|
el('div', { className: 'home-tagline' },
|
|
787
931
|
el('span', { className: 'tagline-desktop' }, 'Personal search engine · privacy-first · local-first'),
|
|
788
|
-
el('span', { className: 'tagline-mobile' }, 'Private search
|
|
932
|
+
el('span', { className: 'tagline-mobile' }, 'Private local search'),
|
|
789
933
|
),
|
|
790
934
|
el('div', { className: 'home-search' }, SearchForm('', (q) => { state.query = q; state.category = 'web'; doSearch(q, 'web'); })),
|
|
791
935
|
el('div', { className: 'home-actions' },
|
|
@@ -857,8 +1001,13 @@ async function renderSettings() {
|
|
|
857
1001
|
presetSelect.append(opt);
|
|
858
1002
|
});
|
|
859
1003
|
const modelInput = makeInput('ai-model', ai.model, 'qwen3.5:4b');
|
|
860
|
-
|
|
861
|
-
|
|
1004
|
+
const modelSelect = el('select', { className: 'form-input', id: 'ai-model-select' },
|
|
1005
|
+
el('option', { value: '' }, 'Load models first…')
|
|
1006
|
+
);
|
|
1007
|
+
const modelQuickList = el('div', { id: 'ai-model-quick-list', className: 'model-quick-list' },
|
|
1008
|
+
el('div', { className: 'form-hint' }, 'No models loaded.')
|
|
1009
|
+
);
|
|
1010
|
+
let loadedModels = [];
|
|
862
1011
|
|
|
863
1012
|
function setModelStatus(message, type = 'info') {
|
|
864
1013
|
aiModelStatus.style.display = 'block';
|
|
@@ -879,9 +1028,31 @@ async function renderSettings() {
|
|
|
879
1028
|
}
|
|
880
1029
|
|
|
881
1030
|
function populateModelList(models) {
|
|
882
|
-
|
|
1031
|
+
loadedModels = models.slice();
|
|
1032
|
+
modelSelect.innerHTML = '';
|
|
883
1033
|
for (const model of models) {
|
|
884
|
-
|
|
1034
|
+
modelSelect.append(el('option', { value: model }, model));
|
|
1035
|
+
}
|
|
1036
|
+
modelQuickList.innerHTML = '';
|
|
1037
|
+
models.forEach((model) => {
|
|
1038
|
+
modelQuickList.append(el('button', {
|
|
1039
|
+
className: 'model-chip-btn',
|
|
1040
|
+
type: 'button',
|
|
1041
|
+
onClick: () => {
|
|
1042
|
+
const modelField = document.getElementById('ai-model');
|
|
1043
|
+
if (modelField) modelField.value = model;
|
|
1044
|
+
modelSelect.value = model;
|
|
1045
|
+
[...modelQuickList.querySelectorAll('.model-chip-btn')].forEach((n) => n.classList.remove('active'));
|
|
1046
|
+
const active = [...modelQuickList.querySelectorAll('.model-chip-btn')].find((n) => n.textContent === model);
|
|
1047
|
+
if (active) active.classList.add('active');
|
|
1048
|
+
},
|
|
1049
|
+
}, model));
|
|
1050
|
+
});
|
|
1051
|
+
const current = val('ai-model');
|
|
1052
|
+
if (current && models.includes(current)) {
|
|
1053
|
+
modelSelect.value = current;
|
|
1054
|
+
const active = [...modelQuickList.querySelectorAll('.model-chip-btn')].find((n) => n.textContent === current);
|
|
1055
|
+
if (active) active.classList.add('active');
|
|
885
1056
|
}
|
|
886
1057
|
}
|
|
887
1058
|
|
|
@@ -911,6 +1082,11 @@ async function renderSettings() {
|
|
|
911
1082
|
const models = Array.isArray(res.models) ? res.models : [];
|
|
912
1083
|
if (!models.length) {
|
|
913
1084
|
setModelStatus(`No models found${res.provider ? ` for ${res.provider}` : ''}.`, 'err');
|
|
1085
|
+
modelSelect.innerHTML = '';
|
|
1086
|
+
modelSelect.append(el('option', { value: '' }, 'No models found'));
|
|
1087
|
+
modelQuickList.innerHTML = '';
|
|
1088
|
+
modelQuickList.append(el('div', { className: 'form-hint' }, 'No models loaded.'));
|
|
1089
|
+
loadedModels = [];
|
|
914
1090
|
return;
|
|
915
1091
|
}
|
|
916
1092
|
populateModelList(models);
|
|
@@ -918,6 +1094,7 @@ async function renderSettings() {
|
|
|
918
1094
|
if (!current || !models.includes(current)) {
|
|
919
1095
|
const modelField = document.getElementById('ai-model');
|
|
920
1096
|
if (modelField) modelField.value = models[0];
|
|
1097
|
+
modelSelect.value = models[0];
|
|
921
1098
|
}
|
|
922
1099
|
setModelStatus(`Loaded ${models.length} model(s)${res.provider ? ` from ${res.provider}` : ''}.`, 'ok');
|
|
923
1100
|
} catch (e) {
|
|
@@ -1114,7 +1291,9 @@ async function renderSettings() {
|
|
|
1114
1291
|
el('div', { className: 'form-group' },
|
|
1115
1292
|
el('label', { className: 'form-label', for: 'ai-model' }, 'Model'),
|
|
1116
1293
|
modelInput,
|
|
1117
|
-
|
|
1294
|
+
el('div', { className: 'form-hint', style: 'margin-top:4px' }, 'Model list (tap to open):'),
|
|
1295
|
+
modelSelect,
|
|
1296
|
+
modelQuickList,
|
|
1118
1297
|
el('div', { className: 'form-row', style: 'margin-top:6px' },
|
|
1119
1298
|
el('button', { id: 'ai-load-models-btn', className: 'btn', onClick: () => loadModels('manual'), type: 'button' }, 'Load models'),
|
|
1120
1299
|
el('span', { className: 'form-hint', style: 'margin-top:0' }, 'Auto-loads from endpoint with current key.'),
|
|
@@ -1216,7 +1395,7 @@ async function renderSettings() {
|
|
|
1216
1395
|
// Server info
|
|
1217
1396
|
el('div', { className: 'settings-section' },
|
|
1218
1397
|
el('h2', {}, 'Server Info'),
|
|
1219
|
-
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Version'), el('span', { className: 'info-val' }, health?.version || '0.3.
|
|
1398
|
+
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Version'), el('span', { className: 'info-val' }, health?.version || '0.3.2')),
|
|
1220
1399
|
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'Active providers'), el('span', { className: 'info-val' }, (health?.providers || []).join(', ') || 'none')),
|
|
1221
1400
|
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'AI'), el('span', { className: 'info-val' }, health?.ai_enabled ? `enabled (${health.ai_model})` : 'not configured')),
|
|
1222
1401
|
el('div', { className: 'info-row' }, el('span', { className: 'info-key' }, 'GitHub'), el('a', { href: 'https://github.com/DioNanos/termsearch', target: '_blank', className: 'info-val', style: 'color:var(--link)' }, 'DioNanos/termsearch')),
|
|
@@ -1248,6 +1427,27 @@ async function renderSettings() {
|
|
|
1248
1427
|
setHistoryEnabled(Boolean(e.target?.checked));
|
|
1249
1428
|
renderHistoryPreview();
|
|
1250
1429
|
});
|
|
1430
|
+
modelSelect.addEventListener('change', () => {
|
|
1431
|
+
if (!modelSelect.value) return;
|
|
1432
|
+
const modelField = document.getElementById('ai-model');
|
|
1433
|
+
if (modelField) modelField.value = modelSelect.value;
|
|
1434
|
+
[...modelQuickList.querySelectorAll('.model-chip-btn')].forEach((n) => {
|
|
1435
|
+
n.classList.toggle('active', n.textContent === modelSelect.value);
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
modelSelect.addEventListener('focus', () => {
|
|
1439
|
+
if (!loadedModels.length) loadModels('auto');
|
|
1440
|
+
});
|
|
1441
|
+
modelInput.addEventListener('input', () => {
|
|
1442
|
+
const current = modelInput.value || '';
|
|
1443
|
+
if ([...modelSelect.options].some((opt) => opt.value === current)) {
|
|
1444
|
+
modelSelect.value = modelInput.value;
|
|
1445
|
+
}
|
|
1446
|
+
[...modelQuickList.querySelectorAll('.model-chip-btn')].forEach((n) => {
|
|
1447
|
+
n.classList.toggle('active', n.textContent === current);
|
|
1448
|
+
n.style.display = !current || n.textContent.toLowerCase().includes(current.toLowerCase()) ? 'inline-flex' : 'none';
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1251
1451
|
document.getElementById('ai-key')?.addEventListener('change', () => loadModels('auto'));
|
|
1252
1452
|
document.getElementById('ai-base')?.addEventListener('change', () => loadModels('auto'));
|
|
1253
1453
|
presetSelect.addEventListener('change', applyPreset);
|
package/frontend/dist/style.css
CHANGED
|
@@ -159,6 +159,97 @@ a:hover { color: var(--link-h); }
|
|
|
159
159
|
background: rgba(109,40,217,0.15);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
.engine-picker {
|
|
163
|
+
margin-left: auto;
|
|
164
|
+
position: relative;
|
|
165
|
+
}
|
|
166
|
+
.engine-picker summary {
|
|
167
|
+
list-style: none;
|
|
168
|
+
}
|
|
169
|
+
.engine-picker summary::-webkit-details-marker {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
.engine-picker-summary {
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 6px;
|
|
176
|
+
padding: 5px 10px;
|
|
177
|
+
border-radius: 999px;
|
|
178
|
+
border: 1px solid var(--border);
|
|
179
|
+
background: var(--bg2);
|
|
180
|
+
color: var(--text2);
|
|
181
|
+
font-size: 11px;
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
}
|
|
184
|
+
.engine-picker-title {
|
|
185
|
+
white-space: nowrap;
|
|
186
|
+
}
|
|
187
|
+
.engine-chevron {
|
|
188
|
+
opacity: 0.8;
|
|
189
|
+
}
|
|
190
|
+
.engine-picker[open] .engine-chevron {
|
|
191
|
+
transform: rotate(180deg);
|
|
192
|
+
}
|
|
193
|
+
.engine-picker-body {
|
|
194
|
+
position: absolute;
|
|
195
|
+
right: 0;
|
|
196
|
+
top: calc(100% + 8px);
|
|
197
|
+
width: min(92vw, 560px);
|
|
198
|
+
max-height: min(65vh, 560px);
|
|
199
|
+
overflow: auto;
|
|
200
|
+
background: var(--bg2);
|
|
201
|
+
border: 1px solid var(--border);
|
|
202
|
+
border-radius: var(--radius);
|
|
203
|
+
padding: 10px;
|
|
204
|
+
z-index: 120;
|
|
205
|
+
box-shadow: 0 16px 32px rgba(0,0,0,0.4);
|
|
206
|
+
}
|
|
207
|
+
.engine-preset-row {
|
|
208
|
+
display: flex;
|
|
209
|
+
gap: 6px;
|
|
210
|
+
margin-bottom: 8px;
|
|
211
|
+
flex-wrap: wrap;
|
|
212
|
+
}
|
|
213
|
+
.engine-group {
|
|
214
|
+
padding: 8px;
|
|
215
|
+
border: 1px solid var(--border2);
|
|
216
|
+
border-radius: var(--radius-sm);
|
|
217
|
+
margin-bottom: 8px;
|
|
218
|
+
}
|
|
219
|
+
.engine-group-title {
|
|
220
|
+
font-size: 10px;
|
|
221
|
+
color: var(--text3);
|
|
222
|
+
margin-bottom: 7px;
|
|
223
|
+
letter-spacing: 0.06em;
|
|
224
|
+
text-transform: uppercase;
|
|
225
|
+
}
|
|
226
|
+
.engine-chip-wrap {
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-wrap: wrap;
|
|
229
|
+
gap: 6px;
|
|
230
|
+
}
|
|
231
|
+
.engine-chip {
|
|
232
|
+
display: inline-flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 5px;
|
|
235
|
+
border: 1px solid var(--border);
|
|
236
|
+
border-radius: 999px;
|
|
237
|
+
padding: 4px 8px;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
font-size: 10px;
|
|
240
|
+
color: var(--text2);
|
|
241
|
+
background: #0d0d0d;
|
|
242
|
+
}
|
|
243
|
+
.engine-chip input {
|
|
244
|
+
margin: 0;
|
|
245
|
+
}
|
|
246
|
+
.engine-actions {
|
|
247
|
+
display: flex;
|
|
248
|
+
gap: 8px;
|
|
249
|
+
justify-content: flex-end;
|
|
250
|
+
margin-top: 8px;
|
|
251
|
+
}
|
|
252
|
+
|
|
162
253
|
/* ─── Homepage ────────────────────────────────────────────────────────────── */
|
|
163
254
|
.home {
|
|
164
255
|
flex: 1;
|
|
@@ -196,7 +287,6 @@ a:hover { color: var(--link-h); }
|
|
|
196
287
|
}
|
|
197
288
|
.tagline-mobile { display: none; }
|
|
198
289
|
.tagline-desktop { display: inline; }
|
|
199
|
-
}
|
|
200
290
|
|
|
201
291
|
.home-search { width: 100%; max-width: 560px; margin-bottom: 14px; }
|
|
202
292
|
|
|
@@ -462,8 +552,9 @@ a:hover { color: var(--link-h); }
|
|
|
462
552
|
|
|
463
553
|
/* AI panel */
|
|
464
554
|
.panel-ai {
|
|
465
|
-
background:
|
|
555
|
+
background: linear-gradient(180deg, rgba(79,70,229,0.12) 0%, rgba(15,15,26,0.95) 42%);
|
|
466
556
|
border: 1px solid var(--ai-border);
|
|
557
|
+
box-shadow: 0 12px 28px rgba(30,27,75,0.25);
|
|
467
558
|
}
|
|
468
559
|
.panel-ai .panel-header-label { color: var(--ai-text2); }
|
|
469
560
|
.panel-ai .panel-label { color: var(--ai-text); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
@@ -480,6 +571,12 @@ a:hover { color: var(--link-h); }
|
|
|
480
571
|
line-height: 1.7;
|
|
481
572
|
white-space: pre-wrap;
|
|
482
573
|
margin-top: 10px;
|
|
574
|
+
background: rgba(10,10,16,0.6);
|
|
575
|
+
border: 1px solid rgba(129,140,248,0.22);
|
|
576
|
+
border-radius: var(--radius-sm);
|
|
577
|
+
padding: 10px;
|
|
578
|
+
max-height: 280px;
|
|
579
|
+
overflow: auto;
|
|
483
580
|
}
|
|
484
581
|
.ai-meta { font-size: 11px; color: var(--text3); margin-top: 8px; }
|
|
485
582
|
|
|
@@ -659,6 +756,29 @@ a:hover { color: var(--link-h); }
|
|
|
659
756
|
.form-input::placeholder { color: var(--text3); }
|
|
660
757
|
.form-input[type="password"] { font-family: var(--font-mono); }
|
|
661
758
|
.form-hint { font-size: 11px; color: var(--text3); margin-top: 3px; }
|
|
759
|
+
.model-quick-list {
|
|
760
|
+
display: flex;
|
|
761
|
+
flex-wrap: wrap;
|
|
762
|
+
gap: 6px;
|
|
763
|
+
margin-top: 8px;
|
|
764
|
+
max-height: 160px;
|
|
765
|
+
overflow: auto;
|
|
766
|
+
padding: 2px 1px 2px 0;
|
|
767
|
+
}
|
|
768
|
+
.model-chip-btn {
|
|
769
|
+
border: 1px solid var(--border);
|
|
770
|
+
border-radius: 999px;
|
|
771
|
+
padding: 4px 8px;
|
|
772
|
+
background: #0b0b0b;
|
|
773
|
+
color: var(--text2);
|
|
774
|
+
font-size: 11px;
|
|
775
|
+
cursor: pointer;
|
|
776
|
+
}
|
|
777
|
+
.model-chip-btn.active {
|
|
778
|
+
color: var(--ai-text2);
|
|
779
|
+
border-color: rgba(109,40,217,0.5);
|
|
780
|
+
background: rgba(109,40,217,0.18);
|
|
781
|
+
}
|
|
662
782
|
|
|
663
783
|
.form-row {
|
|
664
784
|
display: flex; gap: 8px; align-items: flex-end;
|
|
@@ -748,8 +868,11 @@ a:hover { color: var(--link-h); }
|
|
|
748
868
|
/* ─── Responsive ──────────────────────────────────────────────────────────── */
|
|
749
869
|
@media (max-width: 640px) {
|
|
750
870
|
.header { padding: 8px 12px; gap: 8px; }
|
|
751
|
-
.category-tabs { top: 48px; padding: 7px 12px 9px; gap: 5px; }
|
|
871
|
+
.category-tabs { top: 48px; padding: 7px 12px 9px; gap: 5px; flex-wrap: wrap; }
|
|
752
872
|
.cat-tab { font-size: 10px; padding: 4px 8px; }
|
|
873
|
+
.engine-picker { width: 100%; margin-left: 0; }
|
|
874
|
+
.engine-picker-summary { width: 100%; justify-content: space-between; }
|
|
875
|
+
.engine-picker-body { width: calc(100vw - 24px); right: -6px; }
|
|
753
876
|
.logo-text { font-size: 15px; }
|
|
754
877
|
.home-logo { font-size: 40px; }
|
|
755
878
|
.home-tagline { letter-spacing: 0.08em; margin-bottom: 20px; }
|