-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
4180 lines (3934 loc) · 162 KB
/
Copy pathapp.js
File metadata and controls
4180 lines (3934 loc) · 162 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const STORAGE_KEY = "fitsnap-coach-state-v2";
const DB_NAME = "fitsnap-coach-db";
const DB_VERSION = 2;
const CURRENT_USER_ID = "local-user";
const TFJS_URL = "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js";
const POSE_DETECTION_URL =
"https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection@2.1.3/dist/pose-detection.min.js";
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
const today = new Date();
const todayKey = toDateKey(today);
const weekdayLabels = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
const weekdayLabelsEn = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const defaultState = {
profile: {
name: "Gloria",
age: 31,
sex: "female",
heightCm: 165,
currentWeightKg: 62,
targetWeightKg: 56,
activityLevel: "moderate",
trainingExperience: "beginner",
trainingDaysPerWeek: 4,
physiqueGoal: "减脂线条",
dietPreference: "高蛋白、少糖",
injuries: "",
equipment: "gym",
},
nutritionTarget: null,
meals: [],
workoutPlan: [],
completedWorkoutDates: {},
formAnalyses: [],
mediaAssets: [],
agent: {
status: "idle",
lastRunAt: null,
tasks: [],
messages: [],
},
settings: {
language: "zh",
},
health: {
authorized: false,
metrics: {
sleepHours: 6.7,
hrvMs: 48,
restingHeartRate: 62,
spo2: 97,
steps: 8200,
activeEnergyKcal: 430,
workoutLoad: 62,
},
history: [],
updatedAt: null,
},
updatedAt: null,
};
let state = clone(defaultState);
let dbReady = false;
let pendingMealImage = "";
let pendingMealImageName = "";
let pendingMealImageSize = 0;
let pendingFormMedia = null;
let pendingFormMediaUrl = "";
let pendingFormMediaPreview = "";
let liveMotionStream = null;
let liveMotionVideo = null;
let liveMotionCanvas = null;
let liveMotionFrame = 0;
let liveMotionLastInferenceAt = 0;
let liveMotionAnalysis = null;
let poseDetector = null;
let poseModelStatus = "idle";
let poseModelMessage = "";
let visualizationState = {
period: "week",
metric: "calories",
points: [],
hitRegions: [],
};
const i18n = {
zh: {
"brand.subtitle": "AI 健身营养教练",
"nav.today": "今日",
"nav.agent": "Agent",
"nav.insights": "趋势",
"nav.profile": "目标",
"nav.nutrition": "饮食",
"nav.training": "训练",
"nav.motion": "动作",
"nav.health": "健康",
"button.reset": "重置",
"button.refresh": "刷新",
"button.language": "EN",
"button.savePlan": "保存并生成计划",
"button.estimateMeal": "估算并记录",
"button.analyzeForm": "生成动作反馈",
"button.connectHealth": "模拟授权",
"button.importHealth": "导入 JSON/CSV",
"button.saveHealth": "保存恢复数据",
"button.loadPoseModel": "加载 Pose 模型",
"button.startCamera": "实时摄像",
"button.stopCamera": "停止摄像",
"button.runAgent": "运行 Agent",
"button.sendAgent": "发送",
"button.openTask": "打开",
"button.doneTask": "完成",
"button.rerunAgent": "重新规划",
"button.markDone": "标记完成",
"button.recoveryDone": "完成恢复",
"button.completed": "已完成",
"confirm.reset": "重置本地演示数据?",
"status.localSaved": "本地保存",
"status.loadingDb": "数据库加载中",
"status.synced": "已同步",
"status.goalSaved": "目标已保存",
"status.mealSaved": "餐食已记录",
"status.mealAdjusted": "餐食已调整",
"status.mealDeleted": "餐食已删除",
"status.workoutDone": "训练已完成",
"status.workoutUndone": "已取消完成",
"status.formAnalyzed": "动作已分析",
"status.healthConnected": "已模拟授权",
"status.healthSaved": "恢复数据已保存",
"status.healthImported": "健康数据已导入",
"status.adviceRefreshed": "建议已刷新",
"status.resetDone": "已重置",
"status.dbReady": "IndexedDB 已同步",
"status.storageFallback": "LocalStorage 备份",
"status.poseIdle": "Pose 模型未加载",
"status.poseLoading": "Pose 模型加载中",
"status.poseReady": "MoveNet 已就绪",
"status.poseFallback": "Pose 模型不可用,已用规则兜底",
"status.poseAnalyzing": "正在识别关键点",
"status.cameraLive": "摄像头实时分析中",
"status.cameraStopped": "摄像头已停止",
"status.cameraDenied": "摄像头不可用",
"status.agentReady": "Agent 待命",
"status.agentRunning": "Agent 思考中",
"status.agentDone": "Agent 已更新",
"status.agentTaskDone": "任务已完成",
"status.agentTaskOpened": "已打开任务位置",
"title.dashboard": "今日状态",
"title.readiness": "恢复评分",
"title.nutrition": "热量与宏量",
"title.coach": "今日建议",
"title.motionSnapshot": "动作快照",
"title.agent": "AI Agent 工作台",
"title.agentConsole": "Agent 对话",
"title.agentTasks": "行动队列",
"title.insights": "周/月互动趋势",
"title.summary": "数据摘要",
"title.profile": "目标设置",
"title.targetSummary": "目标摘要",
"title.foodLog": "饮食记录",
"title.mealDetails": "餐食明细",
"title.training": "训练计划",
"title.formCheck": "照片/视频动作分析",
"title.formFeedback": "动作反馈",
"title.health": "恢复数据",
"title.recoveryJudgement": "恢复判断",
"fine.readiness": "压力负荷为训练恢复代理指标,不等同于皮质醇检测或医疗诊断。",
"fine.healthkit": "Web 版无法直接访问 HealthKit;真实授权需 iOS 原生能力。",
"fine.form": "MVP 使用文件信息、动作模板和恢复状态生成规则反馈;生产版需接入姿态关键点模型。",
"fine.formPose": "已接入 MoveNet 关键点检测;低置信度或模型不可用时自动回退到规则分析。",
"fine.formLive": "实时摄像只做端侧预览,不自动保存每一帧;保存历史请上传照片/视频后生成动作反馈。",
"label.consumed": "已摄入",
"label.target": "目标",
"label.metric": "指标",
"label.name": "姓名",
"label.age": "年龄",
"label.sex": "生理性别",
"label.height": "身高 cm",
"label.currentWeight": "当前体重 kg",
"label.targetWeight": "目标体重 kg",
"label.activity": "活动水平",
"label.experience": "训练经验",
"label.trainingDays": "每周训练天数",
"label.physique": "想练成的效果",
"label.diet": "饮食偏好",
"label.equipment": "器械条件",
"label.injuries": "伤病或禁忌",
"label.mealType": "餐次",
"label.mealDescription": "餐食描述",
"label.mealPhoto": "餐食照片",
"label.exercise": "动作",
"label.cameraAngle": "拍摄角度",
"label.formMedia": "照片或视频",
"label.sleep": "睡眠小时",
"label.hrv": "HRV ms",
"label.rhr": "静息心率",
"label.spo2": "血氧 %",
"label.steps": "步数",
"label.activeEnergy": "活动能量 kcal",
"label.workoutLoad": "训练负荷 0-100",
"placeholder.diet": "高蛋白、少乳制品、素食...",
"placeholder.injuries": "膝盖不适、腰椎间盘、肩撞击...",
"placeholder.meal": "鸡胸肉饭、牛油果沙拉、拿铁...",
"placeholder.noPhoto": "未选择照片",
"placeholder.choosePhoto": "选择照片",
"placeholder.noMedia": "尚未上传照片/视频",
"placeholder.chooseMedia": "上传照片/视频",
"placeholder.noMeals": "今天还没有餐食记录",
"placeholder.noAnalysis": "上传照片或视频后生成动作反馈",
"placeholder.noUploads": "上传照片/视频后,这里会记录最近媒体数据。",
"placeholder.agentPrompt": "告诉 Agent:今天时间不多、想练臀腿、晚餐怎么吃...",
"placeholder.noAgentTasks": "运行 Agent 后会生成今日行动队列。",
"placeholder.noAgentMessages": "我是你的本地 AI Coach Agent。运行后会读取目标、饮食、训练、动作和恢复数据,再给出下一步。",
"alert.mealRequired": "请添加餐食描述或照片。",
"alert.formRequired": "请先上传动作照片或视频。",
"alert.cameraUnsupported": "当前浏览器不支持直接调用摄像头。",
"alert.cameraFailed": "无法开启摄像头:{message}",
"alert.importFailed": "导入失败:{message}",
"analysis.model": "分析引擎",
"analysis.poseEngine": "MoveNet Pose",
"analysis.ruleEngine": "规则兜底",
"analysis.keypointConfidence": "关键点置信度 {value}%",
"analysis.poseMissing": "未检测到稳定人体关键点,建议使用全身入镜、光线更好的侧面或正面素材。",
"analysis.poseDetected": "检测到 {count} 个有效人体关键点,平均置信度 {confidence}%。",
"analysis.livePreview": "实时预览",
"analysis.liveWaiting": "等待稳定人体关键点,请保持全身入镜并提高光线。",
"period.week": "周",
"period.month": "月",
"range.week": "最近 7 天",
"range.month": "最近 30 天",
"chart.target": "目标",
"chart.total": "周期合计",
"chart.average": "日均",
"chart.vsTarget": "对目标",
"chart.bestDay": "最高日",
"chart.empty": "还没有足够历史数据,记录餐食、训练或恢复后会自动出现趋势。",
"chart.help": "把鼠标移到图表上可查看每日明细。",
"chart.tooltip": "{metric}:{value}<br />餐食 {meals} 条 · 上传 {uploads} 次",
"section.recentUploads": "Recent Uploads",
"unit.times": "次",
"unit.points": "分",
"unit.items": "条",
"metric.calories": "热量摄入",
"metric.protein": "蛋白质",
"metric.workouts": "训练完成",
"metric.readiness": "恢复评分",
"metric.formScore": "动作评分",
"metric.uploads": "上传次数",
"macro.protein": "蛋白质",
"macro.carbs": "碳水",
"macro.fat": "脂肪",
"summary.dailyCalories": "每日热量",
"summary.protein": "蛋白质",
"summary.tdee": "TDEE",
"summary.weightDelta": "目标差值",
"summary.cutPace": "建议每周下降 0.3-0.6kg",
"summary.bulkPace": "建议每周上升 0.1-0.25kg",
"summary.recompPace": "建议围度和力量同步观察",
"summary.profile": "{name} 的当前策略为 {mode}。{pace},每周训练 {days} 天。",
"summary.macros": "碳水 {carbs}g,脂肪 {fat}g;饮食偏好:{diet}。",
"health.interpret.good": "可以按计划训练,注意不要因为状态好而一次性增加过多训练量。",
"health.interpret.medium": "训练可继续,但建议控制 RPE,优先保证动作质量和睡眠窗口。",
"health.interpret.low": "今天更适合降强度或做主动恢复;若血氧、心率持续异常,请咨询专业人士。",
"health.interpret.rest": "建议休息或轻活动,避免高强度训练;若伴随不适,优先寻求医疗建议。",
"coach.priority": "优先级",
"coach.training": "训练",
"coach.form": "动作",
"coach.recovery": "恢复",
"coach.proteinGap": "下一餐补 {proteinGap}g 左右蛋白质,热量还{calorieText}。",
"coach.calorieLeft": "剩 {value} kcal",
"coach.calorieOver": "超 {value} kcal",
"coach.overCalories": "晚间选择低脂高纤维食物,明天不需要极端节食,回到目标热量即可。",
"coach.stableNutrition": "今天营养节奏稳定,保持蛋白质优先和蔬菜体积感。",
"coach.lowReadiness": "恢复评分偏低,今日训练降到 RPE 6,保留动作练习和轻有氧。",
"coach.todayPlan": "今日安排 {focus},主动作保持 {intensity},组间休息约 {rest}。",
"coach.formIssue": "{exercise} 先处理 {compensation},下一组用更轻重量拍 {angle}角度。",
"coach.recoveryFactor": "{factor} 是当前主要压力信号,今晚把睡眠窗口提前 30 分钟。",
"coach.recoveryStable": "恢复指标可训练,睡前保持固定放松流程以稳定 HRV。",
"auth.authorized": "已授权",
"auth.unauthorized": "未授权",
"meal.count": "{count} 条",
"training.progress": "{done}/{total} 完成",
"analysis.confidence": "置信度 {value}%",
"risk.low": "低风险",
"risk.medium": "中等风险",
"risk.high": "高风险",
"readiness.good": "恢复良好",
"readiness.medium": "压力适中",
"readiness.high": "压力偏高",
"readiness.rest": "建议主动恢复",
"mode.cut": "稳态减脂",
"mode.bulk": "轻盈增肌",
"mode.recomp": "体态重组",
"media.meal": "餐食照片",
"media.formAnalysis": "动作媒体",
"media.file": "上传文件",
"agent.observe": "观察",
"agent.reason": "推理",
"agent.act": "行动",
"agent.priority.high": "高优先级",
"agent.priority.medium": "中优先级",
"agent.priority.low": "低优先级",
"agent.status.open": "待处理",
"agent.status.active": "进行中",
"agent.status.done": "已完成",
"agent.evidence": "依据",
},
en: {
"brand.subtitle": "AI fitness and nutrition coach",
"nav.today": "Today",
"nav.agent": "Agent",
"nav.insights": "Trends",
"nav.profile": "Goals",
"nav.nutrition": "Nutrition",
"nav.training": "Training",
"nav.motion": "Form",
"nav.health": "Health",
"button.reset": "Reset",
"button.refresh": "Refresh",
"button.language": "中",
"button.savePlan": "Save and build plan",
"button.estimateMeal": "Estimate and log",
"button.analyzeForm": "Generate form feedback",
"button.connectHealth": "Simulate access",
"button.importHealth": "Import JSON/CSV",
"button.saveHealth": "Save recovery data",
"button.loadPoseModel": "Load pose model",
"button.startCamera": "Live camera",
"button.stopCamera": "Stop camera",
"button.runAgent": "Run agent",
"button.sendAgent": "Send",
"button.openTask": "Open",
"button.doneTask": "Done",
"button.rerunAgent": "Replan",
"button.markDone": "Mark done",
"button.recoveryDone": "Finish recovery",
"button.completed": "Completed",
"confirm.reset": "Reset local demo data?",
"status.localSaved": "Saved locally",
"status.loadingDb": "Loading database",
"status.synced": "Synced",
"status.goalSaved": "Goals saved",
"status.mealSaved": "Meal logged",
"status.mealAdjusted": "Meal adjusted",
"status.mealDeleted": "Meal deleted",
"status.workoutDone": "Workout completed",
"status.workoutUndone": "Workout unchecked",
"status.formAnalyzed": "Form analyzed",
"status.healthConnected": "Access simulated",
"status.healthSaved": "Recovery data saved",
"status.healthImported": "Health data imported",
"status.adviceRefreshed": "Advice refreshed",
"status.resetDone": "Reset complete",
"status.dbReady": "IndexedDB synced",
"status.storageFallback": "LocalStorage fallback",
"status.poseIdle": "Pose model not loaded",
"status.poseLoading": "Loading pose model",
"status.poseReady": "MoveNet ready",
"status.poseFallback": "Pose model unavailable; using rule fallback",
"status.poseAnalyzing": "Detecting keypoints",
"status.cameraLive": "Live camera analyzing",
"status.cameraStopped": "Camera stopped",
"status.cameraDenied": "Camera unavailable",
"status.agentReady": "Agent ready",
"status.agentRunning": "Agent thinking",
"status.agentDone": "Agent updated",
"status.agentTaskDone": "Task completed",
"status.agentTaskOpened": "Task location opened",
"title.dashboard": "Today",
"title.readiness": "Readiness Score",
"title.nutrition": "Calories and Macros",
"title.coach": "Today's Guidance",
"title.motionSnapshot": "Form Snapshot",
"title.agent": "AI Agent Workspace",
"title.agentConsole": "Agent Console",
"title.agentTasks": "Action Queue",
"title.insights": "Weekly / Monthly Trends",
"title.summary": "Data Summary",
"title.profile": "Goal Setup",
"title.targetSummary": "Target Summary",
"title.foodLog": "Food Log",
"title.mealDetails": "Meal Details",
"title.training": "Training Plan",
"title.formCheck": "Photo / Video Form Check",
"title.formFeedback": "Form Feedback",
"title.health": "Recovery Data",
"title.recoveryJudgement": "Recovery Readout",
"fine.readiness": "Stress load is a recovery proxy for training, not a cortisol test or medical diagnosis.",
"fine.healthkit": "The web MVP cannot directly access HealthKit; real authorization requires native iOS support.",
"fine.form": "This MVP uses file metadata, movement templates, and recovery state; production needs pose keypoint models.",
"fine.formPose": "MoveNet keypoint detection is integrated; low-confidence or unavailable model runs fall back to rule analysis.",
"fine.formLive": "Live camera mode is an on-device preview and does not save every frame. Upload a photo/video and generate feedback to save history.",
"label.consumed": "Consumed",
"label.target": "Target",
"label.metric": "Metric",
"label.name": "Name",
"label.age": "Age",
"label.sex": "Biological sex",
"label.height": "Height cm",
"label.currentWeight": "Current weight kg",
"label.targetWeight": "Target weight kg",
"label.activity": "Activity level",
"label.experience": "Training experience",
"label.trainingDays": "Training days per week",
"label.physique": "Desired outcome",
"label.diet": "Diet preferences",
"label.equipment": "Equipment",
"label.injuries": "Injuries or limits",
"label.mealType": "Meal",
"label.mealDescription": "Meal description",
"label.mealPhoto": "Meal photo",
"label.exercise": "Exercise",
"label.cameraAngle": "Camera angle",
"label.formMedia": "Photo or video",
"label.sleep": "Sleep hours",
"label.hrv": "HRV ms",
"label.rhr": "Resting heart rate",
"label.spo2": "Blood oxygen %",
"label.steps": "Steps",
"label.activeEnergy": "Active energy kcal",
"label.workoutLoad": "Training load 0-100",
"placeholder.diet": "High protein, low dairy, vegetarian...",
"placeholder.injuries": "Knee discomfort, lumbar disc, shoulder impingement...",
"placeholder.meal": "Chicken bowl, avocado salad, latte...",
"placeholder.noPhoto": "No photo selected",
"placeholder.choosePhoto": "Choose photo",
"placeholder.noMedia": "No photo/video uploaded",
"placeholder.chooseMedia": "Upload photo/video",
"placeholder.noMeals": "No meals logged today",
"placeholder.noAnalysis": "Upload a photo or video to generate feedback",
"placeholder.noUploads": "Recent uploads will appear here after you add photos or videos.",
"placeholder.agentPrompt": "Tell the agent: I only have 25 minutes today, want glutes, need dinner ideas...",
"placeholder.noAgentTasks": "Run the agent to generate today's action queue.",
"placeholder.noAgentMessages": "I am your local AI Coach Agent. Run me and I will read goals, meals, training, form, and recovery data before choosing the next step.",
"alert.mealRequired": "Please add a meal description or photo.",
"alert.formRequired": "Please upload a form photo or video first.",
"alert.cameraUnsupported": "This browser does not support direct camera access.",
"alert.cameraFailed": "Unable to start the camera: {message}",
"alert.importFailed": "Import failed: {message}",
"analysis.model": "Analysis engine",
"analysis.poseEngine": "MoveNet Pose",
"analysis.ruleEngine": "Rule fallback",
"analysis.keypointConfidence": "{value}% keypoint confidence",
"analysis.poseMissing": "No stable body keypoints were detected. Try full-body framing, better lighting, and a clearer side or front angle.",
"analysis.poseDetected": "Detected {count} reliable body keypoints with {confidence}% average confidence.",
"analysis.livePreview": "Live preview",
"analysis.liveWaiting": "Waiting for stable body keypoints. Keep the full body in frame and improve lighting.",
"period.week": "Week",
"period.month": "Month",
"range.week": "Last 7 days",
"range.month": "Last 30 days",
"chart.target": "Target",
"chart.total": "Period total",
"chart.average": "Daily avg",
"chart.vsTarget": "Vs target",
"chart.bestDay": "Best day",
"chart.empty": "Not enough history yet. Log meals, workouts, or recovery to build trends.",
"chart.help": "Hover the chart to inspect daily details.",
"chart.tooltip": "{metric}: {value}<br />Meals {meals} · Uploads {uploads}",
"section.recentUploads": "Recent Uploads",
"unit.times": "x",
"unit.points": "pts",
"unit.items": "items",
"metric.calories": "Calories",
"metric.protein": "Protein",
"metric.workouts": "Workouts",
"metric.readiness": "Readiness",
"metric.formScore": "Form score",
"metric.uploads": "Uploads",
"macro.protein": "Protein",
"macro.carbs": "Carbs",
"macro.fat": "Fat",
"summary.dailyCalories": "Daily calories",
"summary.protein": "Protein",
"summary.tdee": "TDEE",
"summary.weightDelta": "Weight delta",
"summary.cutPace": "Aim to lose 0.3-0.6kg per week",
"summary.bulkPace": "Aim to gain 0.1-0.25kg per week",
"summary.recompPace": "Track measurements and strength together",
"summary.profile": "{name}'s current strategy is {mode}. {pace}, with {days} training days per week.",
"summary.macros": "Carbs {carbs}g, fat {fat}g; diet preference: {diet}.",
"health.interpret.good": "You can train as planned. Avoid adding too much volume just because readiness is good.",
"health.interpret.medium": "Training can continue, but keep RPE controlled and protect your sleep window.",
"health.interpret.low": "Today is better for lower intensity or active recovery. If oxygen or heart-rate issues persist, consult a professional.",
"health.interpret.rest": "Rest or light movement is recommended. If you feel unwell, seek medical guidance.",
"coach.priority": "Priority",
"coach.training": "Training",
"coach.form": "Form",
"coach.recovery": "Recovery",
"coach.proteinGap": "Add about {proteinGap}g protein at your next meal; calories are still {calorieText}.",
"coach.calorieLeft": "{value} kcal under target",
"coach.calorieOver": "{value} kcal over target",
"coach.overCalories": "Choose lower-fat, higher-fiber foods tonight. No crash dieting tomorrow, just return to target.",
"coach.stableNutrition": "Nutrition rhythm looks steady. Keep protein first and use vegetables for volume.",
"coach.lowReadiness": "Readiness is low. Keep today's session at RPE 6 with technique work and light cardio.",
"coach.todayPlan": "Today's plan is {focus}. Keep main work at {intensity}, with about {rest} between sets.",
"coach.formIssue": "For {exercise}, address {compensation} first. Use lighter load and film from the {angle} angle next set.",
"coach.recoveryFactor": "{factor} is the main stress signal. Move your sleep window 30 minutes earlier tonight.",
"coach.recoveryStable": "Recovery supports training. Keep a consistent bedtime wind-down to stabilize HRV.",
"auth.authorized": "Authorized",
"auth.unauthorized": "Not authorized",
"meal.count": "{count} logs",
"training.progress": "{done}/{total} done",
"analysis.confidence": "{value}% confidence",
"risk.low": "Low risk",
"risk.medium": "Medium risk",
"risk.high": "High risk",
"readiness.good": "Ready",
"readiness.medium": "Moderate load",
"readiness.high": "High load",
"readiness.rest": "Active recovery",
"mode.cut": "Sustainable cut",
"mode.bulk": "Lean gain",
"mode.recomp": "Body recomposition",
"media.meal": "Meal photo",
"media.formAnalysis": "Form media",
"media.file": "Uploaded file",
"agent.observe": "Observe",
"agent.reason": "Reason",
"agent.act": "Act",
"agent.priority.high": "High priority",
"agent.priority.medium": "Medium priority",
"agent.priority.low": "Low priority",
"agent.status.open": "Open",
"agent.status.active": "Active",
"agent.status.done": "Done",
"agent.evidence": "Evidence",
},
};
const phraseTranslations = {
en: {
"女性": "Female",
"男性": "Male",
"久坐": "Sedentary",
"轻度活动": "Lightly active",
"中等活动": "Moderately active",
"高活动": "Highly active",
"新手": "Beginner",
"有基础": "Intermediate",
"进阶": "Advanced",
"减脂线条": "Fat loss and definition",
"增肌塑形": "Muscle gain and shape",
"体态改善": "Posture improvement",
"力量提升": "Strength gain",
"马甲线核心": "Defined core",
"翘臀下肢": "Glutes and lower body",
"居家": "Home",
"健身房": "Gym",
"徒手/弹力带": "Bodyweight / bands",
"早餐": "Breakfast",
"午餐": "Lunch",
"晚餐": "Dinner",
"加餐": "Snack",
"侧面": "side",
"正面": "front",
"45 度": "45-degree",
"鸡胸肉糙米饭": "Chicken and brown rice bowl",
"牛肉藜麦碗": "Beef quinoa bowl",
"三文鱼沙拉": "Salmon salad",
"燕麦酸奶水果": "Oats, yogurt, and fruit",
"拿铁与点心": "Latte and pastry",
"蛋白奶昔": "Protein shake",
"混合餐盘": "Mixed plate",
"手动餐食": "Manual meal",
"高蛋白、少糖": "High protein, low sugar",
"稳态减脂": "Sustainable cut",
"轻盈增肌": "Lean gain",
"体态重组": "Body recomposition",
"深蹲": "Squat",
"硬拉": "Deadlift",
"俯卧撑": "Push-up",
"弓步": "Lunge",
"平板支撑": "Plank",
"卧推": "Bench press",
"划船": "Row",
"肩推": "Shoulder press",
"全身基础": "Full-body basics",
"臀腿核心": "Glutes, legs, and core",
"上肢体态": "Upper body posture",
"全身代谢": "Full-body conditioning",
"全身力量": "Full-body strength",
"低冲击有氧": "Low-impact cardio",
"上肢力量": "Upper-body strength",
"代谢循环": "Conditioning circuit",
"下肢臀腿": "Lower-body glutes",
"上肢推拉": "Upper push / pull",
"全身容量": "Full-body volume",
"推": "Push",
"拉": "Pull",
"腿": "Legs",
"上肢": "Upper body",
"下肢": "Lower body",
"核心体态": "Core and posture",
"主动恢复": "Active recovery",
"轻活动": "Light activity",
"正常": "Normal",
"降强度": "Reduced intensity",
"恢复": "Recovery",
"指标稳定": "Metrics stable",
"恢复良好": "Ready",
"压力适中": "Moderate load",
"压力偏高": "High load",
"建议主动恢复": "Active recovery recommended",
"睡眠明显不足": "Sleep is clearly low",
"睡眠略少": "Sleep is slightly low",
"HRV 低于理想区间": "HRV is below target range",
"HRV 偏低": "HRV is low",
"静息心率偏高": "Resting heart rate is high",
"静息心率略高": "Resting heart rate is slightly high",
"血氧偏低": "Blood oxygen is low",
"训练负荷偏高": "Training load is high",
"训练负荷较高": "Training load is elevated",
"日常活动偏少": "Daily movement is low",
"膝内扣": "knee valgus",
"腰椎代偿": "lumbar compensation",
"踝活动度不足": "limited ankle mobility",
"腘绳肌张力不足": "hamstring tension deficit",
"背阔肌参与不足": "limited lat engagement",
"肩前侧压力": "anterior shoulder stress",
"核心抗伸展不足": "limited anti-extension control",
"臀中肌不足": "weak glute medius",
"足弓塌陷": "arch collapse",
"髋稳定不足": "limited hip stability",
"髋屈肌抢力": "hip flexor dominance",
"肩颈紧张": "neck and shoulder tension",
"前三角代偿": "front-delt compensation",
"肩胛控制不足": "limited scapular control",
"左右发力不均": "left-right force asymmetry",
"上斜方肌代偿": "upper-trap compensation",
"核心稳定不足": "limited core stability",
"上斜方肌紧张": "upper-trap tension",
"肩胛上旋不足": "limited scapular upward rotation",
"今天": "Today",
"视频": "Video",
"照片": "Photo",
"未填写": "Not set",
"髋膝同步下降": "hip and knee descend together",
"底部核心张力": "core tension at the bottom",
"膝盖轨迹": "knee tracking",
"脊柱中立": "neutral spine",
"杠铃路径": "bar path",
"髋主导": "hip dominance",
"肩胛控制": "scapular control",
"核心直线": "straight trunk line",
"肘部角度": "elbow angle",
"骨盆位置": "pelvis position",
"肩肘堆叠": "shoulder-elbow stack",
"呼吸控制": "breathing control",
"下蹲末端膝盖略内扣": "knees drift inward near the bottom",
"底部骨盆控制需要更稳定": "pelvic control needs more stability at the bottom",
"脚跟压力分布偏前": "heel pressure shifts too far forward",
"启动时髋部略先抬": "hips rise slightly early at the start",
"锁定时肋骨外翻": "ribs flare at lockout",
"杠铃离身体偏远": "bar path is too far from the body",
"后半程髋部下沉": "hips drop in the second half",
"肘部外展角度偏大": "elbows flare too wide",
"肩胛前伸不充分": "scapular protraction is incomplete",
"前腿膝盖内移": "front knee drifts inward",
"后侧髋屈肌紧张": "rear hip flexor looks tight",
"骨盆轻微旋转": "pelvis rotates slightly",
"后 20 秒骨盆前倾": "pelvis tilts forward in the final 20 seconds",
"颈部略过度伸展": "neck is slightly overextended",
"腹压维持不足": "bracing is not maintained",
"底部肩胛稳定不足": "scapular stability is limited at the bottom",
"手腕略后折": "wrists extend slightly backward",
"推起时右侧稍慢": "right side presses slightly slower",
"末端耸肩": "shoulders shrug at the end range",
"下放速度偏快": "eccentric phase is too fast",
"躯干轻微晃动": "torso sways slightly",
"推到顶端肋骨外翻": "ribs flare at the top",
"左侧上推路径偏外": "left press path drifts outward",
"核心稳定略不足": "core stability is slightly limited",
"降低 10% 负重,做 3 组暂停深蹲": "Reduce load by 10% and do 3 sets of pause squats",
"热身加入踝背屈和臀中肌激活": "Add ankle dorsiflexion and glute med activation to warm-up",
"每次下降保持膝盖指向第二脚趾": "Keep knees tracking toward the second toe on every descent",
"先练壶铃硬拉找髋铰链": "Use kettlebell deadlifts to groove the hip hinge first",
"拉起前把腋下夹紧": "Tighten the armpits before pulling",
"每组前 2 次使用 3 秒离心": "Use a 3-second eccentric for the first 2 reps of each set",
"改为上斜俯卧撑保持躯干直线": "Switch to incline push-ups and keep the trunk straight",
"肘部维持 30-45 度": "Keep elbows at 30-45 degrees",
"每组结束加 8 次肩胛俯卧撑": "Add 8 scapular push-ups after each set",
"先做分腿蹲静止 2 秒": "Start with split squats and hold 2 seconds",
"脚掌三点支撑": "Keep tripod foot pressure",
"加入侧向弹力带走 2 组": "Add 2 sets of lateral band walks",
"缩短到 25 秒高质量组": "Shorten to high-quality 25-second sets",
"呼气时收肋骨": "Exhale and bring the ribs down",
"每次保持头颈与躯干一条线": "Keep head, neck, and torso aligned",
"空杠热身加入停顿卧推": "Add pause bench reps with the empty bar",
"手腕保持中立": "Keep wrists neutral",
"重量下降 5% 做左右速度一致": "Drop load by 5% and match left-right speed",
"先下沉肩胛再拉": "Depress the shoulder blades before pulling",
"离心 3 秒": "Use a 3-second eccentric",
"胸托划船替代一周": "Use chest-supported rows for one week",
"改为半跪姿单臂推举": "Switch to half-kneeling single-arm press",
"收肋骨后再发力": "Set the ribs down before pressing",
"每组保留 2 次余力": "Keep 2 reps in reserve",
"杯式深蹲": "Goblet squat",
"坐姿划船": "Seated row",
"弹力带划船": "Band row",
"上斜俯卧撑": "Incline push-up",
"罗马尼亚硬拉": "Romanian deadlift",
"臀桥": "Glute bridge",
"分腿蹲": "Split squat",
"死虫": "Dead bug",
"高位下拉": "Lat pulldown",
"弹力带下拉": "Band pulldown",
"哑铃肩推": "Dumbbell shoulder press",
"面拉": "Face pull",
"深蹲到推举": "Squat to press",
"登山跑": "Mountain climber",
"杠铃深蹲": "Barbell squat",
"壶铃硬拉": "Kettlebell deadlift",
"坡度快走": "Incline walk",
"髋屈肌拉伸": "Hip flexor stretch",
"呼吸训练": "Breathing drill",
"壶铃摆动": "Kettlebell swing",
"反向弓步": "Reverse lunge",
"农夫走": "Farmer carry",
"臀推": "Hip thrust",
"单腿臀桥": "Single-leg glute bridge",
"腿举": "Leg press",
"保加利亚分腿蹲": "Bulgarian split squat",
"小腿提踵": "Calf raise",
"哑铃卧推": "Dumbbell bench press",
"胸托划船": "Chest-supported row",
"前蹲": "Front squat",
"硬拉变式": "Deadlift variation",
"核心抗旋转": "Anti-rotation core",
"引体或下拉": "Pull-up or pulldown",
"杠铃划船": "Barbell row",
"二头弯举": "Biceps curl",
"上斜卧推": "Incline bench press",
"侧平举": "Lateral raise",
"腿弯举": "Leg curl",
"鸟狗": "Bird dog",
"绳索下压": "Cable pressdown",
"Zone 2 快走": "Zone 2 brisk walk",
"髋/胸椎活动": "Hip / thoracic mobility",
"睡前放松": "Bedtime downshift",
"核心紧": "core tight",
"鼻吸可说话": "nasal breathing, able to talk",
"不追求疼痛": "do not chase pain",
"慢呼吸": "slow breathing",
"控制呼吸": "control breathing",
"肩胛稳定": "scapular stability",
"低冲击可替换": "low-impact option available",
"保持骨盆": "keep pelvis steady",
"停顿": "pause",
"肩胛": "scapula",
"稳定": "stability",
"控制": "controlled",
"慢速": "slow tempo",
"肩线": "shoulder line",
"骨盆稳定": "pelvic stability",
"腘绳肌": "hamstrings",
"放松": "relax",
"鼻吸慢呼": "nasal inhale, slow exhale",
"肋骨下沉": "ribs down",
"关键点置信度偏低": "Keypoint confidence is low",
"重新拍摄时保持全身入镜并提高光线": "Retake with full-body framing and better lighting",
"左右膝角差异偏大": "Left-right knee angle difference is high",
"降低速度,先做左右对称的控制组": "Slow down and start with controlled symmetrical reps",
"肩线左右高度不一致": "Shoulder height is uneven",
"下一组先做肩胛定位,再开始主动作": "Set the shoulder blades before starting the next set",
"髋部左右高度不一致": "Hip height is uneven",
"加入单侧稳定练习,保持骨盆水平": "Add unilateral stability work and keep the pelvis level",
"膝盖与脚踝轨迹偏差较大": "Knee and ankle tracking differ too much",
"保持脚掌三点支撑,膝盖跟随脚尖方向": "Keep tripod foot pressure and track knees with toes",
"躯干前倾角度偏大": "Torso lean is high",
"髋主导不足": "limited hip dominance",
"减少负重,练习暂停下蹲和髋踝活动度": "Reduce load and practice pause squats plus hip/ankle mobility",
"核心直线需要更稳定": "Core line needs more stability",
"缩短每组时间,保持肋骨下沉和骨盆中立": "Shorten each set and keep ribs down with a neutral pelvis",
"左右肘角差异偏大": "Left-right elbow angle difference is high",
"降低重量,保持左右速度一致": "Reduce load and keep left-right speed consistent",
"关键点轨迹整体稳定": "Keypoint path is generally stable",
"保持当前重量,下一组继续用同角度复拍": "Keep the current load and film the next set from the same angle",
},
};
const foodProfiles = [
{
name: "鸡胸肉糙米饭",
keywords: ["鸡胸", "鸡肉", "chicken", "糙米", "健身餐"],
calories: 520,
proteinG: 46,
carbsG: 55,
fatG: 12,
},
{
name: "牛肉藜麦碗",
keywords: ["牛肉", "beef", "藜麦", "牛排"],
calories: 650,
proteinG: 48,
carbsG: 58,
fatG: 22,
},
{
name: "三文鱼沙拉",
keywords: ["三文鱼", "salmon", "沙拉", "salad"],
calories: 470,
proteinG: 34,
carbsG: 22,
fatG: 27,
},
{
name: "燕麦酸奶水果",
keywords: ["燕麦", "酸奶", "oat", "yogurt", "水果"],
calories: 390,
proteinG: 24,
carbsG: 54,
fatG: 9,
},
{
name: "拿铁与点心",
keywords: ["拿铁", "latte", "咖啡", "蛋糕", "饼干", "甜点"],
calories: 360,
proteinG: 9,
carbsG: 42,
fatG: 16,
},
{
name: "蛋白奶昔",
keywords: ["奶昔", "蛋白粉", "protein", "shake"],
calories: 260,
proteinG: 32,
carbsG: 18,
fatG: 6,
},
];
const exerciseProfiles = {
squat: {
label: "深蹲",
bestAngle: "side",
focus: "下肢力量",
findings: ["髋膝同步下降", "底部核心张力", "膝盖轨迹"],
issues: ["下蹲末端膝盖略内扣", "底部骨盆控制需要更稳定", "脚跟压力分布偏前"],
compensations: ["膝内扣", "腰椎代偿", "踝活动度不足"],
corrections: ["降低 10% 负重,做 3 组暂停深蹲", "热身加入踝背屈和臀中肌激活", "每次下降保持膝盖指向第二脚趾"],
},
deadlift: {
label: "硬拉",
bestAngle: "side",
focus: "髋铰链",
findings: ["脊柱中立", "杠铃路径", "髋主导"],
issues: ["启动时髋部略先抬", "锁定时肋骨外翻", "杠铃离身体偏远"],
compensations: ["腰椎代偿", "腘绳肌张力不足", "背阔肌参与不足"],
corrections: ["先练壶铃硬拉找髋铰链", "拉起前把腋下夹紧", "每组前 2 次使用 3 秒离心"],
},
pushup: {
label: "俯卧撑",
bestAngle: "side",
focus: "上肢推",
findings: ["肩胛控制", "核心直线", "肘部角度"],
issues: ["后半程髋部下沉", "肘部外展角度偏大", "肩胛前伸不充分"],
compensations: ["腰椎代偿", "肩前侧压力", "核心抗伸展不足"],
corrections: ["改为上斜俯卧撑保持躯干直线", "肘部维持 30-45 度", "每组结束加 8 次肩胛俯卧撑"],
},
lunge: {
label: "弓步",
bestAngle: "front",
focus: "单腿稳定",
findings: ["左右对称", "膝盖轨迹", "骨盆水平"],
issues: ["前腿膝盖内移", "后侧髋屈肌紧张", "骨盆轻微旋转"],
compensations: ["臀中肌不足", "足弓塌陷", "髋稳定不足"],
corrections: ["先做分腿蹲静止 2 秒", "脚掌三点支撑", "加入侧向弹力带走 2 组"],
},
plank: {
label: "平板支撑",
bestAngle: "side",
focus: "核心稳定",
findings: ["骨盆位置", "肩肘堆叠", "呼吸控制"],
issues: ["后 20 秒骨盆前倾", "颈部略过度伸展", "腹压维持不足"],
compensations: ["腰椎代偿", "髋屈肌抢力", "肩颈紧张"],
corrections: ["缩短到 25 秒高质量组", "呼气时收肋骨", "每次保持头颈与躯干一条线"],
},
bench: {
label: "卧推",
bestAngle: "diagonal",
focus: "胸肩三头",
findings: ["肩胛稳定", "杠铃路径", "手腕堆叠"],
issues: ["底部肩胛稳定不足", "手腕略后折", "推起时右侧稍慢"],
compensations: ["前三角代偿", "肩胛控制不足", "左右发力不均"],
corrections: ["空杠热身加入停顿卧推", "手腕保持中立", "重量下降 5% 做左右速度一致"],
},
row: {
label: "划船",
bestAngle: "diagonal",
focus: "背部拉",
findings: ["肩胛后缩", "躯干稳定", "肘部路径"],
issues: ["末端耸肩", "下放速度偏快", "躯干轻微晃动"],
compensations: ["上斜方肌代偿", "核心稳定不足", "背阔肌参与不足"],
corrections: ["先下沉肩胛再拉", "离心 3 秒", "胸托划船替代一周"],
},
press: {
label: "肩推",
bestAngle: "front",
focus: "垂直推",
findings: ["肋骨位置", "肩胛上旋", "左右对称"],
issues: ["推到顶端肋骨外翻", "左侧上推路径偏外", "核心稳定略不足"],
compensations: ["腰椎代偿", "上斜方肌紧张", "肩胛上旋不足"],
corrections: ["改为半跪姿单臂推举", "收肋骨后再发力", "每组保留 2 次余力"],
},
};
const insightMetrics = {
calories: { labelKey: "metric.calories", unit: "kcal", color: "#F4C95D", targetKey: "calories", kind: "bar" },
protein: { labelKey: "metric.protein", unit: "g", color: "#3F72D8", targetKey: "proteinG", kind: "bar" },
workouts: { labelKey: "metric.workouts", unit: "unit.times", color: "#2F8A67", kind: "bar" },
readiness: { labelKey: "metric.readiness", unit: "unit.points", color: "#6EE7B7", kind: "line" },
formScore: { labelKey: "metric.formScore", unit: "unit.points", color: "#E56B4F", kind: "line" },
uploads: { labelKey: "metric.uploads", unit: "unit.times", color: "#A78BFA", kind: "bar" },
};
document.addEventListener("DOMContentLoaded", async () => {
pulseStatus("status.loadingDb");
state = await loadState();
ensureComputedState();
bindEvents();
populateForms();
render();
});
function bindEvents() {
$("#profileForm").addEventListener("submit", handleProfileSubmit);
$("#mealForm").addEventListener("submit", handleMealSubmit);
$("#mealPhotoInput").addEventListener("change", handleMealPhotoChange);
$("#mealList").addEventListener("change", handleMealEdit);
$("#mealList").addEventListener("click", handleMealDelete);
$("#formMediaInput").addEventListener("change", handleFormMediaChange);
[$("#formFileSurface"), $("#formPreview")].filter(Boolean).forEach((target) => {
target.addEventListener("dragover", handleFormMediaDragOver);
target.addEventListener("dragleave", handleFormMediaDragLeave);
target.addEventListener("drop", handleFormMediaDrop);
});
$("#formAnalysisForm").addEventListener("submit", handleFormAnalysisSubmit);
$("#loadPoseModel").addEventListener("click", () => loadPoseDetector());
$("#startMotionCamera").addEventListener("click", startLiveMotionCamera);
$("#stopMotionCamera").addEventListener("click", stopLiveMotionCamera);
$("#runAgent").addEventListener("click", () => runAgentCycle());
$("#rerunAgent").addEventListener("click", () => runAgentCycle());
$("#agentForm").addEventListener("submit", handleAgentSubmit);
$("#agentTaskList").addEventListener("click", handleAgentTaskAction);
$("#trainingGrid").addEventListener("click", handleTrainingToggle);
$("#healthForm").addEventListener("submit", handleHealthSubmit);
$("#connectHealth").addEventListener("click", handleHealthConnect);
$("#healthImportInput").addEventListener("change", handleHealthImport);
$("#languageToggle").addEventListener("click", handleLanguageToggle);
$("#insightPeriodControls").addEventListener("click", handleInsightPeriodChange);
$("#insightMetricInput").addEventListener("change", handleInsightMetricChange);
$("#trendCanvas").addEventListener("mousemove", handleChartPointerMove);
$("#trendCanvas").addEventListener("mouseleave", hideChartTooltip);
$("#trendCanvas").addEventListener("click", handleChartPointerMove);
window.addEventListener("resize", () => renderInsights());
$("#refreshCoach").addEventListener("click", () => {
pulseStatus("status.adviceRefreshed");
renderDashboard();
});
$("#resetDemo").addEventListener("click", async () => {
const confirmed = window.confirm(t("confirm.reset"));
if (!confirmed) return;
localStorage.removeItem(STORAGE_KEY);
await clearDatabase();
state = clone(defaultState);
ensureComputedState(true);
pendingMealImage = "";
pendingMealImageName = "";
pendingMealImageSize = 0;
pendingFormMedia = null;
pendingFormMediaUrl = "";
pendingFormMediaPreview = "";
populateForms();
render();
pulseStatus("status.resetDone");
});
}
async function loadState() {
try {
const dbState = await readStateFromDatabase();
if (dbState) {
dbReady = true;
return mergeState(clone(defaultState), dbState);
}
} catch (error) {
console.warn("Failed to load IndexedDB state", error);
dbReady = false;
}
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
dbReady = "indexedDB" in window;
return clone(defaultState);
}