termsearch 0.3.3 → 0.3.5
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/bin/termsearch.js +40 -2
- package/frontend/dist/app.js +158 -31
- package/frontend/dist/style.css +75 -7
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -2
package/bin/termsearch.js
CHANGED
|
@@ -209,7 +209,7 @@ async function cmdStart(flags) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
writeFileSync(paths.pid, String(child.pid));
|
|
212
|
-
ok(`
|
|
212
|
+
ok(`TermSearch v${VERSION} started (PID ${child.pid})`);
|
|
213
213
|
info(`${BOLD}${getUrl()}${RESET}`);
|
|
214
214
|
info(`Logs: ${paths.log}`);
|
|
215
215
|
}
|
|
@@ -242,10 +242,11 @@ async function cmdRestart(flags) {
|
|
|
242
242
|
await cmdStart(flags);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
function cmdStatus() {
|
|
245
|
+
async function cmdStatus() {
|
|
246
246
|
const { running, pid } = getStatus();
|
|
247
247
|
const paths = getPaths();
|
|
248
248
|
console.log('');
|
|
249
|
+
console.log(` ${BOLD}TermSearch v${VERSION}${RESET}`);
|
|
249
250
|
if (running) {
|
|
250
251
|
ok(`${BOLD}Running${RESET} (PID ${pid})`);
|
|
251
252
|
info(`${getUrl()}`);
|
|
@@ -255,6 +256,7 @@ function cmdStatus() {
|
|
|
255
256
|
warn('Stopped');
|
|
256
257
|
info(`Run ${BOLD}termsearch start${RESET} to start`);
|
|
257
258
|
}
|
|
259
|
+
await printUpdateHint();
|
|
258
260
|
console.log('');
|
|
259
261
|
}
|
|
260
262
|
|
|
@@ -333,6 +335,9 @@ async function cmdDoctor() {
|
|
|
333
335
|
} catch (e) { err(`HTTP: cannot reach ${getUrl()} — ${e.message}`); allOk = false; }
|
|
334
336
|
}
|
|
335
337
|
|
|
338
|
+
// npm update check
|
|
339
|
+
await printUpdateHint();
|
|
340
|
+
|
|
336
341
|
console.log('');
|
|
337
342
|
if (allOk) { ok(`${GREEN}All checks passed${RESET}`); }
|
|
338
343
|
else { warn('Some checks failed — see above'); }
|
|
@@ -428,6 +433,39 @@ ${BOLD}URL:${RESET} http://localhost:3000
|
|
|
428
433
|
`);
|
|
429
434
|
}
|
|
430
435
|
|
|
436
|
+
// ─── Update check ─────────────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
async function checkNpmUpdate() {
|
|
439
|
+
try {
|
|
440
|
+
const ac = new AbortController();
|
|
441
|
+
setTimeout(() => ac.abort(), 4000);
|
|
442
|
+
const r = await fetch('https://registry.npmjs.org/termsearch/latest', { signal: ac.signal });
|
|
443
|
+
if (!r.ok) return null;
|
|
444
|
+
const data = await r.json();
|
|
445
|
+
const latest = data.version;
|
|
446
|
+
if (!latest) return null;
|
|
447
|
+
if (latest === VERSION) return { upToDate: true, latest };
|
|
448
|
+
// Simple semver compare: split, compare numerically
|
|
449
|
+
const cur = VERSION.split('.').map(Number);
|
|
450
|
+
const lat = latest.split('.').map(Number);
|
|
451
|
+
const newer = lat[0] > cur[0] || (lat[0] === cur[0] && lat[1] > cur[1]) || (lat[0] === cur[0] && lat[1] === cur[1] && lat[2] > cur[2]);
|
|
452
|
+
return { upToDate: !newer, latest };
|
|
453
|
+
} catch {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function printUpdateHint() {
|
|
459
|
+
const update = await checkNpmUpdate();
|
|
460
|
+
if (!update) return;
|
|
461
|
+
if (update.upToDate) {
|
|
462
|
+
ok(`Up to date (v${VERSION})`);
|
|
463
|
+
} else {
|
|
464
|
+
warn(`Update available: v${VERSION} → v${update.latest}`);
|
|
465
|
+
info(`Run ${BOLD}npm install -g termsearch${RESET} to update`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
431
469
|
// ─── Utilities ────────────────────────────────────────────────────────────
|
|
432
470
|
|
|
433
471
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
package/frontend/dist/app.js
CHANGED
|
@@ -15,6 +15,7 @@ const state = {
|
|
|
15
15
|
aiExpanded: false,
|
|
16
16
|
aiStartTime: null,
|
|
17
17
|
aiLatencyMs: null,
|
|
18
|
+
aiSession: [], // { q, r }[] — ultimi 4 summary (passati all'AI come contesto)
|
|
18
19
|
aiLastQuery: null,
|
|
19
20
|
aiLastResults: null,
|
|
20
21
|
aiLastLang: null,
|
|
@@ -44,6 +45,47 @@ const state = {
|
|
|
44
45
|
})(),
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
let mobileBarCleanup = null;
|
|
49
|
+
|
|
50
|
+
function clearMobileBarLayout() {
|
|
51
|
+
if (typeof mobileBarCleanup === 'function') {
|
|
52
|
+
mobileBarCleanup();
|
|
53
|
+
}
|
|
54
|
+
mobileBarCleanup = null;
|
|
55
|
+
document.documentElement.style.setProperty('--mobile-bar-height', '0px');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function bindMobileBarLayout(mobileBar) {
|
|
59
|
+
clearMobileBarLayout();
|
|
60
|
+
if (!mobileBar) return;
|
|
61
|
+
|
|
62
|
+
const media = window.matchMedia('(max-width: 640px)');
|
|
63
|
+
const root = document.documentElement;
|
|
64
|
+
const update = () => {
|
|
65
|
+
if (!media.matches || !mobileBar.isConnected) {
|
|
66
|
+
root.style.setProperty('--mobile-bar-height', '0px');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const h = Math.ceil(mobileBar.getBoundingClientRect().height);
|
|
70
|
+
root.style.setProperty('--mobile-bar-height', `${h}px`);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const onResize = () => update();
|
|
74
|
+
window.addEventListener('resize', onResize);
|
|
75
|
+
|
|
76
|
+
let observer = null;
|
|
77
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
78
|
+
observer = new ResizeObserver(update);
|
|
79
|
+
observer.observe(mobileBar);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
requestAnimationFrame(update);
|
|
83
|
+
mobileBarCleanup = () => {
|
|
84
|
+
window.removeEventListener('resize', onResize);
|
|
85
|
+
if (observer) observer.disconnect();
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
47
89
|
function buildSearchHash(query, category = 'web') {
|
|
48
90
|
const params = new URLSearchParams();
|
|
49
91
|
if (query) params.set('q', query);
|
|
@@ -225,7 +267,6 @@ const AI_PRESETS = [
|
|
|
225
267
|
{ id: 'chutes', label: 'Chutes.ai TEE', api_base: 'https://llm.chutes.ai/v1', keyRequired: true, defaultModel: 'deepseek-ai/DeepSeek-V3.2-TEE' },
|
|
226
268
|
{ id: 'anthropic',label: 'Anthropic', api_base: 'https://api.anthropic.com/v1', keyRequired: true, defaultModel: 'claude-3-5-haiku-latest' },
|
|
227
269
|
{ id: 'openai', label: 'OpenAI', api_base: 'https://api.openai.com/v1', keyRequired: true, defaultModel: 'gpt-4o-mini' },
|
|
228
|
-
{ id: 'openrouter', label: 'OpenRoute/OpenRouter', api_base: 'https://openrouter.ai/api/v1', keyRequired: true, defaultModel: 'openai/gpt-4o-mini' },
|
|
229
270
|
];
|
|
230
271
|
|
|
231
272
|
const ENGINE_GROUPS = [
|
|
@@ -240,7 +281,7 @@ const ENGINE_GROUPS = [
|
|
|
240
281
|
|
|
241
282
|
const ENGINE_PRESETS = [
|
|
242
283
|
{ id: 'all', label: 'All', engines: [] },
|
|
243
|
-
{ id: 'balanced', label: 'Balanced', engines: ['duckduckgo', 'wikipedia', 'bing', '
|
|
284
|
+
{ id: 'balanced', label: 'Balanced', engines: ['duckduckgo', 'wikipedia', 'bing', 'brave', 'github', 'reddit', 'youtube'] },
|
|
244
285
|
{ id: 'github', label: 'GitHub Focus', engines: ['github-api', 'github', 'duckduckgo', 'wikipedia'] },
|
|
245
286
|
];
|
|
246
287
|
|
|
@@ -443,32 +484,41 @@ function renderAiPanel() {
|
|
|
443
484
|
const isDone = state.aiStatus === 'done';
|
|
444
485
|
const isError = state.aiStatus === 'error';
|
|
445
486
|
const dotsClass = isDone ? 'done' : isError ? 'error' : '';
|
|
446
|
-
const statusText = state.aiStatus === 'loading' ? 'Thinking…'
|
|
447
|
-
: state.aiStatus === 'streaming' ? 'Generating…'
|
|
448
|
-
: isDone ? 'AI Summary' : 'Error';
|
|
449
487
|
|
|
450
|
-
//
|
|
488
|
+
// Row 1: dots + "AI" (always) + model + latency
|
|
451
489
|
const dotsEl = el('div', { className: 'ai-dots' });
|
|
452
490
|
['violet', 'indigo', 'dim'].forEach(c => {
|
|
453
491
|
dotsEl.append(el('div', { className: `ai-dot ${dotsClass || c}` }));
|
|
454
492
|
});
|
|
455
|
-
|
|
456
|
-
// Latency
|
|
457
493
|
const latMs = state.aiLatencyMs;
|
|
458
494
|
const latLabel = latMs != null ? (latMs < 1000 ? `${latMs}ms` : `${(latMs / 1000).toFixed(1)}s`) : null;
|
|
459
|
-
|
|
460
|
-
// Header
|
|
461
|
-
const headerLeft = el('div', { className: 'panel-header-left' },
|
|
495
|
+
const row1 = el('div', { className: 'panel-header-left' },
|
|
462
496
|
dotsEl,
|
|
463
|
-
el('span', { className: 'panel-label' },
|
|
497
|
+
el('span', { className: 'panel-label' }, 'AI'),
|
|
464
498
|
state.aiMeta?.model ? el('span', { className: 'ai-model-label' }, state.aiMeta.model) : null,
|
|
465
499
|
latLabel ? el('span', { className: 'ai-latency-label' }, `· ${latLabel}`) : null,
|
|
466
500
|
);
|
|
501
|
+
|
|
502
|
+
// Row 2: status text (violet) + step/fetch meta
|
|
503
|
+
const statusText = isError ? 'Error'
|
|
504
|
+
: isDone ? 'Done'
|
|
505
|
+
: state.aiStatus === 'loading' ? 'Thinking…' : 'Generating…';
|
|
506
|
+
const statusColor = isError ? '#f87171' : isDone ? '#a78bfa' : '#a78bfa';
|
|
507
|
+
const lastStep = state.aiSteps.length > 0 ? state.aiSteps[state.aiSteps.length - 1] : null;
|
|
508
|
+
const metaText = isDone && state.aiMeta?.fetchedCount ? `· ${state.aiMeta.fetchedCount} pages read`
|
|
509
|
+
: isLoading && lastStep ? `· ${lastStep}` : '';
|
|
510
|
+
const row2 = el('div', { className: 'ai-status-row' },
|
|
511
|
+
el('span', { className: 'ai-status-text', style: `color:${statusColor}` }, statusText),
|
|
512
|
+
metaText ? el('span', { className: 'ai-status-meta' }, metaText) : null,
|
|
513
|
+
);
|
|
514
|
+
|
|
467
515
|
const chevronPath = state.aiExpanded ? '<polyline points="18 15 12 9 6 15"/>' : '<polyline points="6 9 12 15 18 9"/>';
|
|
468
516
|
const expandBtn = el('button', { className: 'ai-expand-btn', type: 'button', title: state.aiExpanded ? 'Collapse' : 'Expand' });
|
|
469
517
|
expandBtn.innerHTML = svg(chevronPath, 14);
|
|
470
518
|
expandBtn.onclick = () => { state.aiExpanded = !state.aiExpanded; renderAiPanel(); };
|
|
471
|
-
|
|
519
|
+
|
|
520
|
+
const headerInner = el('div', { className: 'ai-header-inner' }, row1, row2);
|
|
521
|
+
const header = el('div', { className: 'panel-header' }, headerInner, expandBtn);
|
|
472
522
|
|
|
473
523
|
// Progress bar
|
|
474
524
|
const showProgress = isLoading && state.aiProgress > 0;
|
|
@@ -509,6 +559,19 @@ function renderAiPanel() {
|
|
|
509
559
|
}),
|
|
510
560
|
) : null;
|
|
511
561
|
|
|
562
|
+
// Session memory (shown when expanded + more than 1 entry, all except current)
|
|
563
|
+
const prevSession = state.aiSession.slice(0, -1);
|
|
564
|
+
const sessionEl = (state.aiExpanded && prevSession.length > 0) ? el('div', { className: 'ai-session' },
|
|
565
|
+
el('p', { className: 'ai-session-label' }, 'Session'),
|
|
566
|
+
...prevSession.map((item, i) =>
|
|
567
|
+
el('div', { className: 'ai-session-item' },
|
|
568
|
+
el('span', { className: 'ai-session-num' }, `${i + 1}.`),
|
|
569
|
+
el('span', { className: 'ai-session-q' }, item.q),
|
|
570
|
+
el('span', { className: 'ai-session-r' }, `→ ${item.r}`),
|
|
571
|
+
)
|
|
572
|
+
),
|
|
573
|
+
) : null;
|
|
574
|
+
|
|
512
575
|
// Footer: retry + expand/collapse
|
|
513
576
|
const retryBtn = el('button', { className: 'ai-retry-btn', type: 'button' }, 'Retry');
|
|
514
577
|
retryBtn.onclick = () => {
|
|
@@ -526,6 +589,7 @@ function renderAiPanel() {
|
|
|
526
589
|
if (stepsEl) panel.append(stepsEl);
|
|
527
590
|
panel.append(contentEl);
|
|
528
591
|
if (sourcesEl) panel.append(sourcesEl);
|
|
592
|
+
if (sessionEl) panel.append(sessionEl);
|
|
529
593
|
panel.append(footer);
|
|
530
594
|
|
|
531
595
|
if (state.aiStatus === 'streaming' && state.aiSummary.length < 60) {
|
|
@@ -887,7 +951,7 @@ async function startAiSummary(query, results, lang) {
|
|
|
887
951
|
const r = await fetch('/api/ai-summary', {
|
|
888
952
|
method: 'POST',
|
|
889
953
|
headers: { 'Content-Type': 'application/json' },
|
|
890
|
-
body: JSON.stringify({ query, lang, results: results.slice(0, 10), stream: true }),
|
|
954
|
+
body: JSON.stringify({ query, lang, results: results.slice(0, 10), stream: true, session: state.aiSession }),
|
|
891
955
|
});
|
|
892
956
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
893
957
|
|
|
@@ -917,6 +981,12 @@ async function startAiSummary(query, results, lang) {
|
|
|
917
981
|
state.aiSources = Array.isArray(d.sites) ? d.sites.map(sanitizeHttpUrl).filter(Boolean) : [];
|
|
918
982
|
state.aiMeta = { fetchedCount: d.fetchedCount, model: d.model };
|
|
919
983
|
state.aiLatencyMs = Date.now() - state.aiStartTime;
|
|
984
|
+
// Save to session memory (max 4 entries, skip if already saved for this query)
|
|
985
|
+
const lastEntry = state.aiSession[state.aiSession.length - 1];
|
|
986
|
+
if (state.aiSummary && (!lastEntry || lastEntry.q !== query)) {
|
|
987
|
+
const r = state.aiSummary.split(/[.!?\n]/)[0].slice(0, 100);
|
|
988
|
+
state.aiSession = [...state.aiSession.slice(-3), { q: query, r }];
|
|
989
|
+
}
|
|
920
990
|
renderAiPanel();
|
|
921
991
|
}
|
|
922
992
|
} catch { /* ignore */ }
|
|
@@ -937,12 +1007,13 @@ function renderApp() {
|
|
|
937
1007
|
app.innerHTML = '';
|
|
938
1008
|
|
|
939
1009
|
if (!state.query) {
|
|
1010
|
+
clearMobileBarLayout();
|
|
940
1011
|
renderHome(app);
|
|
941
1012
|
return;
|
|
942
1013
|
}
|
|
943
1014
|
|
|
944
1015
|
// Results page
|
|
945
|
-
const header = el('div', { className: 'header' },
|
|
1016
|
+
const header = el('div', { className: 'header hide-mobile' },
|
|
946
1017
|
el('div', { className: 'logo-text', onClick: () => { state.query = ''; state.category = 'web'; navigate('#/'); renderApp(); } },
|
|
947
1018
|
'Term', el('strong', {}, 'Search'),
|
|
948
1019
|
),
|
|
@@ -953,26 +1024,47 @@ function renderApp() {
|
|
|
953
1024
|
el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
|
|
954
1025
|
),
|
|
955
1026
|
);
|
|
956
|
-
|
|
1027
|
+
|
|
1028
|
+
const categoryBar = el('div', { className: 'category-tabs hide-mobile' });
|
|
957
1029
|
const categories = [
|
|
958
1030
|
{ id: 'web', label: 'Web' },
|
|
959
1031
|
{ id: 'images', label: 'Images' },
|
|
960
1032
|
{ id: 'news', label: 'News' },
|
|
961
1033
|
];
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1034
|
+
const buildCatTabs = (container) => {
|
|
1035
|
+
categories.forEach((cat) => {
|
|
1036
|
+
container.append(el('button', {
|
|
1037
|
+
className: `cat-tab ${state.category === cat.id ? 'active' : ''}`,
|
|
1038
|
+
onClick: () => {
|
|
1039
|
+
if (state.category === cat.id) return;
|
|
1040
|
+
state.category = cat.id;
|
|
1041
|
+
navigate(buildSearchHash(state.query, state.category));
|
|
1042
|
+
if (state.query) doSearch(state.query, state.category);
|
|
1043
|
+
},
|
|
1044
|
+
type: 'button',
|
|
1045
|
+
}, cat.label));
|
|
1046
|
+
});
|
|
1047
|
+
};
|
|
1048
|
+
buildCatTabs(categoryBar);
|
|
974
1049
|
categoryBar.append(EnginePicker());
|
|
975
1050
|
|
|
1051
|
+
const mobileTabs = el('div', { className: 'mobile-bar-tabs' });
|
|
1052
|
+
buildCatTabs(mobileTabs);
|
|
1053
|
+
const mobileBar = el('div', { className: 'mobile-bar' },
|
|
1054
|
+
el('div', { className: 'mobile-bar-search' }, SearchForm(state.query, (q, cat) => { state.query = q; doSearch(q, cat); })),
|
|
1055
|
+
mobileTabs,
|
|
1056
|
+
el('div', { className: 'mobile-bar-row' },
|
|
1057
|
+
el('div', {
|
|
1058
|
+
className: 'mobile-logo',
|
|
1059
|
+
onClick: () => { state.query = ''; state.category = 'web'; navigate('#/'); renderApp(); },
|
|
1060
|
+
}, 'Term', el('strong', {}, 'Search')),
|
|
1061
|
+
EnginePicker(),
|
|
1062
|
+
LangPicker(),
|
|
1063
|
+
el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
|
|
1064
|
+
el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
|
|
1065
|
+
),
|
|
1066
|
+
);
|
|
1067
|
+
|
|
976
1068
|
const main = el('div', { className: 'main' });
|
|
977
1069
|
|
|
978
1070
|
// AI panel placeholder
|
|
@@ -1016,7 +1108,8 @@ function renderApp() {
|
|
|
1016
1108
|
if (socPanel) main.append(socPanel);
|
|
1017
1109
|
}
|
|
1018
1110
|
|
|
1019
|
-
app.append(header, categoryBar, main);
|
|
1111
|
+
app.append(header, categoryBar, main, mobileBar);
|
|
1112
|
+
bindMobileBarLayout(mobileBar);
|
|
1020
1113
|
renderAiPanel();
|
|
1021
1114
|
}
|
|
1022
1115
|
|
|
@@ -1051,6 +1144,7 @@ async function renderSettings() {
|
|
|
1051
1144
|
const app = document.getElementById('app');
|
|
1052
1145
|
if (!app) return;
|
|
1053
1146
|
app.innerHTML = '';
|
|
1147
|
+
clearMobileBarLayout();
|
|
1054
1148
|
|
|
1055
1149
|
let cfg = state.config;
|
|
1056
1150
|
if (!cfg) {
|
|
@@ -1066,6 +1160,9 @@ async function renderSettings() {
|
|
|
1066
1160
|
const brave = cfg.brave || {};
|
|
1067
1161
|
const mojeek = cfg.mojeek || {};
|
|
1068
1162
|
const searxng = cfg.searxng || {};
|
|
1163
|
+
const yandexCfg = cfg.yandex || {};
|
|
1164
|
+
const ahmiaCfg = cfg.ahmia || {};
|
|
1165
|
+
const marginaliaCfg = cfg.marginalia || {};
|
|
1069
1166
|
const detectedPreset = detectPresetFromBase(ai.api_base);
|
|
1070
1167
|
|
|
1071
1168
|
const header = el('div', { className: 'header' },
|
|
@@ -1243,6 +1340,9 @@ async function renderSettings() {
|
|
|
1243
1340
|
brave: { enabled: isChecked('brave-enabled') },
|
|
1244
1341
|
mojeek: { enabled: isChecked('mojeek-enabled') },
|
|
1245
1342
|
searxng:{ url: val('searxng-url'), enabled: isChecked('searxng-enabled') },
|
|
1343
|
+
yandex: { enabled: isChecked('yandex-enabled') },
|
|
1344
|
+
ahmia: { enabled: isChecked('ahmia-enabled') },
|
|
1345
|
+
marginalia: { enabled: isChecked('marginalia-enabled') },
|
|
1246
1346
|
};
|
|
1247
1347
|
if (aiKey) update.ai.api_key = aiKey;
|
|
1248
1348
|
if (braveKey) update.brave.api_key = braveKey;
|
|
@@ -1375,7 +1475,7 @@ async function renderSettings() {
|
|
|
1375
1475
|
el('label', { className: 'form-label', for: 'ai-base' }, 'API Endpoint'),
|
|
1376
1476
|
makeInput('ai-base', ai.api_base, 'http://localhost:11434/v1'),
|
|
1377
1477
|
el('div', { className: 'form-hint' },
|
|
1378
|
-
'Included presets: LocalHost (Ollama · LM Studio · llama.cpp) · Chutes.ai TEE · Anthropic · OpenAI
|
|
1478
|
+
'Included presets: LocalHost (Ollama · LM Studio · llama.cpp) · Chutes.ai TEE · Anthropic · OpenAI',
|
|
1379
1479
|
el('br', {}),
|
|
1380
1480
|
'You can also keep custom OpenAI-compatible endpoints.',
|
|
1381
1481
|
),
|
|
@@ -1468,7 +1568,7 @@ async function renderSettings() {
|
|
|
1468
1568
|
),
|
|
1469
1569
|
|
|
1470
1570
|
// SearXNG
|
|
1471
|
-
el('div', { style: 'padding:10px 0' },
|
|
1571
|
+
el('div', { style: 'padding:10px 0;border-bottom:1px solid var(--border2)' },
|
|
1472
1572
|
el('div', { className: 'toggle-row' },
|
|
1473
1573
|
el('span', { className: 'toggle-label' }, 'SearXNG (self-hosted)'),
|
|
1474
1574
|
el('label', { className: 'toggle' },
|
|
@@ -1483,6 +1583,33 @@ async function renderSettings() {
|
|
|
1483
1583
|
el('div', { id: 'provider-test-searxng', style: 'display:none' }),
|
|
1484
1584
|
),
|
|
1485
1585
|
|
|
1586
|
+
// Uncensored / Alternative
|
|
1587
|
+
el('div', { style: 'padding:10px 0' },
|
|
1588
|
+
el('div', { style: 'font-size:11px;color:var(--text2);margin-bottom:8px;letter-spacing:0.04em;text-transform:uppercase' }, 'Uncensored / Alternative'),
|
|
1589
|
+
el('div', { className: 'toggle-row' },
|
|
1590
|
+
el('span', { className: 'toggle-label' }, 'Yandex (HTML scraper, no key)'),
|
|
1591
|
+
el('label', { className: 'toggle' },
|
|
1592
|
+
el('input', { type: 'checkbox', id: 'yandex-enabled', ...(yandexCfg.enabled !== false ? { checked: '' } : {}) }),
|
|
1593
|
+
el('span', { className: 'toggle-slider' }),
|
|
1594
|
+
),
|
|
1595
|
+
),
|
|
1596
|
+
el('div', { className: 'toggle-row', style: 'margin-top:6px' },
|
|
1597
|
+
el('span', { className: 'toggle-label' }, 'Ahmia (Tor index, no key)'),
|
|
1598
|
+
el('label', { className: 'toggle' },
|
|
1599
|
+
el('input', { type: 'checkbox', id: 'ahmia-enabled', ...(ahmiaCfg.enabled !== false ? { checked: '' } : {}) }),
|
|
1600
|
+
el('span', { className: 'toggle-slider' }),
|
|
1601
|
+
),
|
|
1602
|
+
),
|
|
1603
|
+
el('div', { className: 'toggle-row', style: 'margin-top:6px' },
|
|
1604
|
+
el('span', { className: 'toggle-label' }, 'Marginalia (indie index, no key)'),
|
|
1605
|
+
el('label', { className: 'toggle' },
|
|
1606
|
+
el('input', { type: 'checkbox', id: 'marginalia-enabled', ...(marginaliaCfg.enabled !== false ? { checked: '' } : {}) }),
|
|
1607
|
+
el('span', { className: 'toggle-slider' }),
|
|
1608
|
+
),
|
|
1609
|
+
),
|
|
1610
|
+
el('div', { className: 'form-hint', style: 'margin-top:6px' }, 'Zero-config scraper engines. May be blocked by CAPTCHA under heavy use.'),
|
|
1611
|
+
),
|
|
1612
|
+
|
|
1486
1613
|
el('div', { style: 'margin-top:12px;display:flex;align-items:center;gap:8px' },
|
|
1487
1614
|
el('button', { className: 'btn btn-primary', onClick: saveSettings }, 'Save All'),
|
|
1488
1615
|
saveAlertEl,
|
package/frontend/dist/style.css
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
--link: #8ab4f8;
|
|
14
14
|
--link-h: #aecbfa;
|
|
15
15
|
--url: #34a853;
|
|
16
|
+
--mobile-bar-height: 0px;
|
|
16
17
|
--radius: 12px;
|
|
17
18
|
--radius-sm: 8px;
|
|
18
19
|
--radius-xs: 6px;
|
|
@@ -565,10 +566,14 @@ a:hover { color: var(--link-h); }
|
|
|
565
566
|
.ai-dot.dim { background: #5b21b6; animation: pulse 1.4s ease-in-out 300ms infinite; }
|
|
566
567
|
.ai-dot.done { background: #34d399; animation: none; }
|
|
567
568
|
.ai-dot.error { background: #f87171; animation: none; }
|
|
568
|
-
.panel-ai .panel-header { cursor: default; }
|
|
569
|
+
.panel-ai .panel-header { cursor: default; align-items: flex-start; }
|
|
570
|
+
.ai-header-inner { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
|
571
|
+
.ai-status-row { display: flex; align-items: center; gap: 6px; }
|
|
572
|
+
.ai-status-text { font-size: 14px; font-weight: 500; }
|
|
573
|
+
.ai-status-meta { font-size: 10px; color: #4b5563; }
|
|
569
574
|
.ai-model-label { font-size: 10px; color: var(--text3); margin-left: 6px; }
|
|
570
575
|
.ai-latency-label{ font-size: 10px; color: #4b5563; margin-left: 4px; }
|
|
571
|
-
.ai-expand-btn { background: none; border: none; color: var(--text3); cursor: pointer; padding: 0; margin-left:
|
|
576
|
+
.ai-expand-btn { background: none; border: none; color: var(--text3); cursor: pointer; padding: 0; margin-left: 8px; margin-top: 2px; display: flex; align-items: center; flex-shrink: 0; }
|
|
572
577
|
.ai-expand-btn:hover { color: var(--text2); }
|
|
573
578
|
|
|
574
579
|
/* Progress bar */
|
|
@@ -611,6 +616,14 @@ a:hover { color: var(--link-h); }
|
|
|
611
616
|
}
|
|
612
617
|
.ai-source-pill:hover { color: #a78bfa; border-color: #4c1d95; }
|
|
613
618
|
|
|
619
|
+
/* Session memory */
|
|
620
|
+
.ai-session { margin-top: 10px; padding-top: 8px; border-top: 1px solid #1b1b2d; }
|
|
621
|
+
.ai-session-label { font-size: 10px; color: #374151; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
622
|
+
.ai-session-item { display: flex; align-items: baseline; gap: 5px; margin-bottom: 3px; flex-wrap: wrap; }
|
|
623
|
+
.ai-session-num { font-size: 9px; color: #374151; flex-shrink: 0; }
|
|
624
|
+
.ai-session-q { font-size: 10px; color: #4b5563; }
|
|
625
|
+
.ai-session-r { font-size: 10px; color: #374151; }
|
|
626
|
+
|
|
614
627
|
/* Footer */
|
|
615
628
|
.ai-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
|
|
616
629
|
.ai-retry-btn { font-size: 12px; color: #a78bfa; background: none; border: none; cursor: pointer; padding: 0; }
|
|
@@ -904,14 +917,69 @@ a:hover { color: var(--link-h); }
|
|
|
904
917
|
}
|
|
905
918
|
.footer-link:hover { color: var(--text2); }
|
|
906
919
|
|
|
920
|
+
/* ─── Mobile bottom bar ───────────────────────────────────────────────────── */
|
|
921
|
+
.mobile-bar {
|
|
922
|
+
display: none;
|
|
923
|
+
}
|
|
924
|
+
.mobile-logo {
|
|
925
|
+
font-size: 15px;
|
|
926
|
+
font-weight: 300;
|
|
927
|
+
letter-spacing: -0.2px;
|
|
928
|
+
color: var(--text);
|
|
929
|
+
white-space: nowrap;
|
|
930
|
+
user-select: none;
|
|
931
|
+
cursor: pointer;
|
|
932
|
+
}
|
|
933
|
+
.mobile-logo strong {
|
|
934
|
+
font-weight: 700;
|
|
935
|
+
background: linear-gradient(90deg, #a78bfa, #818cf8);
|
|
936
|
+
-webkit-background-clip: text;
|
|
937
|
+
-webkit-text-fill-color: transparent;
|
|
938
|
+
background-clip: text;
|
|
939
|
+
}
|
|
940
|
+
@media (max-width: 640px) {
|
|
941
|
+
.mobile-bar {
|
|
942
|
+
display: flex;
|
|
943
|
+
flex-direction: column;
|
|
944
|
+
position: fixed;
|
|
945
|
+
bottom: 0; left: 0; right: 0;
|
|
946
|
+
background: var(--bg);
|
|
947
|
+
border-top: 1px solid var(--border2);
|
|
948
|
+
box-shadow: 0 -12px 28px rgba(0,0,0,0.45);
|
|
949
|
+
z-index: 220;
|
|
950
|
+
padding-bottom: max(6px, env(safe-area-inset-bottom, 0px));
|
|
951
|
+
transform: translateZ(0);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
.mobile-bar-search { padding: 8px 12px 4px; }
|
|
955
|
+
.mobile-bar-search .search-form { width: 100%; }
|
|
956
|
+
.mobile-bar-tabs {
|
|
957
|
+
display: flex;
|
|
958
|
+
align-items: center;
|
|
959
|
+
gap: 4px;
|
|
960
|
+
padding: 2px 12px 4px;
|
|
961
|
+
overflow-x: auto;
|
|
962
|
+
scrollbar-width: none;
|
|
963
|
+
}
|
|
964
|
+
.mobile-bar-tabs::-webkit-scrollbar { display: none; }
|
|
965
|
+
.mobile-bar-row {
|
|
966
|
+
display: flex;
|
|
967
|
+
align-items: center;
|
|
968
|
+
gap: 6px;
|
|
969
|
+
padding: 4px 12px 2px;
|
|
970
|
+
}
|
|
971
|
+
.mobile-bar-row .engine-picker { flex-shrink: 0; }
|
|
972
|
+
.mobile-bar-row .lang-wrap { margin-left: auto; }
|
|
973
|
+
|
|
907
974
|
/* ─── Responsive ──────────────────────────────────────────────────────────── */
|
|
908
975
|
@media (max-width: 640px) {
|
|
909
|
-
|
|
910
|
-
.category-tabs {
|
|
976
|
+
/* Hide desktop header + category bar — mobile uses bottom bar */
|
|
977
|
+
.header.hide-mobile, .category-tabs.hide-mobile { display: none !important; }
|
|
978
|
+
.main { padding-bottom: calc(20px + var(--mobile-bar-height, 0px) + env(safe-area-inset-bottom, 0px)); }
|
|
979
|
+
|
|
911
980
|
.cat-tab { font-size: 10px; padding: 4px 8px; }
|
|
912
|
-
.engine-picker {
|
|
913
|
-
.engine-picker-
|
|
914
|
-
.engine-picker-body { width: calc(100vw - 24px); right: -6px; }
|
|
981
|
+
.mobile-bar-row .engine-picker-summary { font-size: 11px; padding: 3px 6px; }
|
|
982
|
+
.engine-picker-body { width: calc(100vw - 24px); right: -6px; bottom: calc(100% + 8px); top: auto; }
|
|
915
983
|
.logo-text { font-size: 15px; }
|
|
916
984
|
.home-logo { font-size: 40px; }
|
|
917
985
|
.home-tagline { letter-spacing: 0.08em; margin-bottom: 20px; }
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -17,9 +17,15 @@ function ok(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
|
|
|
17
17
|
function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
|
|
18
18
|
function info(msg) { console.log(` ${CYAN}→${RESET} ${msg}`); }
|
|
19
19
|
|
|
20
|
+
let VERSION = '0.0.0';
|
|
21
|
+
try {
|
|
22
|
+
const pkgPath = new URL('../package.json', import.meta.url);
|
|
23
|
+
VERSION = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || VERSION;
|
|
24
|
+
} catch { /* ignore */ }
|
|
25
|
+
|
|
20
26
|
try {
|
|
21
27
|
console.log('');
|
|
22
|
-
console.log(`${BOLD} TermSearch — post-install check
|
|
28
|
+
console.log(`${BOLD} TermSearch v${VERSION}${RESET} — post-install check`);
|
|
23
29
|
console.log('');
|
|
24
30
|
|
|
25
31
|
// ── Node.js version ──────────────────────────────────────────────────────
|
|
@@ -76,7 +82,7 @@ try {
|
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
console.log('');
|
|
79
|
-
info(`Run ${BOLD}termsearch${RESET}${CYAN}
|
|
85
|
+
info(`Run ${BOLD}termsearch${RESET}${CYAN} to start v${VERSION} → http://localhost:3000`);
|
|
80
86
|
console.log('');
|
|
81
87
|
|
|
82
88
|
} catch {
|