termsearch 0.3.3 → 0.3.4
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/frontend/dist/app.js +122 -27
- package/frontend/dist/style.css +76 -7
- package/package.json +1 -1
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);
|
|
@@ -443,32 +485,41 @@ function renderAiPanel() {
|
|
|
443
485
|
const isDone = state.aiStatus === 'done';
|
|
444
486
|
const isError = state.aiStatus === 'error';
|
|
445
487
|
const dotsClass = isDone ? 'done' : isError ? 'error' : '';
|
|
446
|
-
const statusText = state.aiStatus === 'loading' ? 'Thinking…'
|
|
447
|
-
: state.aiStatus === 'streaming' ? 'Generating…'
|
|
448
|
-
: isDone ? 'AI Summary' : 'Error';
|
|
449
488
|
|
|
450
|
-
//
|
|
489
|
+
// Row 1: dots + "AI" (always) + model + latency
|
|
451
490
|
const dotsEl = el('div', { className: 'ai-dots' });
|
|
452
491
|
['violet', 'indigo', 'dim'].forEach(c => {
|
|
453
492
|
dotsEl.append(el('div', { className: `ai-dot ${dotsClass || c}` }));
|
|
454
493
|
});
|
|
455
|
-
|
|
456
|
-
// Latency
|
|
457
494
|
const latMs = state.aiLatencyMs;
|
|
458
495
|
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' },
|
|
496
|
+
const row1 = el('div', { className: 'panel-header-left' },
|
|
462
497
|
dotsEl,
|
|
463
|
-
el('span', { className: 'panel-label' },
|
|
498
|
+
el('span', { className: 'panel-label' }, 'AI'),
|
|
464
499
|
state.aiMeta?.model ? el('span', { className: 'ai-model-label' }, state.aiMeta.model) : null,
|
|
465
500
|
latLabel ? el('span', { className: 'ai-latency-label' }, `· ${latLabel}`) : null,
|
|
466
501
|
);
|
|
502
|
+
|
|
503
|
+
// Row 2: status text (violet) + step/fetch meta
|
|
504
|
+
const statusText = isError ? 'Error'
|
|
505
|
+
: isDone ? 'Done'
|
|
506
|
+
: state.aiStatus === 'loading' ? 'Thinking…' : 'Generating…';
|
|
507
|
+
const statusColor = isError ? '#f87171' : isDone ? '#a78bfa' : '#a78bfa';
|
|
508
|
+
const lastStep = state.aiSteps.length > 0 ? state.aiSteps[state.aiSteps.length - 1] : null;
|
|
509
|
+
const metaText = isDone && state.aiMeta?.fetchedCount ? `· ${state.aiMeta.fetchedCount} pages read`
|
|
510
|
+
: isLoading && lastStep ? `· ${lastStep}` : '';
|
|
511
|
+
const row2 = el('div', { className: 'ai-status-row' },
|
|
512
|
+
el('span', { className: 'ai-status-text', style: `color:${statusColor}` }, statusText),
|
|
513
|
+
metaText ? el('span', { className: 'ai-status-meta' }, metaText) : null,
|
|
514
|
+
);
|
|
515
|
+
|
|
467
516
|
const chevronPath = state.aiExpanded ? '<polyline points="18 15 12 9 6 15"/>' : '<polyline points="6 9 12 15 18 9"/>';
|
|
468
517
|
const expandBtn = el('button', { className: 'ai-expand-btn', type: 'button', title: state.aiExpanded ? 'Collapse' : 'Expand' });
|
|
469
518
|
expandBtn.innerHTML = svg(chevronPath, 14);
|
|
470
519
|
expandBtn.onclick = () => { state.aiExpanded = !state.aiExpanded; renderAiPanel(); };
|
|
471
|
-
|
|
520
|
+
|
|
521
|
+
const headerInner = el('div', { className: 'ai-header-inner' }, row1, row2);
|
|
522
|
+
const header = el('div', { className: 'panel-header' }, headerInner, expandBtn);
|
|
472
523
|
|
|
473
524
|
// Progress bar
|
|
474
525
|
const showProgress = isLoading && state.aiProgress > 0;
|
|
@@ -509,6 +560,19 @@ function renderAiPanel() {
|
|
|
509
560
|
}),
|
|
510
561
|
) : null;
|
|
511
562
|
|
|
563
|
+
// Session memory (shown when expanded + more than 1 entry, all except current)
|
|
564
|
+
const prevSession = state.aiSession.slice(0, -1);
|
|
565
|
+
const sessionEl = (state.aiExpanded && prevSession.length > 0) ? el('div', { className: 'ai-session' },
|
|
566
|
+
el('p', { className: 'ai-session-label' }, 'Session'),
|
|
567
|
+
...prevSession.map((item, i) =>
|
|
568
|
+
el('div', { className: 'ai-session-item' },
|
|
569
|
+
el('span', { className: 'ai-session-num' }, `${i + 1}.`),
|
|
570
|
+
el('span', { className: 'ai-session-q' }, item.q),
|
|
571
|
+
el('span', { className: 'ai-session-r' }, `→ ${item.r}`),
|
|
572
|
+
)
|
|
573
|
+
),
|
|
574
|
+
) : null;
|
|
575
|
+
|
|
512
576
|
// Footer: retry + expand/collapse
|
|
513
577
|
const retryBtn = el('button', { className: 'ai-retry-btn', type: 'button' }, 'Retry');
|
|
514
578
|
retryBtn.onclick = () => {
|
|
@@ -526,6 +590,7 @@ function renderAiPanel() {
|
|
|
526
590
|
if (stepsEl) panel.append(stepsEl);
|
|
527
591
|
panel.append(contentEl);
|
|
528
592
|
if (sourcesEl) panel.append(sourcesEl);
|
|
593
|
+
if (sessionEl) panel.append(sessionEl);
|
|
529
594
|
panel.append(footer);
|
|
530
595
|
|
|
531
596
|
if (state.aiStatus === 'streaming' && state.aiSummary.length < 60) {
|
|
@@ -887,7 +952,7 @@ async function startAiSummary(query, results, lang) {
|
|
|
887
952
|
const r = await fetch('/api/ai-summary', {
|
|
888
953
|
method: 'POST',
|
|
889
954
|
headers: { 'Content-Type': 'application/json' },
|
|
890
|
-
body: JSON.stringify({ query, lang, results: results.slice(0, 10), stream: true }),
|
|
955
|
+
body: JSON.stringify({ query, lang, results: results.slice(0, 10), stream: true, session: state.aiSession }),
|
|
891
956
|
});
|
|
892
957
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
893
958
|
|
|
@@ -917,6 +982,12 @@ async function startAiSummary(query, results, lang) {
|
|
|
917
982
|
state.aiSources = Array.isArray(d.sites) ? d.sites.map(sanitizeHttpUrl).filter(Boolean) : [];
|
|
918
983
|
state.aiMeta = { fetchedCount: d.fetchedCount, model: d.model };
|
|
919
984
|
state.aiLatencyMs = Date.now() - state.aiStartTime;
|
|
985
|
+
// Save to session memory (max 4 entries, skip if already saved for this query)
|
|
986
|
+
const lastEntry = state.aiSession[state.aiSession.length - 1];
|
|
987
|
+
if (state.aiSummary && (!lastEntry || lastEntry.q !== query)) {
|
|
988
|
+
const r = state.aiSummary.split(/[.!?\n]/)[0].slice(0, 100);
|
|
989
|
+
state.aiSession = [...state.aiSession.slice(-3), { q: query, r }];
|
|
990
|
+
}
|
|
920
991
|
renderAiPanel();
|
|
921
992
|
}
|
|
922
993
|
} catch { /* ignore */ }
|
|
@@ -937,12 +1008,13 @@ function renderApp() {
|
|
|
937
1008
|
app.innerHTML = '';
|
|
938
1009
|
|
|
939
1010
|
if (!state.query) {
|
|
1011
|
+
clearMobileBarLayout();
|
|
940
1012
|
renderHome(app);
|
|
941
1013
|
return;
|
|
942
1014
|
}
|
|
943
1015
|
|
|
944
1016
|
// Results page
|
|
945
|
-
const header = el('div', { className: 'header' },
|
|
1017
|
+
const header = el('div', { className: 'header hide-mobile' },
|
|
946
1018
|
el('div', { className: 'logo-text', onClick: () => { state.query = ''; state.category = 'web'; navigate('#/'); renderApp(); } },
|
|
947
1019
|
'Term', el('strong', {}, 'Search'),
|
|
948
1020
|
),
|
|
@@ -953,26 +1025,47 @@ function renderApp() {
|
|
|
953
1025
|
el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
|
|
954
1026
|
),
|
|
955
1027
|
);
|
|
956
|
-
|
|
1028
|
+
|
|
1029
|
+
const categoryBar = el('div', { className: 'category-tabs hide-mobile' });
|
|
957
1030
|
const categories = [
|
|
958
1031
|
{ id: 'web', label: 'Web' },
|
|
959
1032
|
{ id: 'images', label: 'Images' },
|
|
960
1033
|
{ id: 'news', label: 'News' },
|
|
961
1034
|
];
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1035
|
+
const buildCatTabs = (container) => {
|
|
1036
|
+
categories.forEach((cat) => {
|
|
1037
|
+
container.append(el('button', {
|
|
1038
|
+
className: `cat-tab ${state.category === cat.id ? 'active' : ''}`,
|
|
1039
|
+
onClick: () => {
|
|
1040
|
+
if (state.category === cat.id) return;
|
|
1041
|
+
state.category = cat.id;
|
|
1042
|
+
navigate(buildSearchHash(state.query, state.category));
|
|
1043
|
+
if (state.query) doSearch(state.query, state.category);
|
|
1044
|
+
},
|
|
1045
|
+
type: 'button',
|
|
1046
|
+
}, cat.label));
|
|
1047
|
+
});
|
|
1048
|
+
};
|
|
1049
|
+
buildCatTabs(categoryBar);
|
|
974
1050
|
categoryBar.append(EnginePicker());
|
|
975
1051
|
|
|
1052
|
+
const mobileTabs = el('div', { className: 'mobile-bar-tabs' });
|
|
1053
|
+
buildCatTabs(mobileTabs);
|
|
1054
|
+
const mobileBar = el('div', { className: 'mobile-bar' },
|
|
1055
|
+
el('div', { className: 'mobile-bar-search' }, SearchForm(state.query, (q, cat) => { state.query = q; doSearch(q, cat); })),
|
|
1056
|
+
mobileTabs,
|
|
1057
|
+
el('div', { className: 'mobile-bar-engine' }, EnginePicker()),
|
|
1058
|
+
el('div', { className: 'mobile-bar-row' },
|
|
1059
|
+
el('div', {
|
|
1060
|
+
className: 'mobile-logo',
|
|
1061
|
+
onClick: () => { state.query = ''; state.category = 'web'; navigate('#/'); renderApp(); },
|
|
1062
|
+
}, 'Term', el('strong', {}, 'Search')),
|
|
1063
|
+
LangPicker(),
|
|
1064
|
+
el('button', { className: 'btn-icon', title: 'Settings', onClick: () => navigate('#/settings') }, iconEl('settings')),
|
|
1065
|
+
el('button', { className: 'btn-icon', title: 'Toggle theme', onClick: toggleTheme }, iconEl('theme')),
|
|
1066
|
+
),
|
|
1067
|
+
);
|
|
1068
|
+
|
|
976
1069
|
const main = el('div', { className: 'main' });
|
|
977
1070
|
|
|
978
1071
|
// AI panel placeholder
|
|
@@ -1016,7 +1109,8 @@ function renderApp() {
|
|
|
1016
1109
|
if (socPanel) main.append(socPanel);
|
|
1017
1110
|
}
|
|
1018
1111
|
|
|
1019
|
-
app.append(header, categoryBar, main);
|
|
1112
|
+
app.append(header, categoryBar, main, mobileBar);
|
|
1113
|
+
bindMobileBarLayout(mobileBar);
|
|
1020
1114
|
renderAiPanel();
|
|
1021
1115
|
}
|
|
1022
1116
|
|
|
@@ -1051,6 +1145,7 @@ async function renderSettings() {
|
|
|
1051
1145
|
const app = document.getElementById('app');
|
|
1052
1146
|
if (!app) return;
|
|
1053
1147
|
app.innerHTML = '';
|
|
1148
|
+
clearMobileBarLayout();
|
|
1054
1149
|
|
|
1055
1150
|
let cfg = state.config;
|
|
1056
1151
|
if (!cfg) {
|
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,70 @@ 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-engine { padding: 0 12px 4px; }
|
|
966
|
+
.mobile-bar-row {
|
|
967
|
+
display: flex;
|
|
968
|
+
align-items: center;
|
|
969
|
+
gap: 6px;
|
|
970
|
+
padding: 4px 12px 2px;
|
|
971
|
+
}
|
|
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 { width: 100%; margin-left: 0; }
|
|
913
|
-
.engine-picker-summary { width: 100%; justify-content: space-between; }
|
|
914
|
-
.engine-picker-body { width: calc(100vw - 24px); right: -6px; }
|
|
981
|
+
.mobile-bar-engine .engine-picker { width: 100%; margin-left: 0; }
|
|
982
|
+
.mobile-bar-engine .engine-picker-summary { width: 100%; justify-content: space-between; }
|
|
983
|
+
.engine-picker-body { width: calc(100vw - 24px); right: -6px; bottom: calc(100% + 8px); top: auto; }
|
|
915
984
|
.logo-text { font-size: 15px; }
|
|
916
985
|
.home-logo { font-size: 40px; }
|
|
917
986
|
.home-tagline { letter-spacing: 0.08em; margin-bottom: 20px; }
|