Shaikhsarib commited on
Commit
daf6bea
·
verified ·
1 Parent(s): 6367b02

Delete index.html

Browse files
Files changed (1) hide show
  1. index.html +0 -1807
index.html DELETED
@@ -1,1807 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>Eatlytic AI — Smart Food Analysis</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,900;1,700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
10
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
- <script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
12
- <style>
13
- :root {
14
- --forest: #1E3A2F; --moss: #2D5241; --sage: #4A7C59;
15
- --mint: #7FB896; --cream: #FAF6EE; --parchment: #F0E9DA;
16
- --gold: #C9A84C; --coral: #D96B47; --ink: #1A1A1A;
17
- --mist: #6B7280; --whisper: #E8E0D5; --white: #FFFFFF;
18
- --good: #2D7A4F; --moderate: #B07D22; --caution: #C94B1E; --bad: #8B1A1A;
19
- --radius: 18px; --radius-sm: 12px;
20
- --shadow: 0 4px 20px rgba(30,58,47,0.10);
21
- --nav-h: 64px;
22
- }
23
- * { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
24
- html { scroll-behavior:smooth; }
25
- body { font-family:'DM Sans',sans-serif; background:var(--cream); color:var(--ink);
26
- height:100dvh; display:flex; flex-direction:column; overflow:hidden; }
27
- /* ===== HEADER ===== */
28
- .header {
29
- padding: 14px 18px;
30
- background: var(--forest);
31
- display: flex; justify-content: space-between; align-items: center;
32
- flex-shrink: 0; z-index: 10;
33
- }
34
- .logo { font-family:'Playfair Display',serif; font-size:1.4rem; color:var(--cream); }
35
- .logo em { color:var(--gold); font-style:italic; }
36
- .header-right { display:flex; gap:8px; align-items:center; }
37
- .hdr-btn {
38
- background:rgba(255,255,255,0.12); border:1px solid rgba(255,255,255,0.2);
39
- color:var(--cream); padding:7px 13px; border-radius:50px;
40
- font-size:0.78rem; font-weight:600; cursor:pointer;
41
- font-family:'DM Sans',sans-serif; display:flex; align-items:center; gap:5px;
42
- }
43
- /* ===== TAB PAGES ===== */
44
- .tab-pages { flex:1; overflow:hidden; position:relative; }
45
- .tab-page {
46
- position:absolute; inset:0; overflow-y:auto; overflow-x:hidden;
47
- padding: 16px 16px 80px;
48
- display:none;
49
- }
50
- .tab-page.active { display:block; }
51
- /* ===== BOTTOM NAV ===== */
52
- .bottom-nav {
53
- height: var(--nav-h); background: var(--white);
54
- border-top: 1px solid var(--whisper);
55
- display: flex; flex-shrink: 0; z-index: 20;
56
- }
57
- .nav-item {
58
- flex:1; display:flex; flex-direction:column; align-items:center;
59
- justify-content:center; gap:3px; cursor:pointer;
60
- font-size:0.65rem; font-weight:700; letter-spacing:0.5px;
61
- text-transform:uppercase; color:var(--mist); transition:color 0.2s;
62
- }
63
- .nav-item i { font-size:1.1rem; }
64
- .nav-item.active { color:var(--forest); }
65
- .nav-item.active i { transform:scale(1.1); }
66
- /* ===== SHARED COMPONENTS ===== */
67
- .section-label {
68
- font-size:0.68rem; font-weight:700; letter-spacing:2px;
69
- text-transform:uppercase; color:var(--sage); margin-bottom:10px;
70
- }
71
- .chip-row {
72
- display:flex; gap:8px; overflow-x:auto; padding-bottom:3px;
73
- scrollbar-width:none;
74
- }
75
- .chip-row::-webkit-scrollbar { display:none; }
76
- .chip {
77
- flex-shrink:0; padding:7px 15px; background:var(--white);
78
- border-radius:50px; border:1.5px solid var(--whisper);
79
- font-size:0.8rem; font-weight:600; cursor:pointer; color:var(--mist);
80
- transition:all 0.2s; font-family:'DM Sans',sans-serif;
81
- }
82
- .chip.active { background:var(--forest); color:var(--cream); border-color:var(--forest); }
83
- .card {
84
- background:var(--white); border-radius:var(--radius);
85
- border:1.5px solid var(--whisper); padding:16px;
86
- box-shadow:var(--shadow);
87
- }
88
- .btn-primary {
89
- width:100%; background:linear-gradient(135deg,var(--forest),var(--moss));
90
- color:var(--cream); padding:15px; border-radius:var(--radius-sm);
91
- font-weight:700; font-size:0.92rem; border:none; cursor:pointer;
92
- font-family:'DM Sans',sans-serif; transition:opacity 0.2s;
93
- box-shadow:0 4px 14px rgba(30,58,47,0.28); margin-top:12px;
94
- }
95
- .btn-primary:active { opacity:0.85; }
96
- .btn-ghost {
97
- width:100%; background:transparent; color:var(--mist); padding:11px;
98
- border:none; font-weight:600; cursor:pointer; font-size:0.84rem;
99
- font-family:'DM Sans',sans-serif; margin-top:4px;
100
- }
101
- /* ===== HOME TAB ===== */
102
- .persona-section, .cat-section { margin-bottom:16px; }
103
- .action-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; }
104
- .scan-card {
105
- background:var(--white); padding:22px 14px; border-radius:var(--radius);
106
- border:1.5px solid var(--whisper); box-shadow:var(--shadow);
107
- cursor:pointer; display:flex; flex-direction:column;
108
- align-items:center; gap:10px; transition:all 0.2s;
109
- }
110
- .scan-card:active { transform:scale(0.96); border-color:var(--sage); }
111
- .scan-icon {
112
- width:50px; height:50px; border-radius:14px;
113
- background:linear-gradient(135deg,var(--forest),var(--sage));
114
- display:flex; align-items:center; justify-content:center;
115
- color:white; font-size:1.2rem;
116
- }
117
- .scan-card h3 { font-size:0.88rem; font-weight:700; color:var(--forest); }
118
- .scan-card p { font-size:0.7rem; color:var(--mist); }
119
- /* ===== UPLOAD / PREVIEW ===== */
120
- #preview-area { display:none; margin-bottom:4px; }
121
- #preview-img {
122
- width:100%; height:230px; object-fit:cover;
123
- border-radius:var(--radius); border:3px solid var(--white);
124
- box-shadow:var(--shadow); margin-bottom:8px;
125
- transition: opacity 0.3s;
126
- }
127
- /* Upload progress bar */
128
- .upload-progress {
129
- width:100%; height:4px; background:var(--whisper);
130
- border-radius:2px; overflow:hidden; margin-bottom:8px; display:none;
131
- }
132
- .upload-progress-bar {
133
- height:100%; width:0%; background:linear-gradient(90deg,var(--sage),var(--gold));
134
- border-radius:2px; transition:width 0.4s ease;
135
- }
136
- /* Image state badge — replaces simple quality-badge */
137
- .img-state-row {
138
- display:flex; align-items:center; gap:8px;
139
- padding:8px 12px; border-radius:var(--radius-sm);
140
- margin-bottom:10px; font-size:0.8rem; font-weight:600;
141
- }
142
- .img-state-row .state-icon { font-size:1.1rem; flex-shrink:0; }
143
- .img-state-row .state-text { flex:1; }
144
- .img-state-row .state-tip { font-size:0.7rem; color:var(--mist); font-weight:400; margin-top:1px; }
145
- .img-state-row.checking { background:#F0F4FF; color:#3730A3; border:1.5px solid #C7D2FE; }
146
- .img-state-row.good { background:#ECFDF5; color:var(--good); border:1.5px solid #A7F3D0; }
147
- .img-state-row.fair { background:#FFFBEB; color:var(--moderate); border:1.5px solid #FDE68A; }
148
- .img-state-row.poor { background:#FEF2F2; color:var(--caution); border:1.5px solid #FECACA; }
149
- /* Error card — rich contextual variant */
150
- .error-card {
151
- border-radius:var(--radius-sm); padding:0;
152
- display:none; margin-bottom:12px; overflow:hidden;
153
- border:1.5px solid #FECACA;
154
- }
155
- .error-card-inner {
156
- padding:16px; text-align:center;
157
- }
158
- /* Flip instruction card — special no_label variant */
159
- .error-card.no-label {
160
- border-color:#FDE68A; background:#FFFBEB;
161
- }
162
- .error-card.no-label .error-card-inner { background:#FFFBEB; }
163
- .flip-illustration {
164
- display:flex; align-items:center; justify-content:center;
165
- gap:10px; margin:10px 0; padding:12px;
166
- background:rgba(255,255,255,0.7); border-radius:10px;
167
- }
168
- .flip-box {
169
- width:52px; height:68px; border-radius:8px;
170
- display:flex; align-items:center; justify-content:center;
171
- flex-direction:column; gap:3px; font-size:0.6rem;
172
- font-weight:700; letter-spacing:0.3px;
173
- }
174
- .flip-box.front { background:#FEE2E2; color:#991B1B; border:2px solid #FECACA; }
175
- .flip-box.back { background:#DCFCE7; color:#166534; border:2px dashed #86EFAC; }
176
- .flip-box .fb-icon { font-size:1.4rem; }
177
- .flip-arrow { font-size:1.4rem; color:var(--mist); }
178
- .error-card .err-icon { font-size:2rem; margin-bottom:6px; display:block; }
179
- .error-card .err-title { font-weight:700; color:#991B1B; margin-bottom:4px; font-size:0.92rem; }
180
- .error-card.no-label .err-title { color:#92400E; }
181
- .error-card .err-msg { font-size:0.82rem; color:#7F1D1D; line-height:1.5; }
182
- .error-card.no-label .err-msg { color:#78350F; }
183
- .err-retry-btn {
184
- width:100%; margin-top:12px; padding:11px;
185
- background:var(--forest); color:var(--cream);
186
- border:none; border-radius:var(--radius-sm); font-weight:700;
187
- font-size:0.84rem; cursor:pointer; font-family:"DM Sans",sans-serif;
188
- }
189
- .err-secondary-btn {
190
- width:100%; margin-top:8px; padding:9px;
191
- background:transparent; color:var(--mist);
192
- border:1.5px solid var(--whisper); border-radius:var(--radius-sm);
193
- font-weight:600; font-size:0.8rem; cursor:pointer;
194
- font-family:"DM Sans",sans-serif;
195
- }
196
- /* Upload guidance hint (shown on landing) */
197
- .upload-hint {
198
- background:var(--white); border:1.5px solid var(--whisper);
199
- border-radius:var(--radius-sm); padding:12px 14px;
200
- display:flex; align-items:flex-start; gap:10px;
201
- margin-bottom:14px;
202
- }
203
- .upload-hint-icon { font-size:1.3rem; flex-shrink:0; margin-top:1px; }
204
- .upload-hint-text { font-size:0.78rem; color:var(--mist); line-height:1.5; }
205
- .upload-hint-text strong { color:var(--forest); }
206
- /* Drag-drop zone */
207
- .drop-zone {
208
- border:2px dashed var(--whisper); border-radius:var(--radius);
209
- padding:24px 16px; text-align:center;
210
- transition:all 0.2s; cursor:pointer; background:var(--white);
211
- }
212
- .drop-zone.drag-over {
213
- border-color:var(--sage); background:#F0FDF4;
214
- }
215
- .drop-zone-icon { font-size:2rem; margin-bottom:6px; }
216
- .drop-zone-text { font-size:0.82rem; color:var(--mist); }
217
- .drop-zone-text strong { color:var(--forest); }
218
- /* ===== HISTORY TAB ===== */
219
- .history-empty {
220
- text-align:center; padding:40px 20px; color:var(--mist);
221
- }
222
- .history-empty i { font-size:2.5rem; opacity:0.3; display:block; margin-bottom:12px; }
223
- .history-card {
224
- background:var(--white); border-radius:var(--radius-sm);
225
- border:1.5px solid var(--whisper); padding:14px 16px;
226
- margin-bottom:10px; display:flex; align-items:center; gap:12px;
227
- cursor:pointer; transition:border-color 0.2s;
228
- }
229
- .history-card:hover { border-color:var(--sage); }
230
- .h-score-ring {
231
- width:46px; height:46px; border-radius:50%; flex-shrink:0;
232
- display:flex; align-items:center; justify-content:center;
233
- font-weight:800; font-size:1rem;
234
- }
235
- .h-name { font-weight:700; font-size:0.88rem; color:var(--forest); }
236
- .h-meta { font-size:0.72rem; color:var(--mist); margin-top:2px; }
237
- .h-del { margin-left:auto; color:#CBD5E1; padding:6px; cursor:pointer; font-size:0.85rem; }
238
- .h-del:hover { color:var(--coral); }
239
- .history-clear-btn {
240
- width:100%; padding:11px; border:1.5px solid var(--whisper);
241
- border-radius:var(--radius-sm); background:transparent;
242
- color:var(--mist); font-weight:600; font-size:0.82rem;
243
- cursor:pointer; font-family:'DM Sans',sans-serif; margin-top:8px;
244
- }
245
- .history-clear-btn:hover { border-color:var(--coral); color:var(--coral); }
246
- /* ===== PROFILE TAB ===== */
247
- .profile-avatar {
248
- width:80px; height:80px; border-radius:50%;
249
- background:linear-gradient(135deg,var(--forest),var(--sage));
250
- display:flex; align-items:center; justify-content:center;
251
- font-size:2rem; margin:0 auto 10px; color:white;
252
- }
253
- .profile-name {
254
- font-family:'Playfair Display',serif; font-size:1.3rem;
255
- text-align:center; color:var(--forest); margin-bottom:4px;
256
- }
257
- .profile-sub { text-align:center; font-size:0.82rem; color:var(--mist); margin-bottom:20px; }
258
- .profile-section { margin-bottom:16px; }
259
- .profile-row {
260
- display:flex; align-items:center; justify-content:space-between;
261
- padding:14px 16px; background:var(--white);
262
- border-radius:var(--radius-sm); border:1.5px solid var(--whisper);
263
- margin-bottom:8px;
264
- }
265
- .profile-row-left { display:flex; align-items:center; gap:12px; }
266
- .profile-row-icon {
267
- width:36px; height:36px; border-radius:10px;
268
- background:var(--parchment); display:flex; align-items:center;
269
- justify-content:center; font-size:0.95rem;
270
- }
271
- .profile-row-label { font-weight:600; font-size:0.87rem; color:var(--forest); }
272
- .profile-row-value { font-size:0.78rem; color:var(--mist); }
273
- .toggle-switch {
274
- width:42px; height:24px; background:var(--whisper);
275
- border-radius:12px; position:relative; cursor:pointer;
276
- transition:background 0.2s;
277
- }
278
- .toggle-switch.on { background:var(--sage); }
279
- .toggle-knob {
280
- position:absolute; top:3px; left:3px; width:18px; height:18px;
281
- border-radius:50%; background:white;
282
- transition:transform 0.2s; box-shadow:0 1px 4px rgba(0,0,0,0.2);
283
- }
284
- .toggle-switch.on .toggle-knob { transform:translateX(18px); }
285
- .profile-select {
286
- border:none; background:transparent; font-family:'DM Sans',sans-serif;
287
- font-size:0.82rem; color:var(--mist); cursor:pointer;
288
- text-align:right;
289
- }
290
- .stat-row {
291
- display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px; margin-bottom:16px;
292
- }
293
- .stat-box {
294
- background:var(--white); border-radius:var(--radius-sm);
295
- border:1.5px solid var(--whisper); padding:14px;
296
- text-align:center;
297
- }
298
- .stat-num { font-family:'Playfair Display',serif; font-size:1.6rem; color:var(--forest); }
299
- .stat-lbl { font-size:0.68rem; color:var(--mist); font-weight:600; letter-spacing:0.5px; margin-top:2px; }
300
- /* ===== LOADER ===== */
301
- #loader {
302
- position:fixed; inset:0; background:rgba(250,246,238,0.95);
303
- backdrop-filter:blur(8px); z-index:600; display:none;
304
- flex-direction:column; align-items:center; justify-content:center; gap:16px;
305
- }
306
- .loader-ring {
307
- width:58px; height:58px; border:4px solid var(--whisper);
308
- border-top-color:var(--forest); border-radius:50%;
309
- animation:spin 0.9s linear infinite;
310
- }
311
- @keyframes spin { to { transform:rotate(360deg); } }
312
- .loader-text { font-family:'Playfair Display',serif; font-size:1.05rem; color:var(--forest); }
313
- .loader-step { font-size:0.78rem; color:var(--mist); text-align:center; max-width:230px; }
314
- .loader-hint { font-size:0.7rem; color:#aab; text-align:center; }
315
- .loader-cancel-btn {
316
- margin-top:16px; padding:9px 26px; background:transparent;
317
- border:1.5px solid var(--whisper); border-radius:50px;
318
- color:var(--mist); font-size:0.8rem; font-weight:600;
319
- cursor:pointer; font-family:'DM Sans',sans-serif;
320
- }
321
- /* ===== CAMERA ===== */
322
- #cam-overlay { position:fixed; inset:0; z-index:500; background:black; display:none; }
323
- #cam-overlay.active { display:block; }
324
- video { width:100%; height:100%; object-fit:cover; display:block; }
325
- .cam-ui { position:absolute; inset:0; display:flex; flex-direction:column; }
326
- .cam-guide { flex:1; display:flex; align-items:center; justify-content:center; }
327
- .cam-frame {
328
- width:270px; height:190px; border:2px solid rgba(255,255,255,0.5);
329
- border-radius:14px; position:relative;
330
- }
331
- .cam-frame::before, .cam-frame::after {
332
- content:''; position:absolute; width:28px; height:28px;
333
- border-color:var(--gold); border-style:solid;
334
- }
335
- .cam-frame::before { top:-2px; left:-2px; border-width:3px 0 0 3px; border-radius:12px 0 0 0; }
336
- .cam-frame::after { bottom:-2px; right:-2px; border-width:0 3px 3px 0; border-radius:0 0 12px 0; }
337
- .cam-hint { color:rgba(255,255,255,0.7); text-align:center; font-size:0.78rem; margin-top:10px; }
338
- .cam-controls {
339
- padding:18px 30px 36px;
340
- background:linear-gradient(to top,rgba(0,0,0,0.8),transparent);
341
- display:flex; justify-content:center; align-items:center; position:relative;
342
- }
343
- .shutter {
344
- width:70px; height:70px; background:white; border-radius:50%;
345
- border:4px solid rgba(255,255,255,0.35); cursor:pointer;
346
- box-shadow:0 4px 20px rgba(0,0,0,0.4); transition:transform 0.1s;
347
- }
348
- .shutter:active { transform:scale(0.9); }
349
- .cam-cancel {
350
- position:absolute; left:28px; color:white;
351
- background:rgba(0,0,0,0.4); padding:8px 16px;
352
- border-radius:50px; border:1px solid rgba(255,255,255,0.2);
353
- font-size:0.82rem; font-weight:600; cursor:pointer;
354
- font-family:'DM Sans',sans-serif; backdrop-filter:blur(4px);
355
- }
356
- /* ===== REPORT ===== */
357
- #report {
358
- position:fixed; inset:0; background:var(--cream); z-index:400;
359
- transform:translateY(100%);
360
- transition:transform 0.42s cubic-bezier(0.16,1,0.3,1);
361
- overflow:hidden; /* NO scroll here — child scrolls */
362
- display:flex; flex-direction:column;
363
- }
364
- #report.open { transform:translateY(0); }
365
- /* Inner scroll area — only this scrolls */
366
- .report-scroll {
367
- flex:1; overflow-y:auto; overflow-x:hidden;
368
- padding-bottom:0; /* footer handles spacing */
369
- -webkit-overflow-scrolling:touch;
370
- }
371
- .report-hero {
372
- background:linear-gradient(155deg,var(--forest) 0%,var(--moss) 55%,var(--sage) 100%);
373
- padding:18px 18px 32px; position:relative; overflow:hidden;
374
- }
375
- .report-hero::after {
376
- content:''; position:absolute; top:-60px; right:-60px;
377
- width:220px; height:220px;
378
- background:radial-gradient(circle,rgba(201,168,76,0.18) 0%,transparent 70%);
379
- pointer-events:none;
380
- }
381
- .report-topbar {
382
- display:flex; align-items:center; justify-content:space-between; margin-bottom:18px;
383
- }
384
- .back-btn {
385
- width:36px; height:36px; border-radius:50%;
386
- background:rgba(255,255,255,0.15); border:1px solid rgba(255,255,255,0.2);
387
- display:flex; align-items:center; justify-content:center;
388
- color:white; cursor:pointer;
389
- }
390
- .verdict-badge {
391
- background:rgba(255,255,255,0.15); border:1px solid rgba(255,255,255,0.25);
392
- color:white; padding:5px 14px; border-radius:50px;
393
- font-size:0.75rem; font-weight:700; letter-spacing:1px;
394
- }
395
- /* Score + Info row */
396
- .hero-row { display:flex; align-items:flex-start; gap:16px; }
397
- .score-col { flex-shrink:0; text-align:center; }
398
- .score-ring-wrap { width:110px; height:110px; position:relative; margin:0 auto; }
399
- .score-ring-center {
400
- position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
401
- text-align:center;
402
- }
403
- .score-num {
404
- font-family:'Playfair Display',serif; font-size:2.2rem;
405
- font-weight:900; color:white; line-height:1;
406
- }
407
- .score-sub { font-size:0.62rem; color:rgba(255,255,255,0.55); font-weight:600; }
408
- /* PIE CHART LEGEND — key fix */
409
- .pie-legend {
410
- display:flex; flex-direction:column; gap:4px; margin-top:8px;
411
- }
412
- .pie-legend-item {
413
- display:flex; align-items:center; gap:6px;
414
- font-size:0.66rem; color:rgba(255,255,255,0.8); font-weight:500;
415
- }
416
- .pie-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
417
- .info-col { flex:1; padding-top:2px; }
418
- .product-cat-tag {
419
- display:inline-flex; align-items:center; gap:5px;
420
- background:rgba(255,255,255,0.14); color:rgba(255,255,255,0.8);
421
- padding:3px 10px; border-radius:50px; font-size:0.7rem; font-weight:600;
422
- margin-bottom:6px;
423
- }
424
- .product-name {
425
- font-family:'Playfair Display',serif; font-size:1.25rem;
426
- color:white; line-height:1.2; margin-bottom:8px;
427
- }
428
- .summary-text { font-size:0.8rem; color:rgba(255,255,255,0.72); line-height:1.5; }
429
- /* ===== REPORT BODY ===== */
430
- .report-body { padding:0 14px 20px; } /* scroll area handles padding */
431
- .r-section {
432
- background:var(--white); border-radius:var(--radius);
433
- padding:16px; margin-top:12px; border:1.5px solid var(--whisper);
434
- }
435
- .r-title {
436
- font-size:0.68rem; font-weight:700; letter-spacing:2px;
437
- text-transform:uppercase; color:var(--sage); margin-bottom:12px;
438
- display:flex; align-items:center; gap:8px;
439
- }
440
- .r-title::after { content:''; flex:1; height:1px; background:var(--whisper); }
441
- /* ELI5 */
442
- .eli5-box {
443
- background:linear-gradient(135deg,#FFFBEB,#FFF8DC);
444
- border:1.5px solid #F5DFA0; border-radius:var(--radius-sm);
445
- padding:14px; font-size:0.87rem; line-height:1.6; color:#6B5A1E;
446
- }
447
- .eli5-label {
448
- font-weight:700; font-size:0.68rem; letter-spacing:1px;
449
- text-transform:uppercase; color:var(--gold);
450
- margin-bottom:6px; display:block;
451
- }
452
- /* NUTRIENTS */
453
- .nutrient-legend { display:flex; gap:14px; margin-bottom:14px; flex-wrap:wrap; }
454
- .n-leg { display:flex; align-items:center; gap:4px; font-size:0.72rem; color:var(--mist); }
455
- .n-dot { width:8px; height:8px; border-radius:50%; }
456
- .nutrient-item { margin-bottom:10px; }
457
- .nutrient-row { display:flex; align-items:center; gap:10px; }
458
- .n-name { font-size:0.8rem; font-weight:600; color:var(--forest); width:76px; flex-shrink:0; }
459
- .n-bar-wrap { flex:1; height:7px; background:var(--parchment); border-radius:10px; overflow:hidden; }
460
- .n-bar { height:100%; border-radius:10px; width:0%; transition:width 0.9s ease; }
461
- .n-val { font-size:0.73rem; color:var(--mist); font-weight:600; width:46px; text-align:right; flex-shrink:0; }
462
- .n-impact { font-size:0.7rem; color:var(--mist); line-height:1.4; margin-top:2px; padding-left:86px; }
463
- .bar-good { background:linear-gradient(90deg,#4CAF7D,#2D9B5A); }
464
- .bar-moderate { background:linear-gradient(90deg,#F5A623,#E8902A); }
465
- .bar-caution { background:linear-gradient(90deg,#FF7043,#E53935); }
466
- .bar-bad { background:linear-gradient(90deg,#C62828,#8B0000); }
467
- /* PROS CONS */
468
- .pc-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
469
- .pc-label { font-size:0.68rem; font-weight:700; letter-spacing:1.5px; text-transform:uppercase; margin-bottom:8px; }
470
- .pc-label.p { color:var(--good); } .pc-label.c { color:var(--caution); }
471
- .pc-item { font-size:0.78rem; line-height:1.5; padding:5px 0; border-bottom:1px solid var(--whisper); display:flex; gap:6px; }
472
- .pc-item:last-child { border:none; }
473
- .pc-dot { width:5px; height:5px; border-radius:50%; flex-shrink:0; margin-top:8px; }
474
- .pc-dot.p { background:var(--good); } .pc-dot.c { background:var(--caution); }
475
- /* AGE WARNINGS */
476
- .age-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
477
- .age-card { padding:12px; border-radius:var(--radius-sm); border:1.5px solid; }
478
- .age-card.good { background:#ECFDF5; border-color:#A7F3D0; }
479
- .age-card.caution { background:#FFFBEB; border-color:#FCD34D; }
480
- .age-card.warning { background:#FEF2F2; border-color:#FECACA; }
481
- .age-emoji { font-size:1.3rem; }
482
- .age-grp { font-size:0.7rem; font-weight:700; letter-spacing:0.5px; margin-top:2px; }
483
- .age-card.good .age-grp { color:#065F46; }
484
- .age-card.caution .age-grp { color:#92400E; }
485
- .age-card.warning .age-grp { color:#991B1B; }
486
- .age-msg { font-size:0.73rem; line-height:1.4; margin-top:4px; color:var(--ink); }
487
- /* PARAGRAPHS */
488
- .para-text { font-size:0.86rem; line-height:1.7; color:var(--ink); }
489
- .para-text + .para-text { margin-top:12px; }
490
- /* MOLECULAR */
491
- .mol-box {
492
- background:#F8F4FF; border:1.5px solid #DDD6FE;
493
- border-radius:var(--radius-sm); padding:13px;
494
- font-size:0.84rem; line-height:1.6; color:#3B1E7A;
495
- }
496
- /* ALTERNATIVE */
497
- .alt-box {
498
- background:linear-gradient(135deg,var(--forest),var(--moss));
499
- border-radius:var(--radius-sm); padding:15px;
500
- display:flex; align-items:flex-start; gap:10px;
501
- }
502
- .alt-icon { font-size:1.4rem; flex-shrink:0; }
503
- .alt-lbl { font-size:0.68rem; font-weight:700; letter-spacing:1px; text-transform:uppercase; color:var(--mint); margin-bottom:3px; }
504
- .alt-text { font-size:0.83rem; color:var(--cream); line-height:1.5; }
505
- /* SHARE BAR — lives outside #report at document level */
506
- .report-share-bar {
507
- position:fixed; bottom:0; left:0; right:0;
508
- padding:12px 14px 26px;
509
- background:var(--cream);
510
- border-top:1px solid var(--whisper);
511
- display:none; /* shown only when report is open */
512
- gap:10px; z-index:420;
513
- align-items:center;
514
- }
515
- .report-share-bar.visible { display:flex; }
516
- .share-btn {
517
- flex:1; padding:13px; border-radius:var(--radius-sm);
518
- font-weight:700; font-size:0.83rem; border:none; cursor:pointer;
519
- font-family:'DM Sans',sans-serif; transition:opacity 0.2s;
520
- }
521
- .share-btn.p { background:var(--forest); color:var(--cream); }
522
- .share-btn.s { background:var(--white); color:var(--forest); border:1.5px solid var(--whisper); }
523
- /* LANG MODAL */
524
- #lang-modal { position:fixed; inset:0; z-index:700; display:none; align-items:flex-end; justify-content:center; }
525
- #lang-modal.open { display:flex; }
526
- .lang-backdrop { position:absolute; inset:0; background:rgba(0,0,0,0.4); backdrop-filter:blur(4px); }
527
- .lang-sheet {
528
- position:relative; z-index:1; background:var(--cream);
529
- border-radius:22px 22px 0 0; padding:18px 16px 36px;
530
- width:100%; max-width:460px;
531
- }
532
- .lang-handle { width:38px; height:4px; background:var(--whisper); border-radius:2px; margin:0 auto 16px; }
533
- .lang-title { font-family:'Playfair Display',serif; font-size:1.2rem; color:var(--forest); margin-bottom:14px; }
534
- .lang-option {
535
- display:flex; align-items:center; gap:12px; padding:13px 14px;
536
- border-radius:var(--radius-sm); cursor:pointer; transition:background 0.15s;
537
- font-weight:500; font-size:0.88rem;
538
- }
539
- .lang-option:hover { background:var(--parchment); }
540
- .lang-option.active { background:var(--forest); color:white; }
541
- .lang-flag { font-size:1.3rem; }
542
- /* TOAST */
543
- .toast {
544
- position:fixed; bottom:90px; left:50%; transform:translateX(-50%) translateY(16px);
545
- background:var(--forest); color:white; padding:9px 20px;
546
- border-radius:50px; font-size:0.8rem; font-weight:600;
547
- opacity:0; transition:all 0.3s; z-index:800; white-space:nowrap;
548
- }
549
- .toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
550
- /* ===== BLUR NOTICE BANNER ===== */
551
- .blur-notice {
552
- display:none; border-radius:var(--radius-sm);
553
- border:1.5px solid #FCD34D; background:#FFFBEB;
554
- padding:12px 14px; margin-bottom:12px;
555
- }
556
- .blur-notice.visible { display:block; }
557
- .blur-notice-header {
558
- display:flex; align-items:center; gap:8px;
559
- font-weight:700; font-size:0.85rem; color:#92400E; margin-bottom:4px;
560
- }
561
- .blur-notice-header .bn-icon { font-size:1.1rem; }
562
- .blur-notice-body { font-size:0.78rem; color:#78350F; line-height:1.5; }
563
- .blur-notice-methods {
564
- margin-top:6px; font-size:0.7rem; color:#A16207;
565
- background:rgba(251,191,36,0.15); padding:5px 9px;
566
- border-radius:6px; font-family:monospace; word-break:break-all;
567
- }
568
- /* Deblurred image preview inside report */
569
- .deblur-report-badge {
570
- display:inline-flex; align-items:center; gap:5px;
571
- padding:4px 10px; border-radius:50px;
572
- background:#FEF3C7; border:1px solid #FCD34D;
573
- font-size:0.72rem; font-weight:700; color:#92400E;
574
- margin-bottom:10px;
575
- }
576
- .deblur-img-wrap {
577
- border-radius:var(--radius-sm); overflow:hidden;
578
- border:2px solid #FCD34D; margin-bottom:10px;
579
- }
580
- .deblur-img-wrap img { width:100%; display:block; }
581
- /* Severity pill */
582
- .severity-pill {
583
- display:inline-block; padding:2px 8px; border-radius:50px;
584
- font-size:0.68rem; font-weight:700; letter-spacing:0.5px;
585
- text-transform:uppercase; margin-left:6px;
586
- }
587
- .severity-pill.severe { background:#FEE2E2; color:#991B1B; }
588
- .severity-pill.moderate { background:#FEF3C7; color:#92400E; }
589
- .severity-pill.mild { background:#ECFDF5; color:#065F46; }
590
- /* ── Scan Quota Counter ───────────────────────────────── */
591
- #scan-counter { text-align:center; margin:8px 0 4px; min-height:22px; }
592
- .quota-badge {
593
- display:inline-block; font-size:0.72rem; font-weight:600;
594
- padding:4px 12px; border-radius:20px;
595
- background:#1A2942; color:#7DD3FC; border:1px solid #1E3A5F;
596
- letter-spacing:0.02em;
597
- }
598
- .quota-badge.empty { background:#3F1515; color:#FCA5A5; border-color:#7F1D1D; }
599
- /* ── Health Warnings ──────────────────────────────────── */
600
- .warnings-list { display:flex; flex-direction:column; gap:10px; }
601
- .warn-card {
602
- border-radius:10px; padding:12px 14px;
603
- }
604
- .warn-header { display:flex; align-items:center; gap:8px; margin-bottom:6px; flex-wrap:wrap; }
605
- .warn-icon { font-size:1rem; }
606
- .warn-sev { font-size:0.7rem; font-weight:800; letter-spacing:0.06em; text-transform:uppercase; }
607
- .warn-cat { font-size:0.72rem; font-weight:600; color:var(--mist); }
608
- .warn-text { font-size:0.8rem; line-height:1.5; font-weight:500; }
609
- .warn-groups { display:flex; flex-wrap:wrap; gap:4px; margin-top:8px; }
610
- .warn-tag {
611
- font-size:0.66rem; padding:2px 8px; border-radius:10px;
612
- background:rgba(0,0,0,0.1); color:inherit; font-weight:600;
613
- }
614
- /* ── Allergens ────────────────────────────────────────── */
615
- .allergen-list { display:flex; flex-direction:column; gap:6px; }
616
- .allergen-item {
617
- font-size:0.8rem; font-weight:600; color:#92400E;
618
- background:#FFFBEB; border:1px solid #FCD34D;
619
- padding:8px 12px; border-radius:8px;
620
- }
621
- /* ── Contraindications ────────────────────────────────── */
622
- .contra-list { display:flex; flex-direction:column; gap:6px; }
623
- .contra-item {
624
- font-size:0.79rem; color:#6B21A8;
625
- background:#FAF5FF; border:1px solid #C084FC;
626
- padding:8px 12px; border-radius:8px; line-height:1.4;
627
- }
628
- /* ── Consumption Guidance ─────────────────────────────── */
629
- .consumption-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
630
- .cons-card {
631
- background:#F0F9FF; border:1px solid #BAE6FD;
632
- border-radius:10px; padding:12px; min-height:80px;
633
- }
634
- .cons-card.cons-full { grid-column:1/-1; }
635
- .cons-icon { font-size:1.1rem; margin-bottom:4px; }
636
- .cons-label { font-size:0.68rem; font-weight:700; color:#0369A1; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:4px; }
637
- .cons-text { font-size:0.78rem; color:#0C4A6E; line-height:1.45; }
638
- /* ── Curiosity + Wellness ─────────────────────────────── */
639
- .curiosity-box {
640
- background:#F0FDF4; border:1px solid #86EFAC;
641
- border-radius:10px; padding:12px 14px;
642
- font-size:0.8rem; color:#14532D; line-height:1.5; margin-bottom:8px;
643
- }
644
- .wellness-box {
645
- background:#FFF7ED; border:1px solid #FED7AA;
646
- border-radius:10px; padding:12px 14px;
647
- font-size:0.8rem; color:#7C2D12; line-height:1.5;
648
- }
649
- /* ── Ingredients Spotlight ────────────────────────────── */
650
- .spotlight-list { display:flex; flex-direction:column; gap:8px; }
651
- .spotlight-item {
652
- background:var(--surface-2); border-radius:10px;
653
- padding:12px 14px; cursor:pointer; transition:background 0.2s;
654
- border:1px solid transparent;
655
- }
656
- .spotlight-item:hover { background:#1A2942; border-color:#2A3E5A; }
657
- .spot-name { font-size:0.85rem; font-weight:700; color:var(--sand); margin-bottom:2px; }
658
- .spot-role { font-size:0.68rem; font-weight:600; color:var(--mist); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:6px; }
659
- .spot-fact { font-size:0.77rem; color:var(--text); line-height:1.4; margin-bottom:6px; }
660
- .spot-tap { font-size:0.68rem; color:#60A5FA; font-weight:600; }
661
- /* ── Ingredients Full List ────────────────────────────── */
662
- .ingredients-full { display:flex; flex-wrap:wrap; gap:6px; }
663
- .ing-chip {
664
- display:inline-block; font-size:0.72rem;
665
- background:#1A2942; color:#94A3B8;
666
- border:1px solid #2A3E5A; border-radius:6px;
667
- padding:3px 8px; line-height:1.4;
668
- }
669
- </style>
670
- </head>
671
- <body>
672
-
673
- <!-- HEADER -->
674
- <div class="header">
675
- <div class="logo">Eat<em>lytic</em></div>
676
- <div class="header-right">
677
- <button class="hdr-btn" onclick="openLang()"><i class="fas fa-globe"></i> <span id="lang-display">EN</span></button>
678
- </div>
679
- </div>
680
-
681
- <!-- TAB PAGES -->
682
- <div class="tab-pages">
683
-
684
- <!-- ══ HOME TAB ══ -->
685
- <div class="tab-page active" id="page-home">
686
-
687
- <!-- Persona -->
688
- <div class="persona-section">
689
- <div class="section-label">Eating profile</div>
690
- <div class="chip-row" id="persona-row">
691
- <div class="chip active" onclick="setChip(this,'persona','General')">🏠 General</div>
692
- <div class="chip" onclick="setChip(this,'persona','Gym')">💪 Gym</div>
693
- <div class="chip" onclick="setChip(this,'persona','Baby')">👶 Baby</div>
694
- <div class="chip" onclick="setChip(this,'persona','Skin')">✨ Skin</div>
695
- <div class="chip" onclick="setChip(this,'persona','Keto')">🥑 Keto</div>
696
- <div class="chip" onclick="setChip(this,'persona','Heart')">❤️ Heart</div>
697
- </div>
698
- </div>
699
-
700
- <!-- Category -->
701
- <div class="cat-section">
702
- <div class="section-label">Product type</div>
703
- <div class="chip-row" id="cat-row">
704
- <div class="chip active" onclick="setChip(this,'cat','general')">All</div>
705
- <div class="chip" onclick="setChip(this,'cat','snack')">🍿 Snack</div>
706
- <div class="chip" onclick="setChip(this,'cat','dairy')">🥛 Dairy</div>
707
- <div class="chip" onclick="setChip(this,'cat','beverage')">🧃 Beverage</div>
708
- <div class="chip" onclick="setChip(this,'cat','supplement')">💊 Supplement</div>
709
- <div class="chip" onclick="setChip(this,'cat','frozen')">🧊 Frozen</div>
710
- </div>
711
- </div>
712
-
713
- <!-- Error card — contextual variants -->
714
- <div class="blur-notice" id="blur-notice">
715
- <div class="blur-notice-header">
716
- <span class="bn-icon">🔧</span>
717
- <span id="bn-title">Blur Detected &amp; Corrected</span>
718
- <span class="severity-pill" id="bn-severity-pill"></span>
719
- </div>
720
- <div class="blur-notice-body" id="bn-body">
721
- The image was identified as blurry. Advanced deblurring and
722
- enhancement algorithms were applied before analysis.
723
- </div>
724
- <div class="blur-notice-methods" id="bn-methods" style="display:none"></div>
725
- </div>
726
- <div class="error-card" id="error-card">
727
- <div class="error-card-inner" id="error-card-inner">
728
- <span class="err-icon" id="err-icon">📷</span>
729
- <div class="err-title" id="err-title">Image Issue</div>
730
- <div class="err-msg" id="err-msg">Please retake your photo.</div>
731
- <div id="err-flip-illustration" style="display:none">
732
- <div class="flip-illustration">
733
- <div class="flip-box front">
734
- <div class="fb-icon">🏷️</div>
735
- <span>FRONT</span>
736
- </div>
737
- <div class="flip-arrow">→</div>
738
- <div class="flip-box back">
739
- <div class="fb-icon">📋</div>
740
- <span>BACK ✓</span>
741
- </div>
742
- </div>
743
- </div>
744
- <button class="err-retry-btn" id="err-retry-btn" onclick="triggerRetake()">
745
- <i class="fas fa-camera" style="margin-right:6px"></i>Retake Photo
746
- </button>
747
- <button class="err-secondary-btn" id="err-upload-btn" onclick="document.getElementById('fileInput').click()" style="display:none">
748
- <i class="fas fa-image" style="margin-right:6px"></i>Upload Different Image
749
- </button>
750
- </div>
751
- </div>
752
-
753
- <!-- Landing -->
754
- <div id="landing-ui">
755
- <!-- Upload guidance hint -->
756
- <div class="upload-hint">
757
- <div class="upload-hint-icon">💡</div>
758
- <div class="upload-hint-text">
759
- <strong>Scan the back label</strong> — point your camera at the side showing ingredients &amp; nutrition facts, not the front of the packet.
760
- </div>
761
- </div>
762
- <div class="action-grid">
763
- <div class="scan-card" onclick="startCamera()">
764
- <div class="scan-icon"><i class="fas fa-camera"></i></div>
765
- <h3>Camera</h3>
766
- <p>Scan live label</p>
767
- </div>
768
- <div class="scan-card"
769
- id="upload-drop-card"
770
- onclick="document.getElementById('fileInput').click()"
771
- ondragover="handleDragOver(event)"
772
- ondragleave="handleDragLeave(event)"
773
- ondrop="handleDrop(event)">
774
- <div class="scan-icon"><i class="fas fa-image"></i></div>
775
- <h3>Upload</h3>
776
- <p>Tap or drag photo</p>
777
- </div>
778
- </div>
779
- </div>
780
-
781
- <!-- Preview -->
782
- <div id="preview-area">
783
- <img id="preview-img" src="" alt="Preview">
784
- <!-- Upload progress bar (shown during OCR) -->
785
- <div class="upload-progress" id="upload-progress">
786
- <div class="upload-progress-bar" id="upload-progress-bar"></div>
787
- </div>
788
- <!-- Rich image state badge -->
789
- <div id="img-state-badge" class="img-state-row checking" style="display:none">
790
- <div class="state-icon" id="state-icon">⏳</div>
791
- <div>
792
- <div class="state-text" id="state-text">Checking image...</div>
793
- <div class="state-tip" id="state-tip"></div>
794
- </div>
795
- </div>
796
- <div id="scan-counter"></div>
797
- <button class="btn-primary" id="analyze-main-btn" onclick="analyze()" style="display:none">
798
- <i class="fas fa-microscope" style="margin-right:8px"></i>
799
- <span id="analyze-btn-text">Analyze Label</span>
800
- </button>
801
- <button class="btn-ghost" onclick="resetHome()">↩ Choose different image</button>
802
- </div>
803
- </div>
804
-
805
- <!-- ══ HISTORY TAB ══ -->
806
- <div class="tab-page" id="page-history">
807
- <div class="section-label" style="margin-bottom:14px">Scan History</div>
808
- <div id="history-list"></div>
809
- </div>
810
-
811
- <!-- ══ PROFILE TAB ══ -->
812
- <div class="tab-page" id="page-profile">
813
- <div class="card" style="margin-bottom:16px; text-align:center; padding:24px 16px;">
814
- <div class="profile-avatar" id="prof-avatar">🌿</div>
815
- <div class="profile-name" id="prof-name">My Profile</div>
816
- <div class="profile-sub" id="prof-sub">General · English</div>
817
- </div>
818
-
819
- <!-- Stats -->
820
- <div class="stat-row">
821
- <div class="stat-box"><div class="stat-num" id="stat-scans">0</div><div class="stat-lbl">Scans</div></div>
822
- <div class="stat-box"><div class="stat-num" id="stat-avg">—</div><div class="stat-lbl">Avg Score</div></div>
823
- <div class="stat-box"><div class="stat-num" id="stat-best">—</div><div class="stat-lbl">Best</div></div>
824
- </div>
825
-
826
- <!-- Settings -->
827
- <div class="profile-section">
828
- <div class="section-label">Preferences</div>
829
-
830
- <div class="profile-row">
831
- <div class="profile-row-left">
832
- <div class="profile-row-icon">🌐</div>
833
- <div>
834
- <div class="profile-row-label">Language</div>
835
- <div class="profile-row-value" id="pref-lang-val">English</div>
836
- </div>
837
- </div>
838
- <button onclick="openLang()" style="background:var(--parchment);border:none;padding:6px 12px;border-radius:8px;font-size:0.78rem;font-weight:600;cursor:pointer;color:var(--forest);">Change</button>
839
- </div>
840
-
841
- <div class="profile-row">
842
- <div class="profile-row-left">
843
- <div class="profile-row-icon">👤</div>
844
- <div>
845
- <div class="profile-row-label">Default Persona</div>
846
- <div class="profile-row-value" id="pref-persona-val">General</div>
847
- </div>
848
- </div>
849
- <select class="profile-select" id="pref-persona-sel" onchange="savePersonaPref(this.value)">
850
- <option value="General">General</option>
851
- <option value="Gym">Gym</option>
852
- <option value="Baby">Baby</option>
853
- <option value="Skin">Skin</option>
854
- <option value="Keto">Keto</option>
855
- <option value="Heart">Heart</option>
856
- </select>
857
- </div>
858
-
859
- <div class="profile-row">
860
- <div class="profile-row-left">
861
- <div class="profile-row-icon">🔔</div>
862
- <div>
863
- <div class="profile-row-label">Save scan history</div>
864
- <div class="profile-row-value">Stored locally on device</div>
865
- </div>
866
- </div>
867
- <div class="toggle-switch on" id="history-toggle" onclick="toggleHistorySave()">
868
- <div class="toggle-knob"></div>
869
- </div>
870
- </div>
871
- </div>
872
-
873
- <!-- Danger zone -->
874
- <div class="profile-section">
875
- <div class="section-label">Data</div>
876
- <button class="history-clear-btn" onclick="clearAllHistory()">🗑 Clear All Scan History</button>
877
- </div>
878
- </div>
879
-
880
- </div><!-- end tab-pages -->
881
-
882
- <!-- BOTTOM NAV -->
883
- <div class="bottom-nav">
884
- <div class="nav-item active" id="nav-home" onclick="switchTab('home')">
885
- <i class="fas fa-home"></i> Home
886
- </div>
887
- <div class="nav-item" id="nav-history" onclick="switchTab('history')">
888
- <i class="fas fa-history"></i> History
889
- </div>
890
- <div class="nav-item" id="nav-profile" onclick="switchTab('profile')">
891
- <i class="fas fa-user"></i> Profile
892
- </div>
893
- </div>
894
-
895
- <!-- LOADER -->
896
- <div id="loader">
897
- <div class="loader-ring"></div>
898
- <div class="loader-text">Analyzing your food...</div>
899
- <div class="loader-step" id="loader-step">Preparing...</div>
900
- <div class="loader-hint">First run may take ~30s while AI models load</div>
901
- <button class="loader-cancel-btn" onclick="cancelAnalysis()">Cancel</button>
902
- </div>
903
-
904
- <!-- CAMERA -->
905
- <div id="cam-overlay">
906
- <video id="video" autoplay playsinline></video>
907
- <div class="cam-ui">
908
- <div class="cam-guide">
909
- <div>
910
- <div class="cam-frame"></div>
911
- <div class="cam-hint">📦 Align the product label inside the frame</div>
912
- </div>
913
- </div>
914
- <div class="cam-controls">
915
- <div class="cam-cancel" onclick="stopCamera()">Cancel</div>
916
- <div class="shutter" onclick="capture()"></div>
917
- </div>
918
- </div>
919
- </div>
920
- <canvas id="canvas" style="display:none"></canvas>
921
-
922
- <!-- LANGUAGE MODAL -->
923
- <div id="lang-modal" onclick="closeLangBackdrop(event)">
924
- <div class="lang-backdrop"></div>
925
- <div class="lang-sheet">
926
- <div class="lang-handle"></div>
927
- <div class="lang-title">Select Language</div>
928
- <div id="lang-options"></div>
929
- </div>
930
- </div>
931
-
932
- <!-- REPORT — flex column, no transform-overflow conflict -->
933
- <div id="report">
934
- <!-- Inner scroll wrapper — only this element scrolls -->
935
- <div class="report-scroll">
936
- <div class="report-hero">
937
- <div class="report-topbar">
938
- <div class="back-btn" onclick="closeReport()"><i class="fas fa-chevron-down"></i></div>
939
- <div class="verdict-badge" id="r-verdict">—</div>
940
- </div>
941
- <div class="hero-row">
942
- <div class="score-col">
943
- <div class="score-ring-wrap">
944
- <canvas id="safetyChart" width="110" height="110"></canvas>
945
- <div class="score-ring-center">
946
- <div class="score-num" id="r-score">—</div>
947
- <div class="score-sub">/ 10</div>
948
- </div>
949
- </div>
950
- <div class="pie-legend">
951
- <div class="pie-legend-item">
952
- <div class="pie-dot" style="background:#2D7A4F"></div>
953
- <span>Safe — generally healthy</span>
954
- </div>
955
- <div class="pie-legend-item">
956
- <div class="pie-dot" style="background:#C9A84C"></div>
957
- <span>Moderate — be aware</span>
958
- </div>
959
- <div class="pie-legend-item">
960
- <div class="pie-dot" style="background:#D96B47"></div>
961
- <span>Risky — limit or avoid</span>
962
- </div>
963
- </div>
964
- </div>
965
- <div class="info-col">
966
- <div class="product-cat-tag" id="r-cat"><i class="fas fa-tag"></i> <span>—</span></div>
967
- <div class="product-name" id="r-name">—</div>
968
- <div class="summary-text" id="r-summary">—</div>
969
- </div>
970
- </div>
971
- </div>
972
- <!-- Report sections — rendered once, cleared before each render -->
973
- <div class="report-body" id="report-body"></div>
974
- <!-- Bottom spacer so last section clears the sticky footer bar -->
975
- <div style="height:80px"></div>
976
- </div>
977
- </div>
978
-
979
- <!-- SHARE BAR — lives at document root, OUTSIDE #report's transform context -->
980
- <div class="report-share-bar" id="report-share-bar">
981
- <button class="share-btn s" onclick="closeReport()">
982
- <i class="fas fa-camera" style="margin-right:6px"></i>New Scan
983
- </button>
984
- <button class="share-btn p" onclick="shareReport()">
985
- <i class="fas fa-share-alt" style="margin-right:6px"></i>Share
986
- </button>
987
- </div>
988
-
989
- <div class="toast" id="toast">✅ Done!</div>
990
- <input type="file" id="fileInput" accept="image/*" style="display:none" onchange="handleFile(this.files[0])">
991
-
992
- <script>
993
- // ===== STATE =====
994
- const API = '';
995
- let currentPersona = 'General';
996
- let currentCat = 'general';
997
- let currentLang = 'en';
998
- let blob = null;
999
- let lastResult = null;
1000
- let myChart = null;
1001
- let scanHistory = [];
1002
- let saveHistory = true;
1003
- let currentAbortCtrl = null;
1004
- let reportRendered = false; // ← prevents duplicate render
1005
- const LANGUAGES = [
1006
- { code:'en', label:'English', flag:'🇬🇧' },
1007
- { code:'zh', label:'简体中文', flag:'🇨🇳' },
1008
- { code:'es', label:'Español', flag:'🇪🇸' },
1009
- { code:'ar', label:'العربية', flag:'🇸🇦' },
1010
- { code:'fr', label:'Français', flag:'🇫🇷' },
1011
- { code:'hi', label:'हिन्दी', flag:'🇮🇳' },
1012
- { code:'pt', label:'Português', flag:'🇧🇷' },
1013
- { code:'de', label:'Deutsch', flag:'🇩🇪' },
1014
- ];
1015
- const LANG_NAMES = { en:'English', zh:'Chinese', es:'Spanish', ar:'Arabic', fr:'French', hi:'Hindi', pt:'Portuguese', de:'German' };
1016
- // ===== INIT =====
1017
- window.onload = () => {
1018
- try { scanHistory = JSON.parse(localStorage.getItem('eatlytic_v3') || '[]'); } catch { scanHistory = []; }
1019
- try { saveHistory = JSON.parse(localStorage.getItem('eatlytic_save_hist') ?? 'true'); } catch { saveHistory = true; }
1020
- const savedLang = localStorage.getItem('eatlytic_lang') || 'en';
1021
- setLang(savedLang, false);
1022
- const savedPersona = localStorage.getItem('eatlytic_persona') || 'General';
1023
- currentPersona = savedPersona;
1024
- renderLangOptions();
1025
- updateProfileStats();
1026
- initScanCounter();
1027
- };
1028
- // ===== TABS =====
1029
- function switchTab(tab) {
1030
- document.querySelectorAll('.tab-page').forEach(p => p.classList.remove('active'));
1031
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
1032
- document.getElementById('page-' + tab).classList.add('active');
1033
- document.getElementById('nav-' + tab).classList.add('active');
1034
- if (tab === 'history') renderHistory();
1035
- if (tab === 'profile') updateProfileStats();
1036
- }
1037
- // ===== CHIPS =====
1038
- function setChip(el, type, value) {
1039
- const rowId = type === 'persona' ? 'persona-row' : 'cat-row';
1040
- document.querySelectorAll('#' + rowId + ' .chip').forEach(c => c.classList.remove('active'));
1041
- el.classList.add('active');
1042
- if (type === 'persona') currentPersona = value;
1043
- else currentCat = value;
1044
- }
1045
- // ===== FILE / CAMERA =====
1046
- // ===== DRAG & DROP =====
1047
- function handleDragOver(e) {
1048
- e.preventDefault();
1049
- document.getElementById('upload-drop-card').classList.add('drag-over');
1050
- }
1051
- function handleDragLeave(e) {
1052
- document.getElementById('upload-drop-card').classList.remove('drag-over');
1053
- }
1054
- function handleDrop(e) {
1055
- e.preventDefault();
1056
- document.getElementById('upload-drop-card').classList.remove('drag-over');
1057
- const file = e.dataTransfer.files[0];
1058
- if (file && file.type.startsWith('image/')) handleFile(file);
1059
- else showContextualError('file_type', '📄', 'Unsupported File', 'Please upload a photo (JPG, PNG, HEIC). Other file types are not supported.');
1060
- }
1061
- // ===== FILE HANDLING =====
1062
- async function handleFile(file) {
1063
- if (!file) return;
1064
- blob = file;
1065
- hideError();
1066
- document.getElementById('landing-ui').style.display = 'none';
1067
- document.getElementById('preview-img').src = URL.createObjectURL(file);
1068
- document.getElementById('preview-area').style.display = 'block';
1069
- document.getElementById('analyze-btn-text').textContent = 'Analyze Label';
1070
- document.getElementById('analyze-main-btn').style.display = 'none';
1071
- // Start image quality assessment with progress animation
1072
- runImageAssessment(file);
1073
- }
1074
- function setImgState(type, icon, text, tip) {
1075
- const badge = document.getElementById('img-state-badge');
1076
- badge.className = 'img-state-row ' + type;
1077
- badge.style.display = 'flex';
1078
- document.getElementById('state-icon').textContent = icon;
1079
- document.getElementById('state-text').textContent = text;
1080
- document.getElementById('state-tip').textContent = tip || '';
1081
- }
1082
- async function runImageAssessment(file) {
1083
- // Show progress bar while assessing
1084
- const progress = document.getElementById('upload-progress');
1085
- const bar = document.getElementById('upload-progress-bar');
1086
- progress.style.display = 'block';
1087
- bar.style.width = '0%';
1088
- setImgState('checking', '⏳', 'Checking image quality...', '');
1089
- // Animate progress bar
1090
- await animateBar(bar, 0, 40, 300);
1091
- // Client-side variance check
1092
- const variance = await getImageVariance(file);
1093
- await animateBar(bar, 40, 80, 300);
1094
- // Small delay so user sees the animation
1095
- await new Promise(r => setTimeout(r, 200));
1096
- await animateBar(bar, 80, 100, 200);
1097
- setTimeout(() => {
1098
- progress.style.display = 'none';
1099
- bar.style.width = '0%';
1100
- }, 400);
1101
- // Set final state badge
1102
- if (variance < 200) {
1103
- setImgState('poor', '⚠️', 'Low contrast detected', 'Try again with better lighting or move closer');
1104
- } else if (variance < 600) {
1105
- setImgState('fair', '🔆', 'Fair image quality', 'Results may vary — clearer photos give better analysis');
1106
- } else {
1107
- setImgState('good', '✅', 'Good image quality', 'Looking good — label is clear and readable');
1108
- }
1109
- // Show analyze button
1110
- document.getElementById('analyze-main-btn').style.display = 'block';
1111
- }
1112
- function animateBar(bar, from, to, ms) {
1113
- return new Promise(resolve => {
1114
- bar.style.transition = 'none';
1115
- bar.style.width = from + '%';
1116
- requestAnimationFrame(() => {
1117
- bar.style.transition = `width ${ms}ms ease`;
1118
- bar.style.width = to + '%';
1119
- setTimeout(resolve, ms);
1120
- });
1121
- });
1122
- }
1123
- function getImageVariance(file) {
1124
- return new Promise(resolve => {
1125
- const img = new Image();
1126
- img.onload = () => {
1127
- const c = document.createElement('canvas');
1128
- c.width = Math.min(img.width, 400);
1129
- c.height = Math.round(img.height * c.width / img.width);
1130
- const ctx = c.getContext('2d');
1131
- ctx.drawImage(img, 0, 0, c.width, c.height);
1132
- const d = ctx.getImageData(0, 0, c.width, c.height).data;
1133
- let vals = [];
1134
- for (let i = 0; i < d.length; i += 4 * 8)
1135
- vals.push(0.299 * d[i] + 0.587 * d[i+1] + 0.114 * d[i+2]);
1136
- const mean = vals.reduce((a,b) => a+b, 0) / vals.length;
1137
- const variance = vals.reduce((a,b) => a + (b-mean)*(b-mean), 0) / vals.length;
1138
- resolve(variance);
1139
- };
1140
- img.onerror = () => resolve(999);
1141
- img.src = URL.createObjectURL(file);
1142
- });
1143
- }
1144
- async function startCamera() {
1145
- try {
1146
- const s = await navigator.mediaDevices.getUserMedia({ video: { facingMode:'environment', width:{ideal:1920} } });
1147
- document.getElementById('video').srcObject = s;
1148
- document.getElementById('cam-overlay').classList.add('active');
1149
- } catch { alert('Camera permission denied. Please allow camera access.'); }
1150
- }
1151
- function stopCamera() {
1152
- const s = document.getElementById('video').srcObject;
1153
- if (s) s.getTracks().forEach(t => t.stop());
1154
- document.getElementById('cam-overlay').classList.remove('active');
1155
- }
1156
- function capture() {
1157
- const v = document.getElementById('video');
1158
- const c = document.getElementById('canvas');
1159
- c.width = v.videoWidth; c.height = v.videoHeight;
1160
- c.getContext('2d').drawImage(v, 0, 0);
1161
- c.toBlob(b => { stopCamera(); handleFile(b); }, 'image/jpeg', 0.95);
1162
- }
1163
- function resetHome() {
1164
- blob = null;
1165
- document.getElementById('preview-area').style.display = 'none';
1166
- document.getElementById('landing-ui').style.display = 'block';
1167
- hideError();
1168
- document.getElementById('blur-notice').classList.remove('visible');
1169
- // Reset file input so same file can be re-selected
1170
- document.getElementById('fileInput').value = '';
1171
- }
1172
- // ===== ERROR HANDLING =====
1173
- // type: 'wrong_side' | 'no_text' | 'blurry' | 'generic' | 'timeout' | 'server'
1174
- function showContextualError(type, icon, title, msg) {
1175
- blob = null;
1176
- document.getElementById('preview-area').style.display = 'none';
1177
- document.getElementById('landing-ui').style.display = 'block';
1178
- const card = document.getElementById('error-card');
1179
- const inner = document.getElementById('error-card-inner');
1180
- const flipIllus = document.getElementById('err-flip-illustration');
1181
- const retryBtn = document.getElementById('err-retry-btn');
1182
- const uploadBtn = document.getElementById('err-upload-btn');
1183
- // Reset classes
1184
- card.className = 'error-card';
1185
- flipIllus.style.display = 'none';
1186
- retryBtn.style.display = 'block';
1187
- uploadBtn.style.display = 'none';
1188
- document.getElementById('err-icon').textContent = icon;
1189
- document.getElementById('err-title').textContent = title;
1190
- document.getElementById('err-msg').textContent = msg;
1191
- if (type === 'wrong_side') {
1192
- // Special no-label / wrong-side treatment
1193
- card.classList.add('no-label');
1194
- flipIllus.style.display = 'block';
1195
- retryBtn.innerHTML = '<i class="fas fa-sync-alt" style="margin-right:6px"></i>Scan the Back Label';
1196
- uploadBtn.style.display = 'block';
1197
- uploadBtn.innerHTML = '<i class="fas fa-image" style="margin-right:6px"></i>Upload Different Image';
1198
- } else if (type === 'blurry') {
1199
- retryBtn.innerHTML = '<i class="fas fa-camera" style="margin-right:6px"></i>Retake Photo — Move Closer';
1200
- uploadBtn.style.display = 'block';
1201
- uploadBtn.innerHTML = '<i class="fas fa-image" style="margin-right:6px"></i>Upload from Gallery';
1202
- } else if (type === 'timeout') {
1203
- retryBtn.innerHTML = '<i class="fas fa-redo" style="margin-right:6px"></i>Try Again';
1204
- uploadBtn.style.display = 'block';
1205
- uploadBtn.innerHTML = '<i class="fas fa-image" style="margin-right:6px"></i>Use Different Image';
1206
- } else {
1207
- retryBtn.innerHTML = '<i class="fas fa-camera" style="margin-right:6px"></i>Retake Photo';
1208
- uploadBtn.style.display = 'block';
1209
- uploadBtn.innerHTML = '<i class="fas fa-image" style="margin-right:6px"></i>Upload from Gallery';
1210
- }
1211
- card.style.display = 'block';
1212
- }
1213
- // Legacy compat shim (called by old paths)
1214
- function showError(icon, title, msg) {
1215
- showContextualError('generic', icon, title, msg);
1216
- }
1217
- function hideError() {
1218
- document.getElementById('error-card').style.display = 'none';
1219
- document.getElementById('error-card').classList.remove('no-label');
1220
- }
1221
- function triggerRetake() {
1222
- hideError();
1223
- startCamera();
1224
- }
1225
- // ===== ANALYZE =====
1226
- function cancelAnalysis() {
1227
- if (currentAbortCtrl) currentAbortCtrl.abort();
1228
- }
1229
- // Show / hide the blur notice banner on the home tab
1230
- function handleBlurNotice(blurInfo) {
1231
- const notice = document.getElementById('blur-notice');
1232
- if (!blurInfo || !blurInfo.detected) {
1233
- notice.classList.remove('visible');
1234
- return;
1235
- }
1236
- const pill = document.getElementById('bn-severity-pill');
1237
- const body = document.getElementById('bn-body');
1238
- const methods = document.getElementById('bn-methods');
1239
- const title = document.getElementById('bn-title');
1240
- const sev = blurInfo.severity || 'moderate';
1241
- pill.textContent = sev.toUpperCase();
1242
- pill.className = `severity-pill ${sev}`;
1243
- if (blurInfo.deblurred) {
1244
- title.textContent = '🔧 Blur Detected & Corrected';
1245
- body.textContent = `Image identified as ${sev}ly blurry. ` +
1246
- `Deblurring and enhancement algorithms were applied. ` +
1247
- `Analysis is based on the enhanced image. ` +
1248
- `Composite blur score: ${blurInfo.score}/100.`;
1249
- if (blurInfo.method_log) {
1250
- methods.textContent = 'Pipeline: ' + blurInfo.method_log;
1251
- methods.style.display = 'block';
1252
- }
1253
- } else {
1254
- title.textContent = '⚠️ Blur Detected — Using Original';
1255
- body.textContent = `Blur detected (severity: ${sev}, score: ${blurInfo.score}/100). ` +
1256
- `Deblurring was attempted but the original image yielded better OCR results. ` +
1257
- `Results may be less accurate — consider retaking with better lighting.`;
1258
- methods.style.display = 'none';
1259
- }
1260
- notice.classList.add('visible');
1261
- }
1262
- // ===== SCAN QUOTA COUNTER =====
1263
- async function initScanCounter() {
1264
- try {
1265
- const r = await fetch(`${API}/scan-quota`);
1266
- const q = await r.json();
1267
- updateScanCounter(q);
1268
- } catch { /* silent fail */ }
1269
- }
1270
- function updateScanCounter(q) {
1271
- if (!q) return;
1272
- const el = document.getElementById('scan-counter');
1273
- if (!el) return;
1274
- const rem = q.remaining ?? (q.limit - q.used);
1275
- const lim = q.limit ?? 5;
1276
- if (rem <= 0) {
1277
- el.innerHTML = `<span class="quota-badge empty">🔒 Daily limit reached — <a href="#" style="color:inherit;font-weight:700">Upgrade to Pro</a></span>`;
1278
- } else {
1279
- el.innerHTML = `<span class="quota-badge" style="opacity:${rem <= 2 ? 1 : 0.75}">⚡ ${rem}/${lim} free scans today</span>`;
1280
- }
1281
- }
1282
- async function analyze() {
1283
- if (!blob) return;
1284
- hideError();
1285
- const loader = document.getElementById('loader');
1286
- const stepEl = document.getElementById('loader-step');
1287
- loader.style.display = 'flex';
1288
- const steps = [
1289
- '📷 Checking image quality...',
1290
- '🔧 Detecting & correcting blur...',
1291
- '🔍 Reading label text (OCR)...',
1292
- '🌐 Searching nutrition database...',
1293
- '🧠 Running AI analysis...',
1294
- '📊 Building your report...',
1295
- ];
1296
- let si = 0;
1297
- stepEl.textContent = steps[0];
1298
- const stepTimer = setInterval(() => {
1299
- si = Math.min(si + 1, steps.length - 1);
1300
- stepEl.textContent = steps[si];
1301
- }, 2800);
1302
- currentAbortCtrl = new AbortController();
1303
- const timeoutId = setTimeout(() => currentAbortCtrl.abort(), 100000);
1304
- const fd = new FormData();
1305
- fd.append('image', blob);
1306
- fd.append('persona', currentPersona);
1307
- fd.append('age_group', 'adult');
1308
- fd.append('product_category', currentCat);
1309
- fd.append('language', currentLang);
1310
- try {
1311
- const res = await fetch(`${API}/analyze`, {
1312
- method: 'POST', body: fd,
1313
- signal: currentAbortCtrl.signal
1314
- });
1315
- if (!res.ok) throw new Error(`Server error ${res.status}: ${res.statusText}`);
1316
- const data = await res.json();
1317
- if (data.error === 'quota_exceeded') {
1318
- showContextualError('quota','🔒','Daily Limit Reached',
1319
- data.message || `You've used all ${data.quota?.limit ?? 5} free scans for today. Come back tomorrow or upgrade to Pro.`);
1320
- updateScanCounter(data.quota);
1321
- return;
1322
- }
1323
- if (data.error === 'image_quality') {
1324
- showContextualError('blurry','📷','Image Too Blurry', data.message || 'The photo is too blurry. Move closer and hold steady.');
1325
- return;
1326
- }
1327
- if (data.error === 'no_label') {
1328
- const isWrongSide = data.tip === 'wrong_side';
1329
- showContextualError('wrong_side', isWrongSide ? '🔄' : '📋',
1330
- isWrongSide ? "That's the Front of the Product" : 'No Nutrition Label Found',
1331
- data.message || 'Please flip your product and scan the back label showing ingredients and nutrition facts.');
1332
- return;
1333
- }
1334
- if (data.error === 'no_text') {
1335
- showContextualError('blurry','🔍','No Text Detected', data.message || 'No text visible. Make sure the label is clearly in frame.');
1336
- return;
1337
- }
1338
- if (data.error === 'ocr_quality') {
1339
- showContextualError('blurry','📋','Label Unreadable', data.message || 'Retake the photo — words are missing.');
1340
- return;
1341
- }
1342
- if (data.error) throw new Error(data.error);
1343
- // Show blur notice if blur was detected
1344
- handleBlurNotice(data.blur_info);
1345
- if (data.quota) updateScanCounter(data.quota);
1346
- lastResult = data;
1347
- renderReport(data);
1348
- if (saveHistory) addToHistory(data);
1349
- } catch (e) {
1350
- if (e.name === 'AbortError') {
1351
- showContextualError('timeout', '⏱️', 'Request Timed Out', 'The server took too long. This happens on first use while AI models load. Please try again in a moment.');
1352
- } else {
1353
- showContextualError('generic', '❌', 'Analysis Failed', e.message || 'Something went wrong. Please try again.');
1354
- }
1355
- } finally {
1356
- clearInterval(stepTimer);
1357
- clearTimeout(timeoutId);
1358
- currentAbortCtrl = null;
1359
- loader.style.display = 'none';
1360
- }
1361
- }
1362
- // ===== RENDER REPORT =====
1363
- // Guard flag prevents the body from being written twice
1364
- function renderReport(d) {
1365
- // ── Hero text ─────────────────────────────────────────────
1366
- document.getElementById('r-score').textContent = d.score ?? '—';
1367
- document.getElementById('r-verdict').textContent = (d.verdict || 'Analyzed').toUpperCase();
1368
- document.getElementById('r-name').textContent = d.product_name || 'Unknown Product';
1369
- document.getElementById('r-cat').querySelector('span').textContent = d.product_category || currentCat;
1370
- document.getElementById('r-summary').textContent = d.summary || '';
1371
- // ── Pie chart (destroy first, always fresh) ───────────────
1372
- if (myChart) { myChart.destroy(); myChart = null; }
1373
- const ctx = document.getElementById('safetyChart').getContext('2d');
1374
- const cd = Array.isArray(d.chart_data) && d.chart_data.length === 3 ? d.chart_data : [70,20,10];
1375
- myChart = new Chart(ctx, {
1376
- type: 'doughnut',
1377
- data: {
1378
- datasets: [{
1379
- data: cd,
1380
- backgroundColor: ['#2D7A4F','#C9A84C','#D96B47'],
1381
- borderWidth: 0, hoverOffset: 3
1382
- }]
1383
- },
1384
- options: {
1385
- cutout: '74%',
1386
- plugins: { legend:{ display:false }, tooltip:{ enabled:false } },
1387
- animation: { duration:900 }
1388
- }
1389
- });
1390
- // ── Body — SINGLE write, guaranteed ──────────────────────
1391
- // Build HTML string first, then assign once.
1392
- // Two-step assignment (clear then set) can cause flicker;
1393
- // single assignment is atomic in the DOM.
1394
- const bodyHtml = buildBody(d);
1395
- const body = document.getElementById('report-body');
1396
- body.innerHTML = bodyHtml; // one write, no duplicates
1397
- // ── Open report + show footer bar ────────────────────────
1398
- document.getElementById('report').classList.add('open');
1399
- document.getElementById('report-share-bar').classList.add('visible');
1400
- // Scroll inner scroll wrapper to top (not the #report which has overflow:hidden)
1401
- const scroller = document.querySelector('#report .report-scroll');
1402
- if (scroller) scroller.scrollTop = 0;
1403
- // ── Animate nutrient bars (wait one frame for DOM paint) ──
1404
- requestAnimationFrame(() => {
1405
- setTimeout(() => {
1406
- document.querySelectorAll('#report-body .n-bar[data-w]').forEach(b => {
1407
- b.style.width = b.dataset.w + '%';
1408
- });
1409
- }, 160);
1410
- });
1411
- }
1412
- function buildBody(d) {
1413
- const nutrients = Array.isArray(d.nutrient_breakdown) ? d.nutrient_breakdown : [];
1414
- const pros = Array.isArray(d.pros) ? d.pros : [];
1415
- const cons = Array.isArray(d.cons) ? d.cons : [];
1416
- const ageW = Array.isArray(d.age_warnings) ? d.age_warnings : [];
1417
- const warnings = Array.isArray(d.health_warnings) ? d.health_warnings : [];
1418
- const contras = Array.isArray(d.contraindications) ? d.contraindications : [];
1419
- const allergens = Array.isArray(d.allergens) ? d.allergens : [];
1420
- const ingList = Array.isArray(d.ingredients_list) ? d.ingredients_list : [];
1421
- const spotlight = Array.isArray(d.ingredients_spotlight) ? d.ingredients_spotlight : [];
1422
- let html = '';
1423
- // 0. Blur info section
1424
- const bi = d.blur_info;
1425
- if (bi && bi.detected) {
1426
- if (bi.deblurred && bi.image_b64) {
1427
- html += `<div class="r-section" style="padding:14px">
1428
- <div class="deblur-report-badge">🔧 Blur Corrected — Enhanced Image Used</div>
1429
- <div class="deblur-img-wrap">
1430
- <img src="${bi.image_b64}" alt="Deblurred image" loading="lazy">
1431
- </div>
1432
- <div style="font-size:0.76rem;color:var(--mist);line-height:1.5">
1433
- The original image was <strong>${escHtml(bi.severity)}ly blurry</strong>
1434
- (score&nbsp;${bi.score}/100). It was processed through a
1435
- multi-stage enhancement pipeline before OCR and analysis.
1436
- ${bi.method_log ? `<br><span style="font-family:monospace;font-size:0.68rem;color:#A16207">Pipeline: ${escHtml(bi.method_log)}</span>` : ''}
1437
- </div>
1438
- </div>`;
1439
- } else {
1440
- html += `<div class="r-section" style="padding:14px;background:#FFFBEB;border:1.5px solid #FCD34D;border-radius:var(--radius-sm)">
1441
- <div class="deblur-report-badge">⚠️ Blur Detected</div>
1442
- <div style="font-size:0.76rem;color:#78350F;line-height:1.5">
1443
- Blur detected (severity:&nbsp;<strong>${escHtml(bi.severity)}</strong>,
1444
- score&nbsp;${bi.score}/100). Original image used after comparison.
1445
- Retake with better lighting for higher accuracy.
1446
- </div>
1447
- </div>`;
1448
- }
1449
- }
1450
- // 0b. Low-confidence notice
1451
- if (d.is_low_confidence) {
1452
- html += `<div class="r-section" style="padding:12px;background:#FFF7ED;border:1.5px solid #FDBA74;border-radius:var(--radius-sm)">
1453
- <div style="font-size:0.8rem;color:#92400E;font-weight:600">⚠️ Partial Label — Lower Confidence</div>
1454
- <div style="font-size:0.75rem;color:#92400E;margin-top:4px">OCR could only extract partial text. Results are directional — retake with a clearer photo for full accuracy.</div>
1455
- </div>`;
1456
- }
1457
- // 1. Health Warnings (CRITICAL — shown first after image info)
1458
- if (warnings.length) {
1459
- const sevOrder = { critical: 0, moderate: 1, low: 2 };
1460
- const sorted = [...warnings].sort((a, b) => (sevOrder[a.severity] ?? 2) - (sevOrder[b.severity] ?? 2));
1461
- const sevStyle = {
1462
- critical: { bg: '#FEF2F2', border: '#F87171', icon: '🚨', color: '#991B1B', label: 'CRITICAL' },
1463
- moderate: { bg: '#FFFBEB', border: '#FCD34D', icon: '⚠️', color: '#92400E', label: 'MODERATE' },
1464
- low: { bg: '#F0F9FF', border: '#7DD3FC', icon: 'ℹ️', color: '#075985', label: 'NOTE' },
1465
- };
1466
- html += `<div class="r-section">
1467
- <div class="r-title">Health Warnings</div>
1468
- <div class="warnings-list">
1469
- ${sorted.map(w => {
1470
- const s = sevStyle[w.severity] || sevStyle.low;
1471
- const groups = Array.isArray(w.affected_groups) && w.affected_groups.length
1472
- ? `<div class="warn-groups">${w.affected_groups.map(g => `<span class="warn-tag">${escHtml(g)}</span>`).join('')}</div>` : '';
1473
- return `<div class="warn-card" style="background:${s.bg};border-left:4px solid ${s.border}">
1474
- <div class="warn-header">
1475
- <span class="warn-icon">${s.icon}</span>
1476
- <span class="warn-sev" style="color:${s.color}">${s.label}</span>
1477
- <span class="warn-cat">${escHtml(w.category || '')}</span>
1478
- </div>
1479
- <div class="warn-text" style="color:${s.color}">${escHtml(w.warning)}</div>
1480
- ${groups}
1481
- </div>`;
1482
- }).join('')}
1483
- </div>
1484
- </div>`;
1485
- }
1486
- // 2. Allergens
1487
- if (allergens.length) {
1488
- html += `<div class="r-section">
1489
- <div class="r-title">Allergen Information</div>
1490
- <div class="allergen-list">
1491
- ${allergens.map(a => `<div class="allergen-item">⚠️ ${escHtml(a)}</div>`).join('')}
1492
- </div>
1493
- </div>`;
1494
- }
1495
- // 3. Contraindications
1496
- if (contras.length) {
1497
- html += `<div class="r-section">
1498
- <div class="r-title">Contraindications</div>
1499
- <div class="contra-list">
1500
- ${contras.map(c => `<div class="contra-item">🚫 ${escHtml(c)}</div>`).join('')}
1501
- </div>
1502
- </div>`;
1503
- }
1504
- // 4. Simple Explanation
1505
- if (d.eli5_explanation) {
1506
- html += `<div class="r-section">
1507
- <div class="r-title">Simple Explanation</div>
1508
- <div class="eli5-box">
1509
- <span class="eli5-label">🍭 Easy Read</span>
1510
- ${escHtml(d.eli5_explanation)}
1511
- </div>
1512
- </div>`;
1513
- }
1514
- // 5. Nutritional Breakdown
1515
- if (nutrients.length) {
1516
- const MAXES = { g: 50, mg: 2000, '%': 100, kcal: 500, kj: 2000 };
1517
- const servingSize = d.serving_size ? `<div style="font-size:0.72rem;color:#4A7C59;background:#EAF5EE;border:1px solid #7FB896;border-radius:6px;padding:5px 10px;margin-bottom:10px;font-weight:600;">📏 Values shown per ${escHtml(d.serving_size)}</div>` : '';
1518
- html += `<div class="r-section">
1519
- <div class="r-title">Nutritional Breakdown</div>
1520
- ${servingSize}
1521
- <div class="nutrient-legend">
1522
- <div class="n-leg"><div class="n-dot" style="background:#2D7A4F"></div>Good</div>
1523
- <div class="n-leg"><div class="n-dot" style="background:#C9A84C"></div>Moderate</div>
1524
- <div class="n-leg"><div class="n-dot" style="background:#D96B47"></div>Caution</div>
1525
- <div class="n-leg"><div class="n-dot" style="background:#8B0000"></div>Bad</div>
1526
- </div>
1527
- ${nutrients.map(n => {
1528
- const r = n.rating || 'moderate';
1529
- const rawVal = parseFloat(String(n.value).replace(/[^0-9.]/g, ''));
1530
- const max = MAXES[n.unit] || 100;
1531
- const pct = isNaN(rawVal) ? 0 : Math.min(100, (rawVal / max) * 100);
1532
- const displayVal = isNaN(rawVal) ? String(n.value) : rawVal;
1533
- return `<div class="nutrient-item">
1534
- <div class="nutrient-row">
1535
- <div class="n-name">${escHtml(n.name)}</div>
1536
- <div class="n-bar-wrap"><div class="n-bar bar-${r}" data-w="${pct.toFixed(1)}"></div></div>
1537
- <div class="n-val">${displayVal}${n.unit || ''}</div>
1538
- </div>
1539
- ${n.impact ? `<div class="n-impact">${escHtml(n.impact)}</div>` : ''}
1540
- </div>`;
1541
- }).join('')}
1542
- </div>`;
1543
- }
1544
- // 6. Pros & Cons
1545
- if (pros.length || cons.length) {
1546
- html += `<div class="r-section">
1547
- <div class="r-title">Pros &amp; Cons</div>
1548
- <div class="pc-grid">
1549
- <div>
1550
- <div class="pc-label p">Benefits</div>
1551
- ${pros.map(p => `<div class="pc-item"><div class="pc-dot p"></div><span>${escHtml(p)}</span></div>`).join('')}
1552
- </div>
1553
- <div>
1554
- <div class="pc-label c">Watch Out</div>
1555
- ${cons.map(c => `<div class="pc-item"><div class="pc-dot c"></div><span>${escHtml(c)}</span></div>`).join('')}
1556
- </div>
1557
- </div>
1558
- </div>`;
1559
- }
1560
- // 7. Consumption Guidance (NEW)
1561
- const hasConsumption = d.daily_limit || d.optimal_timing || d.overconsumption_effects || d.dietary_advice;
1562
- if (hasConsumption) {
1563
- html += `<div class="r-section">
1564
- <div class="r-title">Healthy Consumption Guidance</div>
1565
- <div class="consumption-grid">
1566
- ${d.daily_limit ? `<div class="cons-card">
1567
- <div class="cons-icon">📏</div>
1568
- <div class="cons-label">Daily Limit</div>
1569
- <div class="cons-text">${escHtml(d.daily_limit)}</div>
1570
- </div>` : ''}
1571
- ${d.optimal_timing ? `<div class="cons-card">
1572
- <div class="cons-icon">⏰</div>
1573
- <div class="cons-label">Best Time to Consume</div>
1574
- <div class="cons-text">${escHtml(d.optimal_timing)}</div>
1575
- </div>` : ''}
1576
- ${d.overconsumption_effects ? `<div class="cons-card cons-full">
1577
- <div class="cons-icon">📈</div>
1578
- <div class="cons-label">Effects of Overconsumption</div>
1579
- <div class="cons-text">${escHtml(d.overconsumption_effects)}</div>
1580
- </div>` : ''}
1581
- ${d.dietary_advice ? `<div class="cons-card cons-full">
1582
- <div class="cons-icon">🥗</div>
1583
- <div class="cons-label">Balanced Diet Tip</div>
1584
- <div class="cons-text">${escHtml(d.dietary_advice)}</div>
1585
- </div>` : ''}
1586
- </div>
1587
- </div>`;
1588
- }
1589
- // 8. Product Analysis paragraphs
1590
- if (d.paragraph_benefits || d.paragraph_uniqueness) {
1591
- html += `<div class="r-section">
1592
- <div class="r-title">Product Analysis</div>
1593
- ${d.paragraph_benefits ? `<p class="para-text">${escHtml(d.paragraph_benefits)}</p>` : ''}
1594
- ${d.paragraph_uniqueness ? `<p class="para-text">${escHtml(d.paragraph_uniqueness)}</p>` : ''}
1595
- </div>`;
1596
- }
1597
- // 9. Age Group Warnings
1598
- if (ageW.length) {
1599
- html += `<div class="r-section">
1600
- <div class="r-title">By Age Group</div>
1601
- <div class="age-grid">
1602
- ${ageW.map(w => `
1603
- <div class="age-card ${w.status || 'caution'}">
1604
- <div class="age-emoji">${w.emoji || '👤'}</div>
1605
- <div class="age-grp">${escHtml(w.group)}</div>
1606
- <div class="age-msg">${escHtml(w.message)}</div>
1607
- </div>`).join('')}
1608
- </div>
1609
- </div>`;
1610
- }
1611
- // 10. Curiosity Fact + Wellness Tip
1612
- if (d.curiosity_fact || d.wellness_tip) {
1613
- html += `<div class="r-section">
1614
- <div class="r-title">Curiosity Corner</div>
1615
- ${d.curiosity_fact ? `<div class="curiosity-box">💡 ${escHtml(d.curiosity_fact)}</div>` : ''}
1616
- ${d.wellness_tip ? `<div class="wellness-box">🌿 <strong>Tip for you:</strong> ${escHtml(d.wellness_tip)}</div>` : ''}
1617
- </div>`;
1618
- }
1619
- // 11. Molecular Insight
1620
- if (d.molecular_insight) {
1621
- html += `<div class="r-section">
1622
- <div class="r-title">Molecular Insight</div>
1623
- <div class="mol-box">🔬 ${escHtml(d.molecular_insight)}</div>
1624
- </div>`;
1625
- }
1626
- // 12. Ingredients Spotlight (tap-to-learn)
1627
- if (spotlight.length) {
1628
- html += `<div class="r-section">
1629
- <div class="r-title">Ingredients Spotlight</div>
1630
- <div class="spotlight-list">
1631
- ${spotlight.map(s => `
1632
- <div class="spotlight-item" onclick="openIngSheet('${escHtml(s.name).replace(/'/g,"\\'")}')">
1633
- <div class="spot-name">${escHtml(s.name)}</div>
1634
- <div class="spot-role">${escHtml(s.role || '')}</div>
1635
- <div class="spot-fact">${escHtml(s.curiosity || '')}</div>
1636
- <div class="spot-tap">Tap to learn more →</div>
1637
- </div>`).join('')}
1638
- </div>
1639
- </div>`;
1640
- }
1641
- // 13. Full Ingredients List
1642
- if (ingList.length) {
1643
- html += `<div class="r-section">
1644
- <div class="r-title">Full Ingredients</div>
1645
- <div class="ingredients-full">
1646
- ${ingList.map(i => `<span class="ing-chip">${escHtml(i)}</span>`).join('')}
1647
- </div>
1648
- </div>`;
1649
- }
1650
- // 14. Better Alternative
1651
- if (d.better_alternative) {
1652
- html += `<div class="r-section" style="padding:0;overflow:hidden">
1653
- <div class="alt-box">
1654
- <div class="alt-icon">💡</div>
1655
- <div>
1656
- <div class="alt-lbl">Better Alternative</div>
1657
- <div class="alt-text">${escHtml(d.better_alternative)}</div>
1658
- </div>
1659
- </div>
1660
- </div>`;
1661
- }
1662
- return html;
1663
- }
1664
- function escHtml(str) {
1665
- // Only escape the four chars that break HTML structure.
1666
- // Do NOT encode quotes in text nodes — causes doubled encoding.
1667
- // Emojis, unicode, special chars pass through safely as text nodes.
1668
- if (str === null || str === undefined) return '';
1669
- return String(str)
1670
- .replace(/&/g, '&amp;')
1671
- .replace(/</g, '&lt;')
1672
- .replace(/>/g, '&gt;');
1673
- }
1674
- function closeReport() {
1675
- document.getElementById('report').classList.remove('open');
1676
- document.getElementById('report-share-bar').classList.remove('visible');
1677
- // Reset scroll so next open starts from top
1678
- const scroller = document.querySelector('#report .report-scroll');
1679
- if (scroller) scroller.scrollTop = 0;
1680
- }
1681
- // ===== HISTORY =====
1682
- function addToHistory(data) {
1683
- const item = { id: Date.now(), date: new Date().toLocaleDateString(), lang: currentLang, data };
1684
- scanHistory.unshift(item);
1685
- if (scanHistory.length > 30) scanHistory.pop();
1686
- try { localStorage.setItem('eatlytic_v3', JSON.stringify(scanHistory)); } catch {}
1687
- updateBadge();
1688
- }
1689
- function renderHistory() {
1690
- const div = document.getElementById('history-list');
1691
- if (!scanHistory.length) {
1692
- div.innerHTML = `<div class="history-empty"><i class="fas fa-history"></i>No scans yet.<br>Go to Home and scan a label to get started.</div>`;
1693
- return;
1694
- }
1695
- div.innerHTML = scanHistory.map(item => {
1696
- const sc = item.data.score ?? 0;
1697
- const col = sc >= 7 ? '#2D7A4F' : sc >= 4 ? '#B07D22' : '#C94B1E';
1698
- const bg = sc >= 7 ? '#ECFDF5' : sc >= 4 ? '#FFFBEB' : '#FEF2F2';
1699
- return `<div class="history-card" onclick="loadScan(${item.id})">
1700
- <div class="h-score-ring" style="background:${bg};color:${col}">${sc}</div>
1701
- <div style="flex:1">
1702
- <div class="h-name">${escHtml(item.data.product_name || 'Unknown')}</div>
1703
- <div class="h-meta">${escHtml(item.data.verdict || '')} · ${item.date}</div>
1704
- </div>
1705
- <i class="fas fa-trash h-del" onclick="deleteScan(${item.id},event)"></i>
1706
- </div>`;
1707
- }).join('') +
1708
- (scanHistory.length > 1 ? `<button class="history-clear-btn" onclick="clearAllHistory()">🗑 Clear All History</button>` : '');
1709
- }
1710
- function loadScan(id) {
1711
- const item = scanHistory.find(i => i.id === id);
1712
- if (!item) return;
1713
- lastResult = item.data;
1714
- switchTab('home'); // switch first so home tab is active
1715
- renderReport(item.data); // then open report on top
1716
- }
1717
- function deleteScan(id, e) {
1718
- e.stopPropagation();
1719
- scanHistory = scanHistory.filter(i => i.id !== id);
1720
- try { localStorage.setItem('eatlytic_v3', JSON.stringify(scanHistory)); } catch {}
1721
- renderHistory();
1722
- updateBadge();
1723
- }
1724
- function clearAllHistory() {
1725
- if (!confirm('Delete all scan history?')) return;
1726
- scanHistory = [];
1727
- try { localStorage.removeItem('eatlytic_v3'); } catch {}
1728
- renderHistory();
1729
- updateProfileStats();
1730
- updateBadge();
1731
- }
1732
- function updateBadge() {
1733
- // Could show count badge on nav — leave as hook
1734
- }
1735
- // ===== PROFILE =====
1736
- function updateProfileStats() {
1737
- document.getElementById('stat-scans').textContent = scanHistory.length;
1738
- if (scanHistory.length) {
1739
- const scores = scanHistory.map(i => i.data.score).filter(s => s != null);
1740
- const avg = scores.length ? Math.round(scores.reduce((a,b)=>a+b,0)/scores.length) : '—';
1741
- const best = scores.length ? Math.max(...scores) : '—';
1742
- document.getElementById('stat-avg').textContent = avg;
1743
- document.getElementById('stat-best').textContent = best;
1744
- } else {
1745
- document.getElementById('stat-avg').textContent = '—';
1746
- document.getElementById('stat-best').textContent = '—';
1747
- }
1748
- document.getElementById('pref-lang-val').textContent = LANG_NAMES[currentLang] || 'English';
1749
- document.getElementById('pref-persona-val').textContent = currentPersona;
1750
- document.getElementById('pref-persona-sel').value = currentPersona;
1751
- document.getElementById('prof-sub').textContent = `${currentPersona} · ${LANG_NAMES[currentLang] || 'English'}`;
1752
- const toggle = document.getElementById('history-toggle');
1753
- toggle.className = 'toggle-switch ' + (saveHistory ? 'on' : '');
1754
- }
1755
- function savePersonaPref(val) {
1756
- currentPersona = val;
1757
- // Sync persona chips on home tab
1758
- document.querySelectorAll('#persona-row .chip').forEach(c => {
1759
- c.classList.toggle('active', c.textContent.includes(val));
1760
- });
1761
- localStorage.setItem('eatlytic_persona', val);
1762
- updateProfileStats();
1763
- }
1764
- function toggleHistorySave() {
1765
- saveHistory = !saveHistory;
1766
- try { localStorage.setItem('eatlytic_save_hist', JSON.stringify(saveHistory)); } catch {}
1767
- updateProfileStats();
1768
- }
1769
- // ===== LANGUAGE =====
1770
- function openLang() { document.getElementById('lang-modal').classList.add('open'); }
1771
- function closeLangBackdrop(e) {
1772
- if (e.target.classList.contains('lang-backdrop') || e.target.id === 'lang-modal')
1773
- document.getElementById('lang-modal').classList.remove('open');
1774
- }
1775
- function renderLangOptions() {
1776
- document.getElementById('lang-options').innerHTML = LANGUAGES.map(l => `
1777
- <div class="lang-option ${l.code === currentLang ? 'active' : ''}" onclick="setLang('${l.code}')">
1778
- <span class="lang-flag">${l.flag}</span><span>${l.label}</span>
1779
- </div>`).join('');
1780
- }
1781
- function setLang(code, save = true) {
1782
- currentLang = code;
1783
- document.getElementById('lang-display').textContent = code.toUpperCase();
1784
- document.getElementById('analyze-btn-text').textContent = 'Analyze Label';
1785
- document.getElementById('lang-modal').classList.remove('open');
1786
- if (save) try { localStorage.setItem('eatlytic_lang', code); } catch {}
1787
- renderLangOptions();
1788
- updateProfileStats();
1789
- }
1790
- // ===== SHARE =====
1791
- async function shareReport() {
1792
- if (!lastResult) return;
1793
- const text = `🌿 Eatlytic Food Report\n\n${lastResult.product_name}\nScore: ${lastResult.score}/10 — ${lastResult.verdict}\n\n${lastResult.summary}`;
1794
- try {
1795
- if (navigator.share) { await navigator.share({ title:'Eatlytic Report', text }); }
1796
- else { await navigator.clipboard.writeText(text); showToast('📋 Copied to clipboard!'); }
1797
- } catch { showToast('📋 Report copied!'); }
1798
- }
1799
- function showToast(msg = '✅ Done!') {
1800
- const t = document.getElementById('toast');
1801
- t.textContent = msg;
1802
- t.classList.add('show');
1803
- setTimeout(() => t.classList.remove('show'), 2800);
1804
- }
1805
- </script>
1806
- </body>
1807
- </html>