RJuro Claude Opus 4.6 commited on
Commit
33e7dcd
·
1 Parent(s): c19484b

Redesign frontend: brutalist B&W with red accent

Browse files

Edgy editorial aesthetic — Bebas Neue + JetBrains Mono typography,
animated SVG vector background (grid, floating geometry, constellation
nodes, scan line), pure black/white with single #E63327 red accent.
Zero JS dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. static/index.html +443 -191
static/index.html CHANGED
@@ -3,209 +3,463 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SciFact Multilingual Semantic Search</title>
 
 
7
  <style>
8
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
9
- body{
10
- min-height:100vh;
11
- font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,sans-serif;
12
- overflow-x:hidden;
13
- background:#0f0a1a;
14
- color:#e2e0ea;
 
 
15
  }
16
 
17
- /* ── Animated gradient background ── */
18
- .bg{
19
- position:fixed;inset:0;z-index:0;
20
- background:linear-gradient(135deg,#1a1040 0%,#0f0a1a 30%,#0a1628 60%,#0f0a1a 100%);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
 
 
22
 
23
- /* ── Floating blobs ── */
24
- .blob{
25
- position:fixed;border-radius:50%;filter:blur(80px);opacity:.35;
26
- animation:drift 20s ease-in-out infinite alternate;z-index:0;
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
- .blob-1{width:500px;height:500px;background:#6366f1;top:-120px;left:-100px;animation-duration:22s}
29
- .blob-2{width:400px;height:400px;background:#8b5cf6;bottom:-80px;right:-80px;animation-duration:18s;animation-delay:-5s}
30
- .blob-3{width:300px;height:300px;background:#14b8a6;top:40%;left:60%;animation-duration:25s;animation-delay:-10s}
31
- @keyframes drift{
32
- 0%{transform:translate(0,0) scale(1)}
33
- 33%{transform:translate(30px,-40px) scale(1.05)}
34
- 66%{transform:translate(-20px,30px) scale(.95)}
35
- 100%{transform:translate(10px,-10px) scale(1.02)}
36
  }
37
 
38
  /* ── Layout ── */
39
- .container{
40
- position:relative;z-index:1;
41
- max-width:720px;margin:0 auto;padding:40px 20px 60px;
 
42
  }
43
 
44
  /* ── Header ── */
45
- .header{text-align:center;margin-bottom:32px}
46
- .header h1{
47
- font-size:1.75rem;font-weight:700;
48
- background:linear-gradient(135deg,#a78bfa,#6366f1,#14b8a6);
49
- -webkit-background-clip:text;-webkit-text-fill-color:transparent;
50
- margin-bottom:6px;
51
- }
52
- .header p{font-size:.92rem;color:#9f9bb0}
53
-
54
- /* ── Glass card ── */
55
- .glass{
56
- background:rgba(255,255,255,.06);
57
- backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
58
- border:1px solid rgba(255,255,255,.1);
59
- border-radius:16px;
60
- padding:24px;
61
- }
62
-
63
- /* ── Search box ── */
64
- .search-box{display:flex;gap:10px;margin-bottom:16px}
65
- .search-box input{
66
- flex:1;padding:14px 18px;
67
- background:rgba(255,255,255,.07);
68
- border:1.5px solid rgba(255,255,255,.12);
69
- border-radius:12px;color:#e2e0ea;
70
- font-size:1rem;outline:none;
71
- transition:border-color .2s,box-shadow .2s;
72
- }
73
- .search-box input::placeholder{color:#7a7490}
74
- .search-box input:focus{
75
- border-color:#6366f1;
76
- box-shadow:0 0 0 3px rgba(99,102,241,.25);
77
- }
78
- .search-box button{
79
- padding:0 22px;
80
- background:linear-gradient(135deg,#6366f1,#8b5cf6);
81
- border:none;border-radius:12px;color:#fff;
82
- font-size:.95rem;font-weight:600;cursor:pointer;
83
- transition:opacity .2s;white-space:nowrap;
84
- }
85
- .search-box button:hover{opacity:.88}
86
- .search-box button:disabled{opacity:.5;cursor:not-allowed}
87
-
88
- /* ── Language chips ── */
89
- .chips{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:4px}
90
- .chip{
91
- padding:6px 14px;
92
- background:rgba(255,255,255,.07);
93
- border:1px solid rgba(255,255,255,.1);
94
- border-radius:20px;font-size:.8rem;color:#c4bfd6;
95
- cursor:pointer;transition:background .2s,color .2s;
96
- }
97
- .chip:hover{background:rgba(99,102,241,.25);color:#e2e0ea}
98
- .chip span{font-weight:600;margin-right:4px;color:#a78bfa}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  /* ── Results ── */
101
- .results{margin-top:24px;display:flex;flex-direction:column;gap:14px}
102
 
103
- .result-card{
104
- background:rgba(255,255,255,.05);
105
- backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
106
- border:1px solid rgba(255,255,255,.08);
107
- border-radius:14px;padding:18px 20px;
108
- transition:border-color .2s;
 
 
 
109
  }
110
- .result-card:hover{border-color:rgba(99,102,241,.35)}
111
 
112
- .result-header{display:flex;align-items:center;gap:10px;margin-bottom:8px}
113
- .rank-badge{
114
- width:28px;height:28px;border-radius:8px;
115
- display:flex;align-items:center;justify-content:center;
116
- font-size:.78rem;font-weight:700;color:#fff;flex-shrink:0;
117
- background:linear-gradient(135deg,#6366f1,#8b5cf6);
118
  }
119
- .result-title{font-size:.95rem;font-weight:600;color:#e2e0ea;line-height:1.3}
120
 
121
- .score-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
122
- .score-bar-bg{
123
- flex:1;height:6px;border-radius:3px;
124
- background:rgba(255,255,255,.08);overflow:hidden;
 
125
  }
126
- .score-bar{
127
- height:100%;border-radius:3px;
128
- background:linear-gradient(90deg,#6366f1,#14b8a6);
129
- transition:width .6s ease;
 
 
 
 
 
 
 
 
130
  }
131
- .score-label{font-size:.78rem;color:#a78bfa;font-weight:600;white-space:nowrap}
132
 
133
- .result-text{font-size:.85rem;color:#9f9bb0;line-height:1.55}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  /* ── Empty state ── */
136
- .empty-state{text-align:center;padding:40px 20px}
137
- .empty-state .icon{font-size:2.5rem;margin-bottom:12px}
138
- .empty-state h3{font-size:1.1rem;color:#c4bfd6;margin-bottom:8px}
139
- .empty-state p{font-size:.88rem;color:#7a7490;line-height:1.5;max-width:400px;margin:0 auto}
140
-
141
- /* ── Skeleton loading ── */
142
- .skeleton-card{
143
- background:rgba(255,255,255,.04);
144
- border:1px solid rgba(255,255,255,.06);
145
- border-radius:14px;padding:20px;
146
- animation:pulse 1.5s ease-in-out infinite;
147
- }
148
- .skeleton-line{
149
- height:12px;border-radius:6px;
150
- background:rgba(255,255,255,.08);margin-bottom:10px;
151
- }
152
- .skeleton-line.short{width:40%}
153
- .skeleton-line.medium{width:70%}
154
- .skeleton-line.long{width:100%}
155
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
156
-
157
- /* ── Info bar ── */
158
- .info-bar{
159
- text-align:center;margin-top:28px;
160
- font-size:.78rem;color:#5e5874;
161
- }
162
-
163
- @media(max-width:500px){
164
- .container{padding:24px 14px 40px}
165
- .header h1{font-size:1.35rem}
166
- .search-box{flex-direction:column}
167
- .search-box button{padding:12px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
  </style>
170
  </head>
171
  <body>
172
- <div class="bg"></div>
173
- <div class="blob blob-1"></div>
174
- <div class="blob blob-2"></div>
175
- <div class="blob blob-3"></div>
176
-
177
- <div class="container">
178
- <div class="header">
179
- <h1>SciFact Semantic Search</h1>
180
- <p>Multilingual search over 5,183 scientific abstracts &mdash; powered by E5-small</p>
181
- </div>
182
 
183
- <div class="glass">
184
- <div class="search-box">
185
- <input id="q" type="text" placeholder="Search scientific literature..." autocomplete="off">
186
- <button id="btn" onclick="doSearch()">Search</button>
187
- </div>
188
- <div class="chips">
189
- <div class="chip" onclick="chipSearch(this)"><span>EN</span> effects of vaccination</div>
190
- <div class="chip" onclick="chipSearch(this)"><span>FR</span> effets de la vaccination</div>
191
- <div class="chip" onclick="chipSearch(this)"><span>DE</span> Auswirkungen der Impfung</div>
192
- <div class="chip" onclick="chipSearch(this)"><span>ES</span> efectos de la vacunaci&oacute;n</div>
193
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  </div>
 
 
195
 
196
- <div id="results" class="results">
197
- <div class="empty-state">
198
- <div class="icon">&#128300;</div>
199
- <h3>Multilingual Semantic Search</h3>
200
- <p>Search 5,183 SciFact abstracts in any language. The model understands meaning, not just keywords. Try a query above or click a language chip.</p>
201
- </div>
202
  </div>
 
 
 
 
 
 
 
203
 
204
- <div class="info-bar">
205
- intfloat/multilingual-e5-small &middot; ChromaDB &middot; FastAPI &middot; CPU inference
 
 
 
 
 
 
 
 
206
  </div>
207
  </div>
208
 
 
 
 
 
 
 
 
 
209
  <script>
210
  const input = document.getElementById('q');
211
  const btn = document.getElementById('btn');
@@ -214,8 +468,7 @@ const results = document.getElementById('results');
214
  input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
215
 
216
  function chipSearch(el) {
217
- // Get text after the language tag span
218
- const text = el.textContent.replace(/^[A-Z]{2}\s*/, '').trim();
219
  input.value = text;
220
  doSearch();
221
  }
@@ -229,48 +482,47 @@ async function doSearch() {
229
  results.innerHTML = skeletonHTML();
230
 
231
  try {
232
- const res = await fetch(`/search?q=${encodeURIComponent(q)}&top_k=5`);
233
  if (!res.ok) throw new Error(res.statusText);
234
  const data = await res.json();
235
  renderResults(data.results);
236
  } catch (err) {
237
- results.innerHTML = `<div class="empty-state"><div class="icon">&#9888;&#65039;</div><h3>Search failed</h3><p>${err.message}</p></div>`;
238
  } finally {
239
  btn.disabled = false;
240
- btn.textContent = 'Search';
241
  }
242
  }
243
 
244
  function skeletonHTML() {
245
  let h = '';
246
- for (let i = 0; i < 3; i++) {
247
- h += `<div class="skeleton-card">
248
- <div class="skeleton-line short"></div>
249
- <div class="skeleton-line long"></div>
250
- <div class="skeleton-line medium"></div>
251
- </div>`;
252
  }
253
  return h;
254
  }
255
 
256
  function renderResults(items) {
257
  if (!items || !items.length) {
258
- results.innerHTML = `<div class="empty-state"><div class="icon">&#128269;</div><h3>No results</h3><p>Try a different query.</p></div>`;
259
  return;
260
  }
261
- results.innerHTML = items.map(r => {
262
  const pct = Math.max(0, Math.min(100, r.score * 100));
263
- return `<div class="result-card">
264
- <div class="result-header">
265
- <div class="rank-badge">${r.rank}</div>
266
- <div class="result-title">${esc(r.title)}</div>
267
- </div>
268
- <div class="score-row">
269
- <div class="score-bar-bg"><div class="score-bar" style="width:${pct}%"></div></div>
270
- <div class="score-label">${(pct).toFixed(1)}%</div>
271
- </div>
272
- <div class="result-text">${esc(r.text)}</div>
273
- </div>`;
274
  }).join('');
275
  }
276
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SciFact Semantic Search</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=JetBrains+Mono:wght@400;600&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
9
  <style>
10
+ :root {
11
+ --red: #E63327;
12
+ --black: #0A0A0A;
13
+ --white: #F5F5F0;
14
+ --gray-1: #1A1A1A;
15
+ --gray-2: #2A2A2A;
16
+ --gray-3: #555;
17
+ --gray-4: #888;
18
+ --gray-5: #BBB;
19
  }
20
 
21
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
22
+
23
+ body {
24
+ min-height: 100vh;
25
+ background: var(--black);
26
+ color: var(--white);
27
+ font-family: 'Outfit', sans-serif;
28
+ font-weight: 300;
29
+ overflow-x: hidden;
30
+ -webkit-font-smoothing: antialiased;
31
+ }
32
+
33
+ /* ── SVG Vector Background ── */
34
+ .vector-bg {
35
+ position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: .12;
36
+ }
37
+ .vector-bg line {
38
+ stroke: var(--white);
39
+ stroke-width: .5;
40
+ }
41
+ .vector-bg circle {
42
+ fill: none;
43
+ stroke: var(--white);
44
+ stroke-width: .5;
45
+ }
46
+ .vector-bg .node {
47
+ fill: var(--red);
48
+ opacity: .8;
49
+ }
50
+
51
+ /* Grid pulse */
52
+ @keyframes gridFade {
53
+ 0%, 100% { opacity: .08; }
54
+ 50% { opacity: .18; }
55
  }
56
+ .grid-line { animation: gridFade 6s ease-in-out infinite; }
57
+ .grid-line:nth-child(odd) { animation-delay: -3s; }
58
 
59
+ /* Floating geometry */
60
+ @keyframes float1 { 0%,100%{transform:translate(0,0) rotate(0deg)} 50%{transform:translate(15px,-25px) rotate(180deg)} }
61
+ @keyframes float2 { 0%,100%{transform:translate(0,0) rotate(0deg)} 50%{transform:translate(-20px,20px) rotate(-120deg)} }
62
+ @keyframes float3 { 0%,100%{transform:translate(0,0) rotate(0deg)} 50%{transform:translate(10px,30px) rotate(90deg)} }
63
+ @keyframes orbitPulse { 0%,100%{r:2;opacity:.6} 50%{r:3.5;opacity:1} }
64
+
65
+ .geo-float-1 { animation: float1 18s ease-in-out infinite; }
66
+ .geo-float-2 { animation: float2 22s ease-in-out infinite; }
67
+ .geo-float-3 { animation: float3 15s ease-in-out infinite; }
68
+
69
+ /* Scan line */
70
+ @keyframes scanDown {
71
+ 0% { transform: translateY(-100vh); opacity: 0; }
72
+ 10% { opacity: 1; }
73
+ 90% { opacity: 1; }
74
+ 100% { transform: translateY(100vh); opacity: 0; }
75
  }
76
+ .scan-line {
77
+ animation: scanDown 8s linear infinite;
 
 
 
 
 
 
78
  }
79
 
80
  /* ── Layout ── */
81
+ .container {
82
+ position: relative; z-index: 1;
83
+ max-width: 760px; margin: 0 auto;
84
+ padding: 48px 24px 72px;
85
  }
86
 
87
  /* ── Header ── */
88
+ .header {
89
+ margin-bottom: 48px;
90
+ border-bottom: 1px solid var(--gray-2);
91
+ padding-bottom: 32px;
92
+ }
93
+ .header-top {
94
+ display: flex;
95
+ align-items: flex-end;
96
+ justify-content: space-between;
97
+ gap: 16px;
98
+ margin-bottom: 12px;
99
+ }
100
+ .header h1 {
101
+ font-family: 'Bebas Neue', sans-serif;
102
+ font-size: 4.2rem;
103
+ font-weight: 400;
104
+ letter-spacing: 2px;
105
+ line-height: .9;
106
+ color: var(--white);
107
+ }
108
+ .header h1 .accent {
109
+ color: var(--red);
110
+ }
111
+ .header .tag {
112
+ font-family: 'JetBrains Mono', monospace;
113
+ font-size: .65rem;
114
+ color: var(--gray-4);
115
+ letter-spacing: 1px;
116
+ text-transform: uppercase;
117
+ white-space: nowrap;
118
+ padding-bottom: 6px;
119
+ }
120
+ .header .subtitle {
121
+ font-size: .88rem;
122
+ color: var(--gray-4);
123
+ font-weight: 300;
124
+ letter-spacing: .3px;
125
+ }
126
+
127
+ /* ── Search ── */
128
+ .search-area { margin-bottom: 40px; }
129
+ .search-row {
130
+ display: flex;
131
+ gap: 0;
132
+ border: 1px solid var(--gray-2);
133
+ transition: border-color .2s;
134
+ }
135
+ .search-row:focus-within {
136
+ border-color: var(--red);
137
+ }
138
+ .search-row input {
139
+ flex: 1;
140
+ padding: 16px 20px;
141
+ background: transparent;
142
+ border: none;
143
+ color: var(--white);
144
+ font-family: 'Outfit', sans-serif;
145
+ font-size: 1rem;
146
+ font-weight: 300;
147
+ outline: none;
148
+ letter-spacing: .3px;
149
+ }
150
+ .search-row input::placeholder { color: var(--gray-3); }
151
+ .search-row button {
152
+ padding: 16px 28px;
153
+ background: var(--red);
154
+ border: none;
155
+ color: var(--white);
156
+ font-family: 'Bebas Neue', sans-serif;
157
+ font-size: 1.05rem;
158
+ letter-spacing: 2px;
159
+ cursor: pointer;
160
+ transition: background .15s;
161
+ }
162
+ .search-row button:hover { background: #C42A20; }
163
+ .search-row button:disabled { background: var(--gray-2); color: var(--gray-3); cursor: not-allowed; }
164
+
165
+ /* ── Chips ── */
166
+ .chips {
167
+ display: flex;
168
+ flex-wrap: wrap;
169
+ gap: 8px;
170
+ margin-top: 16px;
171
+ }
172
+ .chip {
173
+ padding: 6px 14px;
174
+ border: 1px solid var(--gray-2);
175
+ background: transparent;
176
+ font-family: 'JetBrains Mono', monospace;
177
+ font-size: .7rem;
178
+ color: var(--gray-4);
179
+ cursor: pointer;
180
+ transition: all .15s;
181
+ letter-spacing: .5px;
182
+ }
183
+ .chip:hover {
184
+ border-color: var(--red);
185
+ color: var(--white);
186
+ }
187
+ .chip .lang {
188
+ color: var(--red);
189
+ font-weight: 600;
190
+ margin-right: 6px;
191
+ }
192
 
193
  /* ── Results ── */
194
+ .results { display: flex; flex-direction: column; gap: 0; }
195
 
196
+ .result-card {
197
+ border-top: 1px solid var(--gray-2);
198
+ padding: 24px 0;
199
+ display: grid;
200
+ grid-template-columns: 48px 1fr;
201
+ gap: 0 20px;
202
+ opacity: 0;
203
+ transform: translateY(12px);
204
+ animation: cardIn .4s ease forwards;
205
  }
206
+ .result-card:last-child { border-bottom: 1px solid var(--gray-2); }
207
 
208
+ @keyframes cardIn {
209
+ to { opacity: 1; transform: translateY(0); }
 
 
 
 
210
  }
 
211
 
212
+ .rank-col {
213
+ display: flex;
214
+ flex-direction: column;
215
+ align-items: center;
216
+ padding-top: 2px;
217
  }
218
+ .rank-num {
219
+ font-family: 'Bebas Neue', sans-serif;
220
+ font-size: 1.8rem;
221
+ color: var(--red);
222
+ line-height: 1;
223
+ }
224
+ .rank-line {
225
+ width: 1px;
226
+ flex: 1;
227
+ background: var(--gray-2);
228
+ margin-top: 8px;
229
+ min-height: 20px;
230
  }
 
231
 
232
+ .result-body { min-width: 0; }
233
+ .result-title {
234
+ font-family: 'Outfit', sans-serif;
235
+ font-size: .95rem;
236
+ font-weight: 500;
237
+ color: var(--white);
238
+ line-height: 1.4;
239
+ margin-bottom: 10px;
240
+ }
241
+
242
+ .score-row {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 12px;
246
+ margin-bottom: 12px;
247
+ }
248
+ .score-track {
249
+ flex: 1;
250
+ height: 2px;
251
+ background: var(--gray-2);
252
+ position: relative;
253
+ overflow: hidden;
254
+ }
255
+ .score-fill {
256
+ position: absolute;
257
+ left: 0; top: 0;
258
+ height: 100%;
259
+ background: var(--red);
260
+ transition: width .6s cubic-bezier(.22,1,.36,1);
261
+ }
262
+ .score-val {
263
+ font-family: 'JetBrains Mono', monospace;
264
+ font-size: .72rem;
265
+ color: var(--red);
266
+ font-weight: 600;
267
+ white-space: nowrap;
268
+ min-width: 48px;
269
+ text-align: right;
270
+ }
271
+
272
+ .result-text {
273
+ font-size: .82rem;
274
+ color: var(--gray-4);
275
+ line-height: 1.6;
276
+ font-weight: 300;
277
+ }
278
 
279
  /* ── Empty state ── */
280
+ .empty-state {
281
+ padding: 64px 20px;
282
+ text-align: center;
283
+ border-top: 1px solid var(--gray-2);
284
+ border-bottom: 1px solid var(--gray-2);
285
+ }
286
+ .empty-state svg {
287
+ margin-bottom: 24px;
288
+ opacity: .3;
289
+ }
290
+ .empty-state h3 {
291
+ font-family: 'Bebas Neue', sans-serif;
292
+ font-size: 1.4rem;
293
+ letter-spacing: 2px;
294
+ color: var(--gray-5);
295
+ margin-bottom: 8px;
296
+ font-weight: 400;
297
+ }
298
+ .empty-state p {
299
+ font-size: .85rem;
300
+ color: var(--gray-3);
301
+ line-height: 1.6;
302
+ max-width: 380px;
303
+ margin: 0 auto;
304
+ }
305
+
306
+ /* ── Skeleton ── */
307
+ .skeleton-card {
308
+ border-top: 1px solid var(--gray-2);
309
+ padding: 24px 0;
310
+ display: grid;
311
+ grid-template-columns: 48px 1fr;
312
+ gap: 0 20px;
313
+ animation: pulse 1.5s ease-in-out infinite;
314
+ }
315
+ .skeleton-card:last-child { border-bottom: 1px solid var(--gray-2); }
316
+ .skel-rank {
317
+ width: 24px; height: 32px;
318
+ background: var(--gray-2);
319
+ }
320
+ .skel-line {
321
+ height: 10px;
322
+ background: var(--gray-2);
323
+ margin-bottom: 10px;
324
+ }
325
+ .skel-line.w30 { width: 30%; }
326
+ .skel-line.w80 { width: 80%; }
327
+ .skel-line.w60 { width: 60%; }
328
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
329
+
330
+ /* ── Footer ── */
331
+ .footer {
332
+ margin-top: 48px;
333
+ padding-top: 24px;
334
+ border-top: 1px solid var(--gray-2);
335
+ display: flex;
336
+ justify-content: space-between;
337
+ align-items: center;
338
+ flex-wrap: wrap;
339
+ gap: 8px;
340
+ }
341
+ .footer span {
342
+ font-family: 'JetBrains Mono', monospace;
343
+ font-size: .62rem;
344
+ color: var(--gray-3);
345
+ letter-spacing: .5px;
346
+ text-transform: uppercase;
347
+ }
348
+ .footer .dot { color: var(--red); margin: 0 6px; }
349
+
350
+ /* ── Responsive ── */
351
+ @media (max-width: 560px) {
352
+ .header h1 { font-size: 2.8rem; }
353
+ .header-top { flex-direction: column; align-items: flex-start; }
354
+ .header .tag { padding-bottom: 0; }
355
+ .container { padding: 32px 16px 48px; }
356
+ .search-row { flex-direction: column; }
357
+ .search-row button { padding: 14px; }
358
+ .result-card { grid-template-columns: 36px 1fr; }
359
  }
360
  </style>
361
  </head>
362
  <body>
 
 
 
 
 
 
 
 
 
 
363
 
364
+ <!-- ── Animated SVG Vector Background ── -->
365
+ <svg class="vector-bg" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" viewBox="0 0 1000 1000" width="100%" height="100%">
366
+ <!-- Grid -->
367
+ <g class="grid-line"><line x1="0" y1="200" x2="1000" y2="200"/></g>
368
+ <g class="grid-line"><line x1="0" y1="400" x2="1000" y2="400"/></g>
369
+ <g class="grid-line"><line x1="0" y1="600" x2="1000" y2="600"/></g>
370
+ <g class="grid-line"><line x1="0" y1="800" x2="1000" y2="800"/></g>
371
+ <g class="grid-line"><line x1="200" y1="0" x2="200" y2="1000"/></g>
372
+ <g class="grid-line"><line x1="400" y1="0" x2="400" y2="1000"/></g>
373
+ <g class="grid-line"><line x1="600" y1="0" x2="600" y2="1000"/></g>
374
+ <g class="grid-line"><line x1="800" y1="0" x2="800" y2="1000"/></g>
375
+
376
+ <!-- Floating triangles -->
377
+ <g class="geo-float-1" transform="translate(150,120)">
378
+ <polygon points="0,-20 17,10 -17,10" fill="none" stroke="#E63327" stroke-width=".8" opacity=".5"/>
379
+ </g>
380
+ <g class="geo-float-2" transform="translate(820,300)">
381
+ <polygon points="0,-28 24,14 -24,14" fill="none" stroke="#F5F5F0" stroke-width=".6" opacity=".3"/>
382
+ </g>
383
+ <g class="geo-float-3" transform="translate(700,750)">
384
+ <polygon points="0,-18 16,9 -16,9" fill="none" stroke="#E63327" stroke-width=".7" opacity=".4"/>
385
+ </g>
386
+
387
+ <!-- Floating circles -->
388
+ <g class="geo-float-2" transform="translate(100,650)">
389
+ <circle cx="0" cy="0" r="18" stroke-width=".6" opacity=".25"/>
390
+ </g>
391
+ <g class="geo-float-1" transform="translate(880,150)">
392
+ <circle cx="0" cy="0" r="12" stroke-width=".6" opacity=".2"/>
393
+ </g>
394
+
395
+ <!-- Constellation nodes -->
396
+ <line x1="150" y1="120" x2="400" y2="200" stroke-width=".3" opacity=".15" stroke="#E63327"/>
397
+ <line x1="400" y1="200" x2="600" y2="150" stroke-width=".3" opacity=".1" stroke="#F5F5F0"/>
398
+ <line x1="600" y1="150" x2="820" y2="300" stroke-width=".3" opacity=".12" stroke="#E63327"/>
399
+ <line x1="820" y1="300" x2="700" y2="750" stroke-width=".3" opacity=".08" stroke="#F5F5F0"/>
400
+
401
+ <circle class="node" cx="400" cy="200"><animate attributeName="r" values="2;3.5;2" dur="4s" repeatCount="indefinite"/></circle>
402
+ <circle class="node" cx="600" cy="150"><animate attributeName="r" values="1.5;3;1.5" dur="5s" repeatCount="indefinite"/></circle>
403
+ <circle class="node" cx="200" cy="600"><animate attributeName="r" values="2;4;2" dur="3.5s" repeatCount="indefinite"/></circle>
404
+ <circle class="node" cx="750" cy="500"><animate attributeName="r" values="1.5;3;1.5" dur="4.5s" repeatCount="indefinite"/></circle>
405
+
406
+ <!-- Scan line -->
407
+ <rect class="scan-line" x="0" y="0" width="1000" height="1" fill="url(#scanGrad)"/>
408
+ <defs>
409
+ <linearGradient id="scanGrad" x1="0%" y1="0%" x2="100%" y2="0%">
410
+ <stop offset="0%" stop-color="#E63327" stop-opacity="0"/>
411
+ <stop offset="50%" stop-color="#E63327" stop-opacity=".4"/>
412
+ <stop offset="100%" stop-color="#E63327" stop-opacity="0"/>
413
+ </linearGradient>
414
+ </defs>
415
+ </svg>
416
+
417
+ <div class="container">
418
+
419
+ <!-- Header -->
420
+ <div class="header">
421
+ <div class="header-top">
422
+ <h1>SCI<span class="accent">FACT</span><br>SEARCH</h1>
423
+ <div class="tag">multilingual-e5-small<br>5,183 abstracts</div>
424
  </div>
425
+ <div class="subtitle">Semantic search across scientific literature. Understands meaning, not keywords.</div>
426
+ </div>
427
 
428
+ <!-- Search -->
429
+ <div class="search-area">
430
+ <div class="search-row">
431
+ <input id="q" type="text" placeholder="Enter a query in any language..." autocomplete="off" spellcheck="false">
432
+ <button id="btn" onclick="doSearch()">SEARCH</button>
 
433
  </div>
434
+ <div class="chips">
435
+ <div class="chip" onclick="chipSearch(this)"><span class="lang">EN</span>effects of vaccination</div>
436
+ <div class="chip" onclick="chipSearch(this)"><span class="lang">FR</span>effets de la vaccination</div>
437
+ <div class="chip" onclick="chipSearch(this)"><span class="lang">DE</span>Auswirkungen der Impfung</div>
438
+ <div class="chip" onclick="chipSearch(this)"><span class="lang">ES</span>efectos de la vacunaci&oacute;n</div>
439
+ </div>
440
+ </div>
441
 
442
+ <!-- Results -->
443
+ <div id="results" class="results">
444
+ <div class="empty-state">
445
+ <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
446
+ <circle cx="24" cy="24" r="22" stroke="#F5F5F0" stroke-width="1"/>
447
+ <line x1="12" y1="24" x2="36" y2="24" stroke="#E63327" stroke-width="1"/>
448
+ <line x1="24" y1="12" x2="24" y2="36" stroke="#E63327" stroke-width="1"/>
449
+ </svg>
450
+ <h3>VECTOR SEARCH</h3>
451
+ <p>Cross-lingual semantic retrieval over SciFact scientific abstracts. Type a query or select a language sample above.</p>
452
  </div>
453
  </div>
454
 
455
+ <!-- Footer -->
456
+ <div class="footer">
457
+ <span>intfloat/multilingual-e5-small</span>
458
+ <span>chromadb<span class="dot">/</span>fastapi<span class="dot">/</span>cpu</span>
459
+ </div>
460
+
461
+ </div>
462
+
463
  <script>
464
  const input = document.getElementById('q');
465
  const btn = document.getElementById('btn');
 
468
  input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
469
 
470
  function chipSearch(el) {
471
+ const text = el.textContent.replace(/^[A-Z]{2}/, '').trim();
 
472
  input.value = text;
473
  doSearch();
474
  }
 
482
  results.innerHTML = skeletonHTML();
483
 
484
  try {
485
+ const res = await fetch('/search?q=' + encodeURIComponent(q) + '&top_k=5');
486
  if (!res.ok) throw new Error(res.statusText);
487
  const data = await res.json();
488
  renderResults(data.results);
489
  } catch (err) {
490
+ results.innerHTML = '<div class="empty-state"><h3>ERROR</h3><p>' + esc(err.message) + '</p></div>';
491
  } finally {
492
  btn.disabled = false;
493
+ btn.textContent = 'SEARCH';
494
  }
495
  }
496
 
497
  function skeletonHTML() {
498
  let h = '';
499
+ for (let i = 0; i < 4; i++) {
500
+ h += '<div class="skeleton-card">' +
501
+ '<div><div class="skel-rank"></div></div>' +
502
+ '<div><div class="skel-line w80"></div><div class="skel-line w30"></div><div class="skel-line w60"></div></div>' +
503
+ '</div>';
 
504
  }
505
  return h;
506
  }
507
 
508
  function renderResults(items) {
509
  if (!items || !items.length) {
510
+ results.innerHTML = '<div class="empty-state"><h3>NO RESULTS</h3><p>Try a different query.</p></div>';
511
  return;
512
  }
513
+ results.innerHTML = items.map((r, i) => {
514
  const pct = Math.max(0, Math.min(100, r.score * 100));
515
+ return '<div class="result-card" style="animation-delay:' + (i * .08) + 's">' +
516
+ '<div class="rank-col"><div class="rank-num">0' + r.rank + '</div><div class="rank-line"></div></div>' +
517
+ '<div class="result-body">' +
518
+ '<div class="result-title">' + esc(r.title) + '</div>' +
519
+ '<div class="score-row">' +
520
+ '<div class="score-track"><div class="score-fill" style="width:' + pct + '%"></div></div>' +
521
+ '<div class="score-val">' + pct.toFixed(1) + '%</div>' +
522
+ '</div>' +
523
+ '<div class="result-text">' + esc(r.text) + '</div>' +
524
+ '</div>' +
525
+ '</div>';
526
  }).join('');
527
  }
528