dsh-biocore 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
package/ui/panel.js CHANGED
@@ -1,28 +1,98 @@
1
- /** BioCore instrument panel: vanilla JS, no external dependencies.
1
+ /** BioCore instrument panel runtime — vanilla JS, no dependencies.
2
2
  *
3
- * Reads GET /api/biocore/state?session=<id> every 2 seconds. The /series
4
- * endpoint is optional: if it is absent we draw from the state extras
5
- * (history / memory_recent) and never block rendering on it.
3
+ * Data contract (unchanged): GET /api/biocore/state?session=<id> is the single
4
+ * polling source; /series /stats /trace /persona /settings /switch are additive
5
+ * reads plus the two write paths the panel is allowed to touch.
6
+ *
7
+ * Pages are classified once, hard:
8
+ * dashboard data only memory data only relation data only
9
+ * settings controls only tools diagnostics only
6
10
  */
7
11
  (function () {
8
12
  'use strict'
9
13
 
14
+ var QUERY = new URLSearchParams(window.location.search)
15
+ var SESSION = (QUERY.get('session') || '').trim()
16
+ var INITIAL_TAB = (QUERY.get('tab') || 'dashboard').trim()
17
+ var THEME = (QUERY.get('theme') || '').trim()
18
+
10
19
  var POLL_MS = 5000
11
- var SESSION = (new URLSearchParams(window.location.search).get('session') || '').trim()
20
+ var STATS_MS = 30000
21
+ var PAGE_START = Date.now()
22
+
12
23
  var SWITCHES = [
13
- { key: 'safety_enabled', label: '安全闸', desc: '打开后可能拦截/改写回复' },
14
- { key: 'boundary_enabled', label: '底线', desc: '越界时先守住边界' },
15
- { key: 'crisis_safety_enabled', label: '危机兜底', desc: '危机信号优先安全' },
16
- { key: 'proactive_send', label: '主动回复', desc: '允许私聊主动发消息' },
24
+ { key: 'core_enabled', label: '核心', desc: '关掉后整条核心链不运行', group: '总开关' },
25
+ { key: 'safety_enabled', label: '安全闸', desc: '可能拦截或改写回复', group: '安全' },
26
+ { key: 'boundary_enabled', label: '底线', desc: '越界时先守边界', group: '安全' },
27
+ { key: 'crisis_safety_enabled', label: '危机兜底', desc: '危机信号优先安全', group: '安全' },
28
+ { key: 'proactive_send', label: '主动回复', desc: '允许私聊主动发消息', group: '主动' },
29
+ { key: 'mood_recall', label: '情绪匹配召回', desc: '召回偏向当前心情', group: '学习' },
30
+ { key: 'affect_encoding', label: '编码带情绪', desc: '写记忆时带情绪强度', group: '学习' },
31
+ { key: 'memory_feedback', label: '回忆回灌情绪', desc: '召回的记忆小幅回灌心情', group: '学习' },
32
+ { key: 'preference_drift', label: '主题偏好漂移', desc: '重复主题缓慢调整偏好', group: '学习' },
17
33
  ]
34
+
35
+ var EMPTY_COPY = {
36
+ no_state_yet: ['这个会话还没聊过', '发一条消息后就有状态'],
37
+ request_failed: ['连不上宿主', '确认 DSH 与插件已加载,面板每 5 秒自动重试'],
38
+ bad_state: ['状态文件坏了', '删掉该会话的状态文件后重试'],
39
+ }
40
+ var STAT_WORDS = {
41
+ tension: ['松弛', '适中', '略紧'],
42
+ warmth: ['偏冷', '适中', '偏暖'],
43
+ heaviness: ['轻', '适中', '偏沉'],
44
+ clarity: ['模糊', '适中', '清醒'],
45
+ }
46
+ var STAT_NOTES = {
47
+ tension: ['松下来了', '还压得住', '明显绷着'],
48
+ warmth: ['有点冷,想靠近', '温度刚好', '心里暖,愿意靠近'],
49
+ heaviness: ['轻一些,能提劲', '有点分量,还扛得住', '沉下去了,先放一放'],
50
+ clarity: ['有点糊,慢一点', '还算清楚,能接话', '想得清楚,状态在线'],
51
+ }
52
+ var KNOWN_SINCE_KEY = 'biocore-known-since-v1'
53
+ var MODEL_DRAFT_KEY = 'biocore-panel-model-draft-v1'
54
+ var TRACE_LIMIT = 5
55
+ var LONG_PRESS_MS = 400
56
+ var MOVE_CANCEL_PX = 8
57
+
18
58
  var lastSuccess = 0
19
59
  var lastData = null
20
60
  var seriesPoints = null
21
- var localPoints = []
22
61
  var switchBusy = false
62
+ var personaText = ''
63
+ var personaDirty = false
64
+ var toastTimer = null
65
+ var bootDone = false
23
66
 
24
67
  function $(id) { return document.getElementById(id) }
25
68
 
69
+ /** Theme: the host may pin <html data-theme>; ?theme= wins for previews,
70
+ * otherwise prefers-color-scheme decides inside CSS. */
71
+ function applyTheme() {
72
+ if (THEME === 'light' || THEME === 'dark') {
73
+ document.documentElement.setAttribute('data-theme', THEME)
74
+ }
75
+ var scheme = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null
76
+ if (!scheme || !scheme.addEventListener) return
77
+ scheme.addEventListener('change', function () {
78
+ if (THEME === 'light' || THEME === 'dark') return
79
+ renderChart(lastData)
80
+ })
81
+ }
82
+
83
+ function chartPalette() {
84
+ var styles = window.getComputedStyle(document.documentElement)
85
+ var read = function (name, fallback) {
86
+ var value = styles.getPropertyValue(name)
87
+ return value && value.trim() ? value.trim() : fallback
88
+ }
89
+ return {
90
+ accent: read('--accent', '#6d5dfc'),
91
+ ink: read('--ink', '#333333'),
92
+ grid: read('--shadow-dark', '#b8bcc2'),
93
+ }
94
+ }
95
+
26
96
  function fmt(value, digits) {
27
97
  var number = Number(value)
28
98
  if (!Number.isFinite(number)) return '–'
@@ -34,6 +104,16 @@
34
104
  if (el) el.textContent = value
35
105
  }
36
106
 
107
+ function clamp01(value) {
108
+ var number = Number(value)
109
+ if (!Number.isFinite(number)) return 0
110
+ return Math.max(0, Math.min(1, number))
111
+ }
112
+
113
+ function pct(value) {
114
+ return Math.round(clamp01(value) * 100) + '%'
115
+ }
116
+
37
117
  /** Presentation helper: pick a one-line label out of string/object payloads. */
38
118
  function scalarLabel(value, fallback) {
39
119
  if (typeof value === 'string') return value.trim() || fallback
@@ -63,15 +143,15 @@
63
143
 
64
144
  function updateStatusClock() {
65
145
  if (!lastSuccess) return
146
+ if (lastData && lastData.available === false) return
66
147
  var seconds = Math.max(0, Math.round((Date.now() - lastSuccess) / 1000))
67
- setStatus(true, '已连接 · ' + seconds + ' 秒前')
148
+ setStatus(true, '已连接 ' + seconds + ' 秒前')
68
149
  }
69
150
 
70
151
  function setBar(id, ratio) {
71
152
  var el = $(id)
72
153
  if (!el) return
73
- var value = Math.max(0, Math.min(1, Number(ratio) || 0))
74
- el.style.width = Math.round(value * 100) + '%'
154
+ el.style.width = pct(ratio)
75
155
  }
76
156
 
77
157
  function formatDelta(previous, current) {
@@ -102,10 +182,13 @@
102
182
  el.classList.toggle('down', delta.charAt(0) === '-')
103
183
  }
104
184
 
105
- var EMPTY_COPY = {
106
- no_state_yet: ['这个会话还没聊过', '发一条消息后就有状态'],
107
- request_failed: ['连不上宿主', '确认 3081 上的 DSH 与插件已加载'],
108
- bad_state: ['状态文件坏了', '删掉该会话的状态文件后重试'],
185
+ function paintArrow(id, delta) {
186
+ var el = $(id)
187
+ if (!el) return
188
+ var direction = arrowFor(delta)
189
+ el.textContent = direction
190
+ el.classList.toggle('up', direction === '↑')
191
+ el.classList.toggle('down', direction === '↓')
109
192
  }
110
193
 
111
194
  function renderEmpty(kind) {
@@ -122,34 +205,8 @@
122
205
 
123
206
  function shortSession() {
124
207
  if (!SESSION) return '-'
125
- return SESSION.length > 10 ? SESSION.slice(0, 8) : SESSION
126
- }
127
-
128
- function isPlausibleMs(ts) {
129
- return Number.isFinite(ts) && ts > 1000000000000
130
- }
131
-
132
- function timeLabel(ts) {
133
- if (!isPlausibleMs(ts)) return '--:--'
134
- var date = new Date(ts)
135
- var hh = String(date.getHours()).padStart(2, '0')
136
- var mm = String(date.getMinutes()).padStart(2, '0')
137
- return hh + ':' + mm
138
- }
139
-
140
- var STAT_WORDS = {
141
- tension: ['松弛', '适中', '略紧'],
142
- warmth: ['偏冷', '适中', '偏暖'],
143
- heaviness: ['轻', '适中', '偏沉'],
144
- clarity: ['模糊', '适中', '清醒'],
208
+ return SESSION.length > 12 ? SESSION.slice(0, 10) + '…' : SESSION
145
209
  }
146
- var STAT_NOTES = {
147
- tension: ['松下来了,不紧绷', '还压得住,但不太放松', '明显绷着,先缓一缓'],
148
- warmth: ['有点冷,想要靠近', '温度刚好,慢慢热', '心里暖,愿意靠近'],
149
- heaviness: ['轻一些,能提起劲', '有点分量,还扛得住', '沉下去了,先放一放'],
150
- clarity: ['有点糊,慢一点', '还算清楚,能接话', '想得清楚,状态在线'],
151
- }
152
- var KNOWN_SINCE_KEY = 'biocore-known-since-v1'
153
210
 
154
211
  function statLevel(value) {
155
212
  var number = Number(value)
@@ -159,26 +216,12 @@
159
216
  return 2
160
217
  }
161
218
 
162
- function paintStatDelta(channel, data, available) {
163
- var delta = available ? historyDelta(data, channel) : '—'
164
- paintDelta('delta-' + channel, delta)
165
- var arrow = $('arrow-' + channel)
166
- if (!arrow) return
167
- var direction = arrowFor(delta)
168
- arrow.textContent = direction
169
- arrow.classList.toggle('up', direction === '↑')
170
- arrow.classList.toggle('down', direction === '↓')
171
- }
172
-
173
- function setStatBar(channel, value) {
174
- var ratio = Number.isFinite(Number(value)) ? Math.max(0, Math.min(1, Number(value))) : 0
175
- var fill = $('bar-' + channel)
176
- if (fill) fill.style.width = Math.round(ratio * 100) + '%'
177
- var over = $('over-' + channel)
178
- if (over) {
179
- over.style.left = '65%'
180
- over.style.width = ratio > 0.65 ? Math.round((ratio - 0.65) * 100) + '%' : '0%'
181
- }
219
+ function moodWord(value) {
220
+ var number = Number(value)
221
+ if (!Number.isFinite(number)) return '—'
222
+ if (number > 0.25) return '偏高'
223
+ if (number < -0.25) return '偏低'
224
+ return '平稳'
182
225
  }
183
226
 
184
227
  function knownDays() {
@@ -201,16 +244,6 @@
201
244
  return '焦虑型'
202
245
  }
203
246
 
204
- function closenessNote(value) {
205
- var level = statLevel(value)
206
- return ['还不太熟,慢慢来', '挺自然,不装', '很亲近,想粘着'][level]
207
- }
208
-
209
- function trustNote(value) {
210
- var level = statLevel(value)
211
- return ['还在观察你', '慢慢信你', '信你,能交底'][level]
212
- }
213
-
214
247
  function memoryKind(text) {
215
248
  var value = String(text || '')
216
249
  if (/(喜欢|爱|讨厌|偏好|习惯|最怕|想要|想吃)/.test(value)) return '偏好'
@@ -228,164 +261,192 @@
228
261
  return Math.round(seconds / 86400) + ' 天前'
229
262
  }
230
263
 
231
- function renderNumbers(data) {
232
- var available = data && data.available !== false
233
- var intero = (available && data.intero) || {}
234
- setText('num-v', available ? fmt(data.v) : '–')
235
- setText('num-tension', available ? fmt(intero.tension) : '–')
236
- setText('num-warmth', available ? fmt(intero.warmth) : '–')
237
- setText('num-heaviness', available ? fmt(intero.heaviness) : '–')
238
- setText('num-clarity', available ? fmt(intero.clarity) : '–')
239
- setBar('bar-v', available ? (Number(data.v) + 1) / 2 : 0)
240
-
241
- var channels = ['tension', 'warmth', 'heaviness', 'clarity']
242
- for (var channelIndex = 0; channelIndex < channels.length; channelIndex += 1) {
243
- var channel = channels[channelIndex]
244
- var rawValue = available ? intero[channel] : null
245
- setText('status-' + channel, available ? STAT_WORDS[channel][statLevel(rawValue)] : '—')
246
- setText('note-' + channel, available ? STAT_NOTES[channel][statLevel(rawValue)] : '—')
247
- setStatBar(channel, rawValue)
248
- paintStatDelta(channel, data, available)
249
- }
250
-
251
- // 情绪卡右侧「张力」复用同一份数据(只换 DOM 节点,不改数据)。
252
- var emotionTension = available ? intero.tension : null
253
- setText('num-emotion-tension', available ? fmt(emotionTension) : '–')
254
- setText('note-emotion-tension', available ? STAT_NOTES.tension[statLevel(emotionTension)] : '—')
255
- setBar('bar-emotion-tension', emotionTension)
256
- paintDelta('delta-emotion-tension', available ? historyDelta(data, 'tension') : '—')
257
- var emotionArrow = $('arrow-emotion-tension')
258
- if (emotionArrow) {
259
- var emotionDirection = arrowFor(available ? historyDelta(data, 'tension') : '—')
260
- emotionArrow.textContent = emotionDirection
261
- emotionArrow.classList.toggle('up', emotionDirection === '↑')
262
- emotionArrow.classList.toggle('down', emotionDirection === '↓')
263
- }
264
- setText('moodAside', available ? emotionMoodWord(data.v) : '—')
265
- setText('moodNote', available ? emotionMoodWord(data.v) : '—')
266
-
267
- var tone = available ? scalarLabel(data.tone, '') : ''
268
- setText('word-tone', tone || (available ? '正常' : '–'))
269
- setText('word-relation', available ? (scalarLabel(data.relation, '') || '–') : '–')
270
- setText('word-autonomy', available ? (scalarLabel(data.autonomy, '') || '–') : '–')
271
- if (available) {
272
- setText('motivation', data.motivation || '安静待着,等你开口')
273
- }
274
- var fields = ['v', 'tension', 'warmth', 'heaviness', 'clarity']
275
- for (var index = 0; index < fields.length; index += 1) {
276
- var delta = available ? historyDelta(data, fields[index]) : '—'
277
- paintDelta('delta-' + fields[index], delta)
278
- }
279
- var arrow = $('arrow-v')
280
- if (arrow) {
281
- var direction = arrowFor(available ? historyDelta(data, 'v') : '—')
282
- arrow.textContent = direction
283
- arrow.classList.toggle('up', direction === '↑')
284
- arrow.classList.toggle('down', direction === '↓')
285
- }
264
+ function startOfToday() {
265
+ var dayStart = new Date()
266
+ dayStart.setHours(0, 0, 0, 0)
267
+ return dayStart.getTime()
286
268
  }
287
269
 
288
- function emotionMoodWord(value) {
289
- var number = Number(value)
290
- if (!Number.isFinite(number)) return '—'
291
- if (number > 0.25) return '偏高'
292
- if (number < -0.25) return '偏低'
293
- return '平稳'
294
- }
270
+ /* ------------------------------------------------------------ 仪表盘 --- */
295
271
 
296
272
  function renderMetrics(data) {
297
- var box = $('metrics')
298
- if (!box) return
299
- box.textContent = ''
300
273
  var available = data && data.available !== false
301
- var rel = (available && data.relationship_metrics) || {}
302
- var closeness = Number(rel.closeness)
303
- var trust = Number(rel.trust)
304
- var conflict = Number(rel.conflict)
305
- var safeCloseness = Number.isFinite(closeness) ? closeness : null
306
- var safeTrust = Number.isFinite(trust) ? trust : null
307
- var safeConflict = Number.isFinite(conflict) ? conflict : 0
274
+ var intero = (available && data.intero) || {}
275
+
276
+ setText('moodValue', available ? fmt(data.v) : '—')
277
+ setText('moodWord', available ? moodWord(data.v) : '')
278
+ setBar('moodBar', available ? (Number(data.v) + 1) / 2 : 0)
279
+ var moodDelta = available ? historyDelta(data, 'v') : '—'
280
+ paintDelta('moodDelta', moodDelta)
281
+ paintArrow('moodArrow', moodDelta)
282
+
283
+ setText('tensionValue', available ? fmt(intero.tension) : '—')
284
+ setText('tensionWord', available ? STAT_WORDS.tension[statLevel(intero.tension)] : '')
285
+ setBar('tensionBar', intero.tension)
286
+ var tensionDelta = available ? historyDelta(data, 'tension') : '—'
287
+ paintDelta('tensionDelta', tensionDelta)
288
+ paintArrow('tensionArrow', tensionDelta)
289
+ var over = $('tensionOver')
290
+ if (over) {
291
+ var ratio = clamp01(intero.tension)
292
+ over.style.width = ratio > 0.65 ? Math.round((ratio - 0.65) * 100) + '%' : '0%'
293
+ }
308
294
 
309
- var grid = document.createElement('div')
310
- grid.className = 'relation-grid'
311
- var columns = [
312
- ['亲近', safeCloseness, closenessNote(safeCloseness)],
313
- ['信任', safeTrust, trustNote(safeTrust)],
295
+ var channels = ['warmth', 'heaviness', 'clarity']
296
+ for (var i = 0; i < channels.length; i += 1) {
297
+ var channel = channels[i]
298
+ var raw = available ? intero[channel] : null
299
+ setText(channel + 'Value', available ? fmt(raw) : '—')
300
+ setText(channel + 'Status', available ? STAT_WORDS[channel][statLevel(raw)] : '')
301
+ setText(channel + 'Note', available ? STAT_NOTES[channel][statLevel(raw)] : '')
302
+ setBar(channel + 'Bar', raw)
303
+ var channelDelta = available ? historyDelta(data, channel) : '—'
304
+ paintArrow(channel + 'Arrow', channelDelta)
305
+ paintDelta(channel + 'Delta', channelDelta)
306
+ }
307
+
308
+ var averages = [
309
+ ['avgTension', 'tension'], ['avgWarmth', 'warmth'],
310
+ ['avgHeaviness', 'heaviness'], ['avgClarity', 'clarity'],
314
311
  ]
315
- for (var i = 0; i < columns.length; i += 1) {
316
- var column = document.createElement('div')
317
- column.className = 'relation-col'
318
- var label = document.createElement('span')
319
- label.className = 'relation-label'
320
- label.textContent = columns[i][0]
321
- var value = document.createElement('strong')
322
- value.className = 'relation-value'
323
- value.textContent = available && columns[i][1] !== null ? fmt(columns[i][1]) : '–'
324
- var note = document.createElement('p')
325
- note.className = 'relation-note'
326
- note.textContent = available ? columns[i][2] : '—'
327
- column.appendChild(label)
328
- column.appendChild(value)
329
- column.appendChild(note)
330
- grid.appendChild(column)
312
+ for (var a = 0; a < averages.length; a += 1) {
313
+ var value = Number(intero[averages[a][1]])
314
+ setText(averages[a][0], available && Number.isFinite(value) ? fmt(value) : '—')
331
315
  }
332
- box.appendChild(grid)
316
+ setText('avgHint', available && Array.isArray(data.history) && data.history.length
317
+ ? data.history.length + ' 个采样' : '—')
333
318
 
334
- var count = Number(rel.conflict_count)
335
- if (!Number.isFinite(count)) count = safeConflict > 0 ? Math.round(safeConflict * 10) : 0
336
- var foot = document.createElement('div')
337
- foot.className = 'relation-foot'
338
- foot.textContent = available
339
- ? '认识 ' + knownDays() + ' 天 · 冲突 ' + count + ' 次 · 依恋 ' + attachmentOf(safeTrust || 0, safeCloseness || 0, safeConflict)
340
- : '认识 — 天 · 冲突 — 次 · 依恋 —'
341
- box.appendChild(foot)
319
+ setText('toneWord', available ? (scalarLabel(data.tone, '') || '正常') : '–')
320
+ setText('relationWord', available ? (scalarLabel(data.relation, '') || '–') : '–')
321
+ setText('autonomyWord', available ? (scalarLabel(data.autonomy, '') || '–') : '–')
322
+ if (available) setText('motivation', data.motivation || '安静待着,等你开口')
342
323
  }
343
324
 
344
- function renderStats(data) {
325
+ function renderNow(data) {
345
326
  var available = data && data.available !== false
346
- setText('statMood', available ? fmt(data.v) : '–')
347
- var relation = available && data.relation ? data.relation.stage : ''
348
- var rel = available && data.relationship_metrics ? Number(data.relationship_metrics.closeness) : NaN
349
- setText('statRelation', relation || (Number.isFinite(rel) ? fmt(rel) : '–'))
350
- setText('statMemory', String(available && typeof data.memory_count === 'number' ? data.memory_count : 0))
351
- var total = available && typeof data.memory_count === 'number' ? data.memory_count : 0
352
- var capacity = Math.max(0, Math.min(1, total / 200))
353
- var fill = $('capFill')
354
- if (fill) fill.style.width = Math.round(capacity * 100) + '%'
355
- setText('capText', total + ' / 200')
327
+ var narrative = (available && data.narrative) || {}
328
+ setText('nowIntent', available ? (String(narrative.intent || '').trim() || '—') : '—')
329
+ setText('nowLength', available && narrative.length_factor !== undefined
330
+ ? fmt(narrative.length_factor) : '—')
331
+ setText('nowInitiative', available && narrative.initiative !== undefined
332
+ ? fmt(narrative.initiative) : '—')
333
+ setText('nowBand', available ? (String(narrative.metacog_band || '').trim() || '—') : '—')
334
+ setText('nowHint', available ? '只读' : '无数据')
356
335
  }
357
336
 
358
- function renderMemorySummary(data) {
337
+ function renderToday(data) {
359
338
  var available = data && data.available !== false
360
339
  var rows = available && Array.isArray(data.memory_recent) ? data.memory_recent : []
361
- var total = available && typeof data.memory_count === 'number' ? data.memory_count : rows.length
362
- var today = 0
363
- var dayStart = new Date()
364
- dayStart.setHours(0, 0, 0, 0)
340
+ var dayStart = startOfToday()
341
+ var fresh = 0
365
342
  for (var i = 0; i < rows.length; i += 1) {
366
343
  var ts = Number(rows[i] && rows[i].ts)
367
- if (Number.isFinite(ts) && ts > 1000000000000 && ts >= dayStart.getTime()) today += 1
344
+ if (Number.isFinite(ts) && ts >= dayStart) fresh += 1
368
345
  }
369
- setText('memTotal', String(total))
370
- setText('memToday', String(available ? today : '—'))
371
- setText('memRecall', '—')
346
+ setText('todayMem', available ? String(fresh) : '—')
347
+ setText('todayTurns', available && Array.isArray(data.history) ? String(data.history.length) : '—')
348
+ var proactive = available ? Number(data.proactive_sent_today) : NaN
349
+ setText('todayProactive', Number.isFinite(proactive) ? String(Math.round(proactive)) : '—')
372
350
  }
373
351
 
374
- function renderRuntime(data) {
375
- var available = data && data.available !== false
376
- var seconds = Math.max(0, Math.round((Date.now() - PAGE_START) / 1000))
377
- var uptime = seconds < 60 ? seconds + ' 秒'
378
- : (seconds < 3600 ? Math.round(seconds / 60) + ' 分钟' : Math.round(seconds / 3600) + ' 小时')
379
- setText('runtimeUptime', uptime)
380
- var turns = available && Array.isArray(data.history) ? data.history.length : '—'
381
- setText('runtimeTurns', String(turns))
382
- var mode = $('embeddingMode')
383
- setText('runtimeVector', mode ? (mode.value || 'auto') : '—')
352
+ function mergePoints(data) {
353
+ var extras = data && data.available !== false ? data : {}
354
+ var history = Array.isArray(extras.history) ? extras.history : []
355
+ if (history.length) {
356
+ return history.map(function (item) {
357
+ var ts = Number(item.ts) || 0
358
+ return { ts: ts < 1e12 ? ts * 1000 : ts, v: Number(item.v) || 0 }
359
+ })
360
+ }
361
+ if (seriesPoints && seriesPoints.length) {
362
+ return seriesPoints.map(function (item) {
363
+ var ts = Number(item.ts) || 0
364
+ return { ts: ts < 1e12 ? ts * 1000 : ts, v: Number(item.v) || 0 }
365
+ })
366
+ }
367
+ return []
384
368
  }
385
369
 
370
+ function drawChartThrough(canvas, points) {
371
+ if (!canvas) return
372
+ var width = Math.max(120, Math.round(canvas.clientWidth || 320))
373
+ var height = Math.max(90, Math.round(canvas.clientHeight || 150))
374
+ var dpr = window.devicePixelRatio || 1
375
+ canvas.width = Math.round(width * dpr)
376
+ canvas.height = Math.round(height * dpr)
377
+ var ctx = canvas.getContext('2d')
378
+ if (!ctx) return
379
+ var palette = chartPalette()
380
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
381
+ ctx.clearRect(0, 0, width, height)
382
+
383
+ ctx.strokeStyle = palette.grid
384
+ ctx.globalAlpha = 0.45
385
+ ctx.lineWidth = 1
386
+ for (var g = 1; g <= 3; g += 1) {
387
+ var y = Math.round((height / 4) * g) + 0.5
388
+ ctx.beginPath()
389
+ ctx.moveTo(0, y)
390
+ ctx.lineTo(width, y)
391
+ ctx.stroke()
392
+ }
393
+
394
+ if (!points.length) return
395
+
396
+ var minTs = points[0].ts
397
+ var maxTs = points[points.length - 1].ts
398
+ var span = Math.max(1, maxTs - minTs)
399
+ var pad = 6
400
+ var usable = height - pad * 2
401
+
402
+ function xOf(ts) { return ((ts - minTs) / span) * (width - 2) + 1 }
403
+ function yOf(v) { return pad + (1 - (Math.max(-1, Math.min(1, v)) + 1) / 2) * usable }
404
+
405
+ ctx.globalAlpha = 1
406
+
407
+ ctx.beginPath()
408
+ ctx.moveTo(xOf(points[0].ts), yOf(points[0].v))
409
+ for (var i = 1; i < points.length; i += 1) ctx.lineTo(xOf(points[i].ts), yOf(points[i].v))
410
+ ctx.lineTo(xOf(points[points.length - 1].ts), height)
411
+ ctx.lineTo(xOf(points[0].ts), height)
412
+ ctx.closePath()
413
+ ctx.globalAlpha = 0.14
414
+ ctx.fillStyle = palette.accent
415
+ ctx.fill()
416
+ ctx.globalAlpha = 1
417
+
418
+ ctx.beginPath()
419
+ ctx.moveTo(xOf(points[0].ts), yOf(points[0].v))
420
+ for (var j = 1; j < points.length; j += 1) ctx.lineTo(xOf(points[j].ts), yOf(points[j].v))
421
+ ctx.strokeStyle = palette.accent
422
+ ctx.lineWidth = 2
423
+ ctx.lineJoin = 'round'
424
+ ctx.lineCap = 'round'
425
+ ctx.stroke()
426
+
427
+ var last = points[points.length - 1]
428
+ ctx.beginPath()
429
+ ctx.arc(xOf(last.ts), yOf(last.v), 3.4, 0, Math.PI * 2)
430
+ ctx.fillStyle = palette.ink
431
+ ctx.fill()
432
+ }
433
+
434
+ function renderChart(data) {
435
+ var canvas = $('moodChart')
436
+ if (!canvas) return
437
+ var points = mergePoints(data)
438
+ var empty = $('chartEmpty')
439
+ setText('chartHint', points.length ? points.length + ' 个采样点' : '—')
440
+ if (empty) empty.hidden = points.length > 0
441
+ canvas.hidden = !points.length
442
+ if (!points.length) return
443
+ drawChartThrough(canvas, points)
444
+ }
445
+
446
+ /* -------------------------------------------------------------- 记忆 --- */
447
+
386
448
  function renderMemories(data) {
387
449
  var list = $('memoryList')
388
- var hint = $('memoryHint')
389
450
  if (!list) return
390
451
  list.textContent = ''
391
452
  var rows = data && data.available !== false && Array.isArray(data.memory_recent)
@@ -402,344 +463,278 @@
402
463
  }
403
464
  for (var i = 0; i < rows.length; i += 1) {
404
465
  var item = rows[i] || {}
405
- var text = String(item.text || '').slice(0, 40)
406
466
  var li = document.createElement('li')
407
- var head = document.createElement('div')
408
- head.className = 'memory-head'
409
467
  var time = document.createElement('time')
410
468
  time.textContent = relativeTime(Number(item.ts))
411
- var tag = document.createElement('span')
412
- tag.className = 'memory-tag'
413
- tag.textContent = memoryKind(item.text)
414
- head.appendChild(time)
415
- head.appendChild(tag)
469
+ var kind = document.createElement('span')
470
+ kind.className = 'memory-kind'
471
+ kind.textContent = memoryKind(item.text)
416
472
  var body = document.createElement('span')
417
473
  body.className = 'memory-text'
418
- body.textContent = text
419
- li.appendChild(head)
474
+ body.textContent = String(item.text || '').slice(0, 80)
475
+ li.appendChild(time)
476
+ li.appendChild(kind)
420
477
  li.appendChild(body)
421
478
  list.appendChild(li)
422
479
  }
423
480
  }
424
481
 
425
- function mergePoints(data) {
426
- var extras = data && data.available !== false ? data : {}
427
- var history = Array.isArray(extras.history) ? extras.history : []
428
- if (history.length) {
429
- return history.map(function (item) {
430
- return { ts: Number(item.ts) || 0, v: Number(item.v) || 0 }
431
- })
432
- }
433
- if (seriesPoints && seriesPoints.length) {
434
- return seriesPoints.map(function (item) {
435
- return { ts: Number(item.ts) / 1000, v: Number(item.value) || 0 }
436
- })
437
- }
438
- if (data && data.available !== false) {
439
- localPoints.push({ ts: Date.now() / 1000, v: Number(data.v) || 0 })
440
- if (localPoints.length > 120) localPoints = localPoints.slice(-120)
482
+ function renderMemorySummary(data) {
483
+ var available = data && data.available !== false
484
+ var rows = available && Array.isArray(data.memory_recent) ? data.memory_recent : []
485
+ var total = available && typeof data.memory_count === 'number' ? data.memory_count : rows.length
486
+ var dayStart = startOfToday()
487
+ var today = 0
488
+ for (var i = 0; i < rows.length; i += 1) {
489
+ var ts = Number(rows[i] && rows[i].ts)
490
+ if (Number.isFinite(ts) && ts >= dayStart) today += 1
441
491
  }
442
- return localPoints.slice()
492
+ setText('memTotal', String(total))
493
+ setText('memToday', String(available ? today : '—'))
494
+ var recall = available ? Number(data.recall_count) : NaN
495
+ setText('memRecall', Number.isFinite(recall) && recall > 0 ? String(Math.round(recall)) : '—')
496
+ var ratio = clamp01(total / 200)
497
+ setBar('capFill', ratio)
498
+ setText('capFillText', pct(ratio))
499
+ setText('capText', total + ' / 200')
443
500
  }
444
501
 
445
- function drawChartInto(canvas, data) {
446
- if (!canvas) return
447
- var points = mergePoints(data)
448
- var wrapWidth = canvas.clientWidth || canvas.parentElement.clientWidth || 600
449
- var wrapHeight = 170
450
- var dpr = window.devicePixelRatio || 1
451
- canvas.width = Math.round(wrapWidth * dpr)
452
- canvas.height = Math.round(wrapHeight * dpr)
453
- var ctx = canvas.getContext('2d')
454
- if (!ctx) return
455
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
456
- ctx.clearRect(0, 0, wrapWidth, wrapHeight)
457
-
458
- var styles = window.getComputedStyle(document.documentElement)
459
- var lineColor = styles.getPropertyValue('--accent').trim() || '#4176e6'
460
- var gridColor = styles.getPropertyValue('--line').trim() || 'rgba(127,127,127,.2)'
461
- var mutedColor = styles.getPropertyValue('--text-3').trim() || '#81858c'
462
- var amber = styles.getPropertyValue('--amber').trim() || '#f7ad31'
463
-
464
- var padL = 34
465
- var padR = 12
466
- var padT = 12
467
- var padB = 20
468
- var plotW = Math.max(10, wrapWidth - padL - padR)
469
- var plotH = Math.max(10, wrapHeight - padT - padB)
470
-
471
- ctx.strokeStyle = gridColor
472
- ctx.lineWidth = 1
473
- ctx.font = '10px -apple-system, "Microsoft YaHei", sans-serif'
474
- ctx.fillStyle = mutedColor
475
- for (var g = 0; g < 5; g += 1) {
476
- var y = padT + (plotH * g) / 4
477
- ctx.beginPath()
478
- ctx.moveTo(padL, y)
479
- ctx.lineTo(padL + plotW, y)
480
- ctx.stroke()
481
- if (g === 0 || g === 4) {
482
- var label = g === 0 ? '1.0' : '-1.0'
483
- ctx.fillText(label, 6, y + 3)
484
- }
485
- }
486
- if (points.length > 1) {
487
- var minTs = points[0].ts
488
- var maxTs = points[points.length - 1].ts
489
- var span = maxTs - minTs || 1
490
- var toX = function (ts) { return padL + ((ts - minTs) / span) * plotW }
491
- var toY = function (v) { return padT + ((1 - Math.max(-1, Math.min(1, v))) / 2) * plotH }
492
-
493
- ctx.beginPath()
494
- for (var i = 0; i < points.length; i += 1) {
495
- var x = toX(points[i].ts)
496
- var py = toY(points[i].v)
497
- if (i === 0) ctx.moveTo(x, py)
498
- else ctx.lineTo(x, py)
499
- }
500
- ctx.strokeStyle = lineColor
501
- ctx.lineWidth = 2
502
- ctx.lineJoin = 'round'
503
- ctx.stroke()
502
+ /* -------------------------------------------------------------- 关系 --- */
504
503
 
505
- var last = points[points.length - 1]
506
- ctx.fillStyle = lineColor
507
- ctx.beginPath()
508
- ctx.arc(toX(last.ts), toY(last.v), 3, 0, Math.PI * 2)
509
- ctx.fill()
510
-
511
- var events = data && data.available !== false && Array.isArray(data.memory_recent)
512
- ? data.memory_recent : []
513
- ctx.fillStyle = amber
514
- for (var e = 0; e < events.length; e += 1) {
515
- var eventTs = Number(events[e].ts) / 1000
516
- if (!Number.isFinite(eventTs) || eventTs < minTs || eventTs > maxTs) continue
517
- var eventY = toY(last.v)
518
- for (var p = 0; p < points.length; p += 1) {
519
- if (points[p].ts >= eventTs) { eventY = toY(points[p].v); break }
520
- }
521
- ctx.beginPath()
522
- ctx.arc(toX(eventTs), eventY, 2.6, 0, Math.PI * 2)
523
- ctx.fill()
524
- }
525
- setText(canvas.id === 'deskChart' ? 'deskChartHint' : 'chartHint', '单线 + ' + events.length + ' 个事件点')
526
- } else {
527
- ctx.strokeStyle = lineColor
528
- ctx.lineWidth = 2
529
- var midY = padT + plotH / 2
530
- ctx.beginPath()
531
- ctx.moveTo(padL, midY)
532
- ctx.lineTo(padL + plotW, midY)
533
- ctx.stroke()
534
- if (points.length === 1) {
535
- ctx.fillStyle = lineColor
536
- ctx.beginPath()
537
- ctx.arc(padL + plotW - 3, padT + ((1 - Math.max(-1, Math.min(1, points[0].v))) / 2) * plotH, 3, 0, Math.PI * 2)
538
- ctx.fill()
539
- }
540
- setText(canvas.id === 'deskChart' ? 'deskChartHint' : 'chartHint', '积累中 · 单点')
504
+ function relationRow(label, value, note, available) {
505
+ var row = document.createElement('div')
506
+ var dt = document.createElement('dt')
507
+ dt.textContent = label
508
+ var dd = document.createElement('dd')
509
+ dd.textContent = available ? fmt(value) : '—'
510
+ if (note && available) {
511
+ var small = document.createElement('small')
512
+ small.textContent = note
513
+ dd.appendChild(small)
541
514
  }
542
- ctx.fillStyle = mutedColor
543
- ctx.fillText('24 小时', padL, wrapHeight - 5)
544
- }
545
-
546
- function renderChart(data) {
547
- drawChartInto($('moodChart'), data)
548
- drawChartInto($('deskChart'), data)
549
- }
550
-
551
- var DESK_HIDDEN_KEY = 'biocore-desk-hidden'
552
- var RAIL_KEY = 'biocore-rail-collapsed'
553
-
554
- function copyText(fromId, toId) {
555
- var from = $(fromId)
556
- var to = $(toId)
557
- if (from && to) to.textContent = from.textContent
515
+ row.appendChild(dt)
516
+ row.appendChild(dd)
517
+ return row
558
518
  }
559
519
 
560
- function copyBar(fromId, toId) {
561
- var from = $(fromId)
562
- var to = $(toId)
563
- if (from && to) to.style.width = from.style.width
564
- }
565
-
566
- /** Desktop grid mirrors the mobile DOM (single data source, no duplicate state). */
567
- function deskSync(data) {
568
- copyText('motivation', 'deskMotivation')
569
- copyText('statusText', 'deskStatusText')
570
- var mobileStatus = $('status')
571
- var deskStatus = $('deskStatus')
572
- if (mobileStatus && deskStatus) {
573
- deskStatus.className = 'status' + (mobileStatus.classList.contains('on') ? ' on' : (mobileStatus.classList.contains('off') ? ' off' : ''))
574
- }
575
- copyText('num-v', 'deskNumV')
576
- copyText('moodNote', 'deskWordV')
577
- copyText('arrow-v', 'deskArrowV')
578
- copyText('delta-v', 'deskDeltaV')
579
- copyBar('bar-v', 'deskBarV')
580
- copyText('num-emotion-tension', 'deskNumTension')
581
- copyText('status-tension', 'deskWordTension')
582
- copyText('arrow-emotion-tension', 'deskArrowTension')
583
- copyText('delta-emotion-tension', 'deskDeltaTension')
584
- copyBar('bar-emotion-tension', 'deskBarTension')
585
- copyText('runtimeUptime', 'deskUptime')
586
- copyText('runtimeTurns', 'deskTurns')
587
- copyText('runtimeVector', 'deskVector')
588
- copyText('capText', 'deskCapText')
589
- copyBar('capFill', 'deskCapFill')
590
- copyText('dataDirText', 'deskDataDir')
591
- copyText('memTotal', 'deskTodayMem')
592
- copyText('runtimeTurns', 'deskTodayTurns')
593
- copyText('memoryHint', 'deskMemoryHint')
594
- var memoryFrom = $('memoryList')
595
- var memoryTo = $('deskMemoryList')
596
- if (memoryFrom && memoryTo) memoryTo.innerHTML = memoryFrom.innerHTML
520
+ function renderRelation(data) {
521
+ var box = $('metrics')
522
+ if (!box) return
523
+ box.textContent = ''
597
524
  var available = data && data.available !== false
598
- var intero = (available && data.intero) || {}
599
- var channels = [['tension', 'deskAvgTension'], ['warmth', 'deskAvgWarmth'], ['heaviness', 'deskAvgHeaviness'], ['clarity', 'deskAvgClarity']]
600
- for (var i = 0; i < channels.length; i += 1) {
601
- var value = Number(intero[channels[i][0]])
602
- setText(channels[i][1], Number.isFinite(value) ? fmt(value) : '—')
603
- }
604
525
  var rel = (available && data.relationship_metrics) || {}
605
- setText('deskRelCloseness', available && Number.isFinite(Number(rel.closeness)) ? fmt(rel.closeness) : '—')
606
- setText('deskRelTrust', available && Number.isFinite(Number(rel.trust)) ? fmt(rel.trust) : '—')
607
- setText('deskRelConflict', available && Number.isFinite(Number(rel.conflict)) ? fmt(rel.conflict) : '—')
608
- setText('deskTodayProactive', '—')
609
- var personaBox = $('personaText')
610
- var personaChars = personaBox ? String(personaBox.value || '').replace(/s+/g, '').length : 0
611
- setText('deskPersona', personaChars ? '已载入' : '未载入')
612
- setText('deskPersonaNote', personaChars ? (personaChars + ' 字 · 编译后只改倾向') : '还没有写人设')
613
- }
526
+ var closeness = Number(rel.closeness)
527
+ var trust = Number(rel.trust)
528
+ var conflict = Number(rel.conflict)
529
+ var safeCloseness = Number.isFinite(closeness) ? closeness : 0
530
+ var safeTrust = Number.isFinite(trust) ? trust : 0
531
+ var safeConflict = Number.isFinite(conflict) ? conflict : 0
614
532
 
615
- function deskCards() {
616
- return Array.prototype.slice.call(document.querySelectorAll('#deskGrid .desk-card'))
617
- }
533
+ var list = document.createElement('dl')
534
+ list.className = 'data-list'
535
+ list.appendChild(relationRow('亲近', safeCloseness, '', available))
536
+ list.appendChild(relationRow('信任', safeTrust, '', available))
537
+ list.appendChild(relationRow('冲突', safeConflict, '', available))
538
+ box.appendChild(list)
618
539
 
619
- function deskCardId(card, index) {
620
- if (!card.id) card.id = 'desk-card-' + index
621
- return card.id
540
+ var count = Number(rel.conflict_count)
541
+ if (!Number.isFinite(count)) count = safeConflict > 0 ? Math.round(safeConflict * 10) : 0
542
+ var foot = document.createElement('div')
543
+ foot.className = 'relation-foot'
544
+ var facts = [
545
+ ['认识', available ? knownDays() + ' 天' : '— 天'],
546
+ ['冲突', available ? count + ' 次' : '— 次'],
547
+ ['依恋', available ? attachmentOf(safeTrust, safeCloseness, safeConflict) : '—'],
548
+ ]
549
+ for (var i = 0; i < facts.length; i += 1) {
550
+ var cell = document.createElement('span')
551
+ cell.textContent = facts[i][0]
552
+ var value = document.createElement('b')
553
+ value.textContent = facts[i][1]
554
+ cell.appendChild(value)
555
+ foot.appendChild(cell)
556
+ }
557
+ box.appendChild(foot)
558
+ setText('relationHint', available ? '只读' : '无数据')
622
559
  }
623
560
 
624
- function readDeskHidden() {
625
- try {
626
- var raw = window.localStorage.getItem(DESK_HIDDEN_KEY)
627
- var list = raw ? JSON.parse(raw) : []
628
- return Array.isArray(list) ? list : []
629
- } catch (e) { return [] }
561
+ function renderNarrative(data) {
562
+ var box = $('narrative')
563
+ if (!box) return
564
+ box.textContent = ''
565
+ var available = data && data.available !== false
566
+ var narrative = (available && data.narrative) || {}
567
+ var rows = [
568
+ ['意图', available ? (String(narrative.intent || '').trim() || '—') : '—'],
569
+ ['主动性', available && narrative.initiative !== undefined ? fmt(narrative.initiative) : '—'],
570
+ ['长度因子', available && narrative.length_factor !== undefined ? fmt(narrative.length_factor) : '—'],
571
+ ['元认知带', available ? (String(narrative.metacog_band || '').trim() || '—') : '—'],
572
+ ['允许纠偏', available ? (narrative.allow_correction === true ? '是' : '否') : '—'],
573
+ ]
574
+ for (var i = 0; i < rows.length; i += 1) {
575
+ var wrap = document.createElement('div')
576
+ var dt = document.createElement('dt')
577
+ dt.textContent = rows[i][0]
578
+ var dd = document.createElement('dd')
579
+ dd.textContent = rows[i][1]
580
+ wrap.appendChild(dt)
581
+ wrap.appendChild(dd)
582
+ box.appendChild(wrap)
583
+ }
584
+ setText('narrativeHint', available ? '上一轮' : '无数据')
630
585
  }
631
586
 
632
- function writeDeskHidden(list) {
633
- try { window.localStorage.setItem(DESK_HIDDEN_KEY, JSON.stringify(list)) } catch (e) { /* ignore */ }
634
- }
587
+ /* -------------------------------------------------------------- 工具 --- */
635
588
 
636
- function applyDeskHidden() {
637
- var hidden = readDeskHidden()
638
- deskCards().forEach(function (card, index) {
639
- card.classList.toggle('is-desk-hidden', hidden.indexOf(deskCardId(card, index)) >= 0)
640
- })
589
+ function renderRuntime(data) {
590
+ var available = data && data.available !== false
591
+ var seconds = Math.max(0, Math.round((Date.now() - PAGE_START) / 1000))
592
+ var uptime = seconds < 60 ? seconds + ' 秒'
593
+ : (seconds < 3600 ? Math.round(seconds / 60) + ' 分钟' : Math.round(seconds / 3600) + ' 小时')
594
+ setText('runtimeUptime', uptime)
595
+ setText('runtimeTurns', available && Array.isArray(data.history) ? String(data.history.length) : '—')
596
+ var mode = $('embeddingMode')
597
+ setText('runtimeVector', mode ? (mode.value || 'auto') : '—')
598
+ setText('runSession', '会话 ' + shortSession())
599
+ setText('diagSession', SESSION || '未指定')
641
600
  }
642
601
 
643
- function ensureDeskHides() {
644
- deskCards().forEach(function (card, index) {
645
- deskCardId(card, index)
646
- if (card.querySelector('[data-desk-hide]')) return
647
- var button = document.createElement('button')
648
- button.type = 'button'
649
- button.className = 'desk-hide'
650
- button.setAttribute('data-desk-hide', '')
651
- button.setAttribute('aria-label', '隐藏这张卡片')
652
- button.textContent = '−'
653
- card.appendChild(button)
602
+ function renderDiagnostics(data) {
603
+ var available = data && data.available !== false
604
+ var degraded = available && Array.isArray(data.degraded) ? data.degraded : []
605
+ var errors = available && Array.isArray(data.errors) ? data.errors : []
606
+ var parts = degraded.concat(errors).filter(Boolean).map(function (item) {
607
+ return String(item).slice(0, 40)
654
608
  })
609
+ setText('diagDegraded', parts.length ? parts.join(',') : '无')
610
+ var generated = available ? Number(data.generated_at) : NaN
611
+ setText('diagGenerated', Number.isFinite(generated) && generated > 0
612
+ ? new Date(generated).toLocaleTimeString() : '—')
613
+ setText('diagAdapter', available && data.adapter && data.adapter.version
614
+ ? String(data.adapter.version) : '—')
615
+ }
616
+
617
+ function loadStats() {
618
+ var rate = $('cacheStatText')
619
+ var detail = $('cacheDetail')
620
+ if (!rate) return
621
+ fetch('/api/biocore/stats', { headers: { accept: 'application/json' } })
622
+ .then(function (response) { return response.ok ? response.json() : null })
623
+ .then(function (body) {
624
+ var cache = body && body.cache ? body.cache : null
625
+ if (cache && Number(cache.turns) > 0) {
626
+ var hitRate = Number(cache.hit_rate) || 0
627
+ rate.textContent = (Math.round(hitRate * 1000) / 10) + '%'
628
+ if (detail) {
629
+ detail.textContent = (Number(cache.cache_read) || 0) + ' / ' + (Number(cache.prompt_tokens) || 0)
630
+ }
631
+ return
632
+ }
633
+ var embed = body && body.embed ? body.embed : null
634
+ var hit = embed ? (Number(embed.hit) || 0) : 0
635
+ var miss = embed ? (Number(embed.miss) || 0) : 0
636
+ if (hit + miss > 0) {
637
+ rate.textContent = (Math.round(hit / (hit + miss) * 1000) / 10) + '%'
638
+ if (detail) detail.textContent = 'hit ' + hit + ' / miss ' + miss
639
+ return
640
+ }
641
+ rate.textContent = '暂无数据'
642
+ if (detail) detail.textContent = '—'
643
+ })
644
+ .catch(function () {
645
+ rate.textContent = '暂无数据'
646
+ if (detail) detail.textContent = '—'
647
+ })
655
648
  }
656
649
 
657
- function deskLabel(card) {
658
- var named = card.getAttribute('data-card-label')
659
- if (named) return named
660
- var title = card.querySelector('h2')
661
- return title ? title.textContent.trim() : card.id
662
- }
663
-
664
- function restoreDeskCard(id) {
665
- writeDeskHidden(readDeskHidden().filter(function (item) { return item !== id }))
666
- applyDeskHidden()
667
- renderAddCards()
668
- layoutToast('已恢复卡片')
669
- }
650
+ var traceFormatter = function () { return window.BioTraceView || null }
670
651
 
671
- function onDeskEditClick(event) {
672
- var target = event.target
673
- var add = target && target.closest ? target.closest('[data-desk-add]') : null
674
- if (add) {
675
- event.preventDefault()
676
- restoreDeskCard(add.getAttribute('data-desk-add'))
652
+ function renderTrace(body) {
653
+ var list = $('traceList')
654
+ if (!list) return
655
+ list.textContent = ''
656
+ var view = traceFormatter()
657
+ var failed = !body || body.ok === false
658
+ var rows = !failed && Array.isArray(body.rows) ? body.rows.slice(0, TRACE_LIMIT) : []
659
+ var formatted = {
660
+ rows: view ? rows.map(function (row) { return view.formatRow(row) }) : rows,
661
+ empty: !rows.length,
662
+ error: failed,
663
+ }
664
+ setText('traceHint', rows.length ? '最近 ' + rows.length + ' 轮' : '还没有记录')
665
+ if (formatted.empty) {
666
+ var empty = document.createElement('p')
667
+ empty.className = 'state-note'
668
+ empty.textContent = failed ? '读取失败,稍后自动重试' : '还没有记录'
669
+ list.appendChild(empty)
677
670
  return
678
671
  }
679
- var hide = target && target.closest ? target.closest('[data-desk-hide]') : null
680
- if (!hide) return
681
- event.preventDefault()
682
- var card = hide.closest('.desk-card')
683
- if (!card) return
684
- var hidden = readDeskHidden()
685
- var id = card.id
686
- if (hidden.indexOf(id) < 0) hidden.push(id)
687
- writeDeskHidden(hidden)
688
- applyDeskHidden()
689
- renderAddCards()
690
- layoutToast('已隐藏「' + deskLabel(card) + '」')
691
- }
692
-
693
- function railApply(collapsed) {
694
- document.body.classList.toggle('rail-collapsed', !!collapsed)
695
- var button = $('railToggle')
696
- if (!button) return
697
- var label = collapsed ? '展开导航' : '收起导航'
698
- button.setAttribute('aria-expanded', String(!collapsed))
699
- button.setAttribute('aria-label', label)
700
- button.setAttribute('title', label)
701
- }
702
-
703
- function railInit() {
704
- var storage = layoutStorage()
705
- var collapsed = false
706
- try { collapsed = !!storage && storage.getItem(RAIL_KEY) === '1' } catch (e) { collapsed = false }
707
- railApply(collapsed)
708
- var button = $('railToggle')
709
- if (!button) return
710
- button.addEventListener('click', function () {
711
- var next = !document.body.classList.contains('rail-collapsed')
712
- railApply(next)
713
- try { if (storage) storage.setItem(RAIL_KEY, next ? '1' : '0') } catch (e) { /* ignore */ }
672
+ for (var i = 0; i < formatted.rows.length; i += 1) {
673
+ var row = formatted.rows[i] || {}
674
+ var item = document.createElement('div')
675
+ item.className = 'trace-row'
676
+ var values = [row.time, row.changes, row.intent, row.length, row.gate, row.memory]
677
+ for (var c = 0; c < values.length; c += 1) {
678
+ var cell = document.createElement('span')
679
+ if (c === 0) cell.className = 'trace-time'
680
+ var cellText = String(values[c] || '—').split(' · ').join(',')
681
+ cell.textContent = cellText
682
+ cell.title = cellText
683
+ cell.setAttribute('data-label', ['时间', '状态变化', '意图', '长度来源', '闸命中', '记忆'][c])
684
+ item.appendChild(cell)
685
+ }
686
+ list.appendChild(item)
687
+ }
688
+ }
689
+
690
+ function fetchTrace() {
691
+ if (!SESSION) {
692
+ renderTrace({ ok: true, rows: [] })
693
+ return Promise.resolve(null)
694
+ }
695
+ return fetch('/api/biocore/trace?session=' + encodeURIComponent(SESSION) + '&limit=' + TRACE_LIMIT, {
696
+ headers: { accept: 'application/json' },
714
697
  })
698
+ .then(function (response) { return response.json() })
699
+ .then(function (body) { renderTrace(body); return body })
700
+ .catch(function () { renderTrace(null); return null })
715
701
  }
716
702
 
717
- function updateFooter(data) {
718
- var memory = data && data.available !== false && typeof data.memory_count === 'number'
719
- ? data.memory_count : '-'
720
- setText('footerMeta', '会话 ' + shortSession() + ' · 记忆 ' + memory + ' · 每 2 秒刷新')
703
+ /* ------------------------------------------------------------ 状态轮询 --- */
704
+
705
+ function finishBoot() {
706
+ if (bootDone) return
707
+ bootDone = true
708
+ document.body.classList.remove('is-booting')
721
709
  }
722
710
 
723
711
  function render(data) {
724
712
  lastData = data
725
- renderNumbers(data)
726
713
  renderMetrics(data)
714
+ renderNow(data)
715
+ renderToday(data)
727
716
  renderMemories(data)
728
717
  renderMemorySummary(data)
729
- renderStats(data)
718
+ renderRelation(data)
719
+ renderNarrative(data)
730
720
  renderRuntime(data)
721
+ renderDiagnostics(data)
731
722
  renderChart(data)
732
- updateFooter(data)
733
- deskSync(data)
734
- deskSync(data)
723
+ var memory = data && data.available !== false && typeof data.memory_count === 'number'
724
+ ? '记忆 ' + data.memory_count : '记忆 -'
725
+ setText('footerMeta', '会话 ' + (SESSION || '-') + ' ' + memory + ' 每 ' + (POLL_MS / 1000) + ' 秒刷新')
726
+ var live = !!(data && data.available !== false)
727
+ document.body.classList.toggle('is-empty', !live)
735
728
  if (!data) renderEmpty('request_failed')
736
729
  else if (data.available === false) renderEmpty(String(data.reason || 'request_failed'))
737
730
  else clearEmpty()
731
+ finishBoot()
738
732
  }
739
733
 
740
734
  function fetchState() {
741
735
  if (!SESSION) {
742
736
  setStatus(false, '未连接')
737
+ render(null)
743
738
  setText('motivation', '没有会话参数,先在聊天会话里打开')
744
739
  return Promise.resolve(null)
745
740
  }
@@ -749,7 +744,8 @@
749
744
  .then(function (response) { return response.json() })
750
745
  .then(function (data) {
751
746
  lastSuccess = Date.now()
752
- setStatus(true, '已连接 · 0 秒前')
747
+ var live = data && data.available !== false
748
+ setStatus(live, live ? '已连接 0 秒前' : '还没有状态')
753
749
  render(data)
754
750
  return data
755
751
  })
@@ -767,13 +763,20 @@
767
763
  })
768
764
  .then(function (response) { return response.ok ? response.json() : null })
769
765
  .then(function (body) {
770
- if (body && body.series && Array.isArray(body.series.v) && body.series.v.length) {
771
- seriesPoints = body.series.v
766
+ if (body && Array.isArray(body.points) && body.points.length) {
767
+ seriesPoints = body.points
768
+ } else if (body && body.series && Array.isArray(body.series.v)
769
+ && Array.isArray(body.series.ts) && body.series.v.length === body.series.ts.length) {
770
+ seriesPoints = body.series.ts.map(function (ts, index) {
771
+ return { ts: ts, v: body.series.v[index] }
772
+ })
772
773
  }
773
774
  })
774
775
  .catch(function () { /* optional: state extras already cover the chart */ })
775
776
  }
776
777
 
778
+ /* -------------------------------------------------------------- 开关 --- */
779
+
777
780
  function switchPost(key, value) {
778
781
  return fetch('/api/biocore/switch', {
779
782
  method: 'POST',
@@ -782,15 +785,9 @@
782
785
  }).then(function (response) { return response.json() })
783
786
  }
784
787
 
785
- function paintSwitchState(key, checked) {
786
- var states = document.querySelectorAll('[data-switch-state="' + key + '"]')
787
- for (var i = 0; i < states.length; i += 1) states[i].textContent = '当前:' + (checked ? '开' : '关')
788
- }
789
-
790
788
  function setSwitchInputs(key, checked) {
791
789
  var inputs = document.querySelectorAll('input[data-switch="' + key + '"]')
792
790
  for (var i = 0; i < inputs.length; i += 1) inputs[i].checked = !!checked
793
- paintSwitchState(key, checked)
794
791
  }
795
792
 
796
793
  function applySwitchValues(values) {
@@ -805,8 +802,16 @@
805
802
  var box = $('switches')
806
803
  if (!box) return
807
804
  box.textContent = ''
805
+ var lastGroup = ''
808
806
  for (var i = 0; i < SWITCHES.length; i += 1) {
809
807
  var meta = SWITCHES[i]
808
+ if (meta.group && meta.group !== lastGroup) {
809
+ lastGroup = meta.group
810
+ var heading = document.createElement('p')
811
+ heading.className = 'switch-group'
812
+ heading.textContent = meta.group
813
+ box.appendChild(heading)
814
+ }
810
815
  var row = document.createElement('label')
811
816
  row.className = 'switch-row'
812
817
  var textWrap = document.createElement('span')
@@ -817,13 +822,8 @@
817
822
  var desc = document.createElement('span')
818
823
  desc.className = 'switch-desc'
819
824
  desc.textContent = meta.desc
820
- var state = document.createElement('span')
821
- state.className = 'switch-state'
822
- state.setAttribute('data-switch-state', meta.key)
823
- state.textContent = '当前:关'
824
825
  textWrap.appendChild(title)
825
826
  textWrap.appendChild(desc)
826
- textWrap.appendChild(state)
827
827
  var input = document.createElement('input')
828
828
  input.type = 'checkbox'
829
829
  input.setAttribute('data-switch', meta.key)
@@ -842,20 +842,20 @@
842
842
  var next = !!input.checked
843
843
  switchBusy = true
844
844
  input.disabled = true
845
- setText('switchHint', '写入中…')
845
+ setText('switchHint', '写入中')
846
846
  switchPost(key, next)
847
847
  .then(function (body) {
848
848
  if (body && body.ok === true && body.switches) {
849
849
  applySwitchValues(body.switches)
850
- setText('switchHint', '已写入宿主设置')
850
+ setText('switchHint', '已写入 flags')
851
851
  } else {
852
852
  setSwitchInputs(key, !next)
853
- setText('switchHint', '写入失败:' + ((body && body.reason) || 'unknown'))
853
+ setText('switchHint', '写入失败 ' + ((body && body.reason) || 'unknown'))
854
854
  }
855
855
  })
856
856
  .catch(function () {
857
857
  setSwitchInputs(key, !next)
858
- setText('switchHint', '写入失败:未连接')
858
+ setText('switchHint', '写入失败 未连接')
859
859
  })
860
860
  .then(function () {
861
861
  switchBusy = false
@@ -863,8 +863,16 @@
863
863
  })
864
864
  }
865
865
 
866
- var personaText = ''
867
- var personaDirty = false
866
+ function loadSwitches() {
867
+ return fetch('/api/biocore/switch', { headers: { accept: 'application/json' } })
868
+ .then(function (response) { return response.json() })
869
+ .then(function (body) {
870
+ if (body && body.ok === true && body.switches) applySwitchValues(body.switches)
871
+ })
872
+ .catch(function () { /* keep defaults, POST still tries */ })
873
+ }
874
+
875
+ /* -------------------------------------------------------------- 人设 --- */
868
876
 
869
877
  function personaStatus(message, tone) {
870
878
  var el = $('personaStatus')
@@ -874,22 +882,13 @@
874
882
  el.classList.toggle('persona-status-ok', tone === 'ok')
875
883
  }
876
884
 
877
- function renderPersonaPreview(rows) {
878
- var box = $('personaPreview')
879
- if (!box) return
880
- box.textContent = ''
881
- var list = Array.isArray(rows) ? rows : []
882
- for (var i = 0; i < list.length; i += 1) {
883
- var row = list[i] || {}
884
- var item = document.createElement('div')
885
- var label = document.createElement('span')
886
- label.textContent = String(row.label || row.key || '')
887
- var value = document.createElement('strong')
888
- value.textContent = row.value === null || row.value === undefined ? '–' : String(row.value)
889
- item.appendChild(label)
890
- item.appendChild(value)
891
- box.appendChild(item)
892
- }
885
+ /** One status line for the persona card: file, compile and persist state. */
886
+ function personaPersistHint(message, tone) {
887
+ var el = $('personaHint')
888
+ if (!el) return
889
+ el.textContent = message
890
+ el.classList.toggle('persona-status-error', tone === 'error')
891
+ el.classList.toggle('persona-status-ok', tone === 'ok')
893
892
  }
894
893
 
895
894
  function loadPersona() {
@@ -897,19 +896,18 @@
897
896
  .then(function (response) { return response.json() })
898
897
  .then(function (body) {
899
898
  if (!body || body.ok !== true) {
900
- personaStatus('人设文件坏了:' + ((body && body.reason) || 'bad_profile'), 'error')
899
+ personaStatus('人设文件坏了 ' + ((body && body.reason) || 'bad_profile'), 'error')
901
900
  return body
902
901
  }
903
902
  personaText = String(body.text || '')
904
903
  var area = $('personaText')
905
904
  if (area) area.value = personaText
906
905
  personaDirty = false
907
- renderPersonaPreview(body.preview)
908
- personaStatus(body.text ? '已载入' : '未设置人设', body.text ? 'ok' : '')
906
+ personaStatus(body.text ? '已载入' : '未设置', body.text ? 'ok' : '')
909
907
  return body
910
908
  })
911
909
  .catch(function () {
912
- personaStatus('连不上宿主,读不到人设', 'error')
910
+ personaStatus('连不上宿主', 'error')
913
911
  return null
914
912
  })
915
913
  }
@@ -919,11 +917,11 @@
919
917
  var button = $('personaSave')
920
918
  var text = area ? area.value : ''
921
919
  if (!text.trim()) {
922
- personaStatus('编译失败:人设不能为空', 'error')
920
+ personaStatus('人设不能为空', 'error')
923
921
  return
924
922
  }
925
923
  if (!window.confirm('确定保存并编译人设吗?这会覆盖当前数值画像。')) return
926
- personaStatus('编译中…')
924
+ personaStatus('编译中')
927
925
  if (button) button.disabled = true
928
926
  fetch('/api/biocore/persona', {
929
927
  method: 'POST',
@@ -933,31 +931,20 @@
933
931
  .then(function (response) { return response.json() })
934
932
  .then(function (body) {
935
933
  if (!body || body.ok !== true) {
936
- personaStatus('编译失败:' + ((body && body.reason) || 'unknown'), 'error')
934
+ personaStatus('编译失败 ' + ((body && body.reason) || 'unknown'), 'error')
937
935
  if (body && body.detail) setText('personaHint', String(body.detail).slice(0, 120))
938
936
  return body
939
937
  }
940
938
  personaDirty = false
941
939
  personaText = String(body.text || text)
942
- renderPersonaPreview(body.preview)
943
- personaStatus('已编译 · ' + new Date().toLocaleTimeString(), 'ok')
940
+ personaStatus('已编译 ' + new Date().toLocaleTimeString(), 'ok')
944
941
  return body
945
942
  })
946
943
  .catch(function () {
947
- personaStatus('编译失败:连不上宿主', 'error')
944
+ personaStatus('编译失败 连不上宿主', 'error')
948
945
  return null
949
946
  })
950
- .then(function () {
951
- if (button) button.disabled = false
952
- })
953
- }
954
-
955
- function personaPersistHint(message, tone) {
956
- var el = $('personaPersistHint')
957
- if (!el) return
958
- el.textContent = message
959
- el.classList.toggle('persona-status-error', tone === 'error')
960
- el.classList.toggle('persona-status-ok', tone === 'ok')
947
+ .then(function () { if (button) button.disabled = false })
961
948
  }
962
949
 
963
950
  function loadPersonaPersist() {
@@ -967,31 +954,34 @@
967
954
  var applyButton = $('personaPersist')
968
955
  var rollbackButton = $('personaRollback')
969
956
  if (!body || body.ok !== true) {
970
- personaPersistHint('常驻:宿主不可用(' + ((body && body.reason) || 'unknown') + ')', 'error')
957
+ personaPersistHint('常驻不可用 ' + ((body && body.reason) || 'unknown'), 'error')
971
958
  if (rollbackButton) rollbackButton.hidden = true
972
959
  return body
973
960
  }
974
961
  if (body.applied) {
975
- personaPersistHint('常驻:已应用 · 预设 ' + String(body.preset_id || '') + ' · ' + Number(body.chars || 0) + ' 字' + (body.matches_preset ? '' : '(预设已变更)'), body.matches_preset ? 'ok' : 'error')
962
+ personaPersistHint('常驻已应用 预设 ' + String(body.preset_id || '') + ' '
963
+ + Number(body.chars || 0) + ' 字'
964
+ + (body.matches_preset ? '' : ' 预设已变更'), body.matches_preset ? 'ok' : 'error')
976
965
  if (rollbackButton) rollbackButton.hidden = false
977
966
  if (applyButton) applyButton.textContent = '重新应用常驻'
978
967
  } else {
979
- personaPersistHint('常驻:未应用(' + String(body.reason || 'not_applied') + ')', '')
968
+ var reason = String(body.reason || 'not_applied')
969
+ personaPersistHint(reason === 'not_applied' ? '已保存人设,常驻未启用' : '常驻未应用 ' + reason, '')
980
970
  if (rollbackButton) rollbackButton.hidden = !body.has_backup
981
- if (applyButton) applyButton.textContent = '设为常驻模式'
971
+ if (applyButton) applyButton.textContent = '设为常驻'
982
972
  }
983
973
  return body
984
974
  })
985
975
  .catch(function () {
986
- personaPersistHint('常驻:连不上宿主', 'error')
976
+ personaPersistHint('常驻 连不上宿主', 'error')
987
977
  return null
988
978
  })
989
979
  }
990
980
 
991
981
  function applyPersonaPersist() {
992
982
  var button = $('personaPersist')
993
- if (!window.confirm('把人设写入 DSH 默认预设并常驻?\n应用后由预设提供人设,插件不再每轮注入人设;可随时恢复原人设。')) return
994
- personaPersistHint('常驻:写入预设中…')
983
+ if (!window.confirm('把人设写入 DSH 默认预设并常驻?应用后由预设提供人设,可随时恢复。')) return
984
+ personaPersistHint('正在写入预设')
995
985
  if (button) button.disabled = true
996
986
  fetch('/api/biocore/persona/apply', {
997
987
  method: 'POST',
@@ -1001,26 +991,23 @@
1001
991
  .then(function (response) { return response.json() })
1002
992
  .then(function (body) {
1003
993
  if (!body || body.ok !== true) {
1004
- personaPersistHint('常驻:应用失败(' + ((body && body.reason) || 'unknown') + ')', 'error')
1005
- if (body && body.detail) personaStatus(String(body.detail).slice(0, 120), 'error')
994
+ personaPersistHint('常驻应用失败 ' + ((body && body.reason) || 'unknown'), 'error')
1006
995
  return body
1007
996
  }
1008
997
  personaStatus('已写入常驻预设', 'ok')
1009
998
  return loadPersonaPersist()
1010
999
  })
1011
1000
  .catch(function () {
1012
- personaPersistHint('常驻:应用失败,连不上宿主', 'error')
1001
+ personaPersistHint('常驻应用失败 连不上宿主', 'error')
1013
1002
  return null
1014
1003
  })
1015
- .then(function () {
1016
- if (button) button.disabled = false
1017
- })
1004
+ .then(function () { if (button) button.disabled = false })
1018
1005
  }
1019
1006
 
1020
1007
  function rollbackPersonaPersist() {
1021
1008
  var button = $('personaRollback')
1022
1009
  if (!window.confirm('恢复应用常驻前的预设人设?')) return
1023
- personaPersistHint('常驻:恢复中…')
1010
+ personaPersistHint('正在恢复')
1024
1011
  if (button) button.disabled = true
1025
1012
  fetch('/api/biocore/persona/apply', {
1026
1013
  method: 'POST',
@@ -1030,19 +1017,17 @@
1030
1017
  .then(function (response) { return response.json() })
1031
1018
  .then(function (body) {
1032
1019
  if (!body || body.ok !== true) {
1033
- personaPersistHint('常驻:恢复失败(' + ((body && body.reason) || 'unknown') + ')', 'error')
1020
+ personaPersistHint('恢复失败 ' + ((body && body.reason) || 'unknown'), 'error')
1034
1021
  return body
1035
1022
  }
1036
1023
  personaStatus('已恢复原预设人设', 'ok')
1037
1024
  return loadPersonaPersist()
1038
1025
  })
1039
1026
  .catch(function () {
1040
- personaPersistHint('常驻:恢复失败,连不上宿主', 'error')
1027
+ personaPersistHint('恢复失败 连不上宿主', 'error')
1041
1028
  return null
1042
1029
  })
1043
- .then(function () {
1044
- if (button) button.disabled = false
1045
- })
1030
+ .then(function () { if (button) button.disabled = false })
1046
1031
  }
1047
1032
 
1048
1033
  function bindPersona() {
@@ -1062,7 +1047,7 @@
1062
1047
  loadPersonaPersist()
1063
1048
  }
1064
1049
 
1065
- var MODEL_DRAFT_KEY = 'biocore-panel-model-draft-v1'
1050
+ /* -------------------------------------------------------- 模型与接口 --- */
1066
1051
 
1067
1052
  function modelInputs() {
1068
1053
  return {
@@ -1070,7 +1055,6 @@
1070
1055
  baseUrl: $('modelBaseUrl'),
1071
1056
  model: $('modelName'),
1072
1057
  apiKey: $('modelApiKey'),
1073
- localPath: $('modelLocalPath'),
1074
1058
  }
1075
1059
  }
1076
1060
 
@@ -1081,7 +1065,6 @@
1081
1065
  base_url: fields.baseUrl ? fields.baseUrl.value.trim() : '',
1082
1066
  model: fields.model ? fields.model.value.trim() : '',
1083
1067
  api_key: fields.apiKey ? fields.apiKey.value : '',
1084
- local_path: fields.localPath ? fields.localPath.value.trim() : '',
1085
1068
  }
1086
1069
  }
1087
1070
 
@@ -1107,16 +1090,6 @@
1107
1090
  }
1108
1091
  }
1109
1092
 
1110
- function fillRerank(value) {
1111
- var fields = rerankInputs()
1112
- var data = value && typeof value === 'object' ? value : {}
1113
- if (fields.enabled) fields.enabled.checked = data.enabled === true
1114
- if (fields.baseUrl) fields.baseUrl.value = String(data.base_url || 'https://api.jina.ai/v1')
1115
- if (fields.model) fields.model.value = String(data.model || 'jina-reranker-v2-base-multilingual')
1116
- if (fields.apiKey && data.api_key) fields.apiKey.value = '••••••••'
1117
- if (fields.topN) fields.topN.value = String(data.top_n || 20)
1118
- }
1119
-
1120
1093
  function fillModel(values) {
1121
1094
  var fields = modelInputs()
1122
1095
  var data = values || {}
@@ -1124,13 +1097,20 @@
1124
1097
  if (fields.baseUrl) fields.baseUrl.value = String(data.base_url || '')
1125
1098
  if (fields.model) fields.model.value = String(data.model || '')
1126
1099
  if (fields.apiKey && data.api_key) fields.apiKey.value = '••••••••'
1127
- if (fields.localPath) fields.localPath.value = String(data.local_path || '')
1128
1100
  }
1129
1101
 
1130
- function setModelStatus(message) {
1131
- setText('modelStatus', message)
1102
+ function fillRerank(value) {
1103
+ var fields = rerankInputs()
1104
+ var data = value && typeof value === 'object' ? value : {}
1105
+ if (fields.enabled) fields.enabled.checked = data.enabled === true
1106
+ if (fields.baseUrl) fields.baseUrl.value = String(data.base_url || 'https://api.jina.ai/v1')
1107
+ if (fields.model) fields.model.value = String(data.model || 'jina-reranker-v2-base-multilingual')
1108
+ if (fields.apiKey && data.api_key) fields.apiKey.value = '••••••••'
1109
+ if (fields.topN) fields.topN.value = String(data.top_n || 20)
1132
1110
  }
1133
1111
 
1112
+ function setModelStatus(message) { setText('modelStatus', message) }
1113
+
1134
1114
  function saveModelDraft(values) {
1135
1115
  try { window.localStorage.setItem(MODEL_DRAFT_KEY, JSON.stringify(values)) } catch { /* ignore */ }
1136
1116
  }
@@ -1139,9 +1119,7 @@
1139
1119
  try {
1140
1120
  var raw = window.localStorage.getItem(MODEL_DRAFT_KEY)
1141
1121
  return raw ? JSON.parse(raw) : null
1142
- } catch {
1143
- return null
1144
- }
1122
+ } catch { return null }
1145
1123
  }
1146
1124
 
1147
1125
  function expressionInputs() {
@@ -1151,7 +1129,6 @@
1151
1129
  target: $('lengthTarget'),
1152
1130
  max: $('lengthMax'),
1153
1131
  status: $('expressionStatus'),
1154
- hint: $('expressionHint'),
1155
1132
  save: $('expressionSave'),
1156
1133
  }
1157
1134
  }
@@ -1161,8 +1138,7 @@
1161
1138
  if (!inputs.toggle) return
1162
1139
  var on = inputs.toggle.checked === true
1163
1140
  if (inputs.fields) inputs.fields.hidden = !on
1164
- if (inputs.status) inputs.status.textContent = on ? '按目标/上限约束' : '长度由人设决定'
1165
- if (inputs.hint) inputs.hint.textContent = on ? '打开后按目标/上限字数约束表达' : '关闭时长度由人设决定'
1141
+ if (inputs.status) inputs.status.textContent = on ? '按目标与上限约束' : '长度由人设决定'
1166
1142
  }
1167
1143
 
1168
1144
  function fillExpression(value) {
@@ -1193,7 +1169,7 @@
1193
1169
  var values = expressionValues()
1194
1170
  if (inputs.save.disabled) return
1195
1171
  inputs.save.disabled = true
1196
- if (inputs.status) inputs.status.textContent = '保存中…'
1172
+ if (inputs.status) inputs.status.textContent = '保存中'
1197
1173
  fetch('/api/biocore/settings', {
1198
1174
  method: 'POST',
1199
1175
  headers: { 'content-type': 'application/json', accept: 'application/json' },
@@ -1206,10 +1182,10 @@
1206
1182
  if (inputs.status) inputs.status.textContent = '已保存,重启后生效'
1207
1183
  return
1208
1184
  }
1209
- if (inputs.status) inputs.status.textContent = '保存失败:' + ((body && body.reason) || 'unknown')
1185
+ if (inputs.status) inputs.status.textContent = '保存失败 ' + ((body && body.reason) || 'unknown')
1210
1186
  })
1211
1187
  .catch(function () {
1212
- if (inputs.status) inputs.status.textContent = '保存失败:连不上宿主'
1188
+ if (inputs.status) inputs.status.textContent = '保存失败 连不上宿主'
1213
1189
  })
1214
1190
  .then(function () { inputs.save.disabled = false })
1215
1191
  })
@@ -1239,40 +1215,31 @@
1239
1215
  })
1240
1216
  }
1241
1217
 
1242
- function bindAdvanced() {
1243
- var reset = $('resetLayout')
1244
- if (reset) reset.addEventListener('click', function () { resetLayout() })
1245
- var clear = $('clearDraft')
1246
- if (clear) clear.addEventListener('click', function () {
1247
- try { window.localStorage.removeItem(MODEL_DRAFT_KEY) } catch { /* ignore */ }
1248
- setText('advancedHint', '已清空本地草稿')
1249
- layoutToast('已清空本地草稿')
1250
- })
1251
- }
1252
-
1253
1218
  function bindModelActions() {
1254
1219
  var button = $('modelSave')
1255
1220
  var test = $('modelTest')
1256
1221
  var rerankTest = $('rerankTest')
1222
+
1257
1223
  function runConnectionTest(kind, buttonEl) {
1258
- setModelStatus((kind === 'rerank' ? 'Rerank ' : 'Embedding ') + '测试中…')
1224
+ setModelStatus((kind === 'rerank' ? 'Rerank ' : 'Embedding ') + '测试中')
1259
1225
  if (buttonEl) buttonEl.disabled = true
1260
1226
  fetch('/api/biocore/settings/test', {
1261
1227
  method: 'POST',
1262
1228
  headers: { 'content-type': 'application/json', accept: 'application/json' },
1263
- body: JSON.stringify({ kind }),
1229
+ body: JSON.stringify({ kind: kind }),
1264
1230
  })
1265
1231
  .then(function (response) { return response.ok ? response.json() : null })
1266
1232
  .then(function (body) {
1267
1233
  if (body && body.ok === true) {
1268
- setModelStatus(kind + ' 连接正常 · ' + (body.elapsed_ms || 0) + 'ms')
1234
+ setModelStatus(kind + ' 正常 ' + (body.elapsed_ms || 0) + 'ms')
1269
1235
  return
1270
1236
  }
1271
- setModelStatus(kind + ' 连接失败:' + ((body && (body.detail || body.reason)) || 'no_response'))
1237
+ setModelStatus(kind + ' 失败 ' + ((body && (body.detail || body.reason)) || 'no_response'))
1272
1238
  })
1273
- .catch(function () { setModelStatus(kind + ' 连接失败:连不上宿主') })
1239
+ .catch(function () { setModelStatus(kind + ' 失败 连不上宿主') })
1274
1240
  .then(function () { if (buttonEl) buttonEl.disabled = false })
1275
1241
  }
1242
+
1276
1243
  if (test) test.addEventListener('click', function () { runConnectionTest('embedding', test) })
1277
1244
  if (rerankTest) rerankTest.addEventListener('click', function () { runConnectionTest('rerank', rerankTest) })
1278
1245
  if (!button) return
@@ -1281,12 +1248,12 @@
1281
1248
  if (values.api_key.indexOf('•') === 0) delete values.api_key
1282
1249
  var rerank = rerankValues()
1283
1250
  if (String(rerank.api_key || '').indexOf('•') === 0) delete rerank.api_key
1284
- setModelStatus('保存中…')
1251
+ setModelStatus('保存中')
1285
1252
  button.disabled = true
1286
1253
  fetch('/api/biocore/settings', {
1287
1254
  method: 'POST',
1288
1255
  headers: { 'content-type': 'application/json', accept: 'application/json' },
1289
- body: JSON.stringify({ confirm: true, embedding: values, rerank }),
1256
+ body: JSON.stringify({ confirm: true, embedding: values, rerank: rerank }),
1290
1257
  })
1291
1258
  .then(function (response) { return response.ok ? response.json() : null })
1292
1259
  .then(function (body) {
@@ -1295,136 +1262,54 @@
1295
1262
  return
1296
1263
  }
1297
1264
  saveModelDraft(values)
1298
- setModelStatus('宿主接口未开启:已存本地草稿')
1265
+ setModelStatus('接口未开启 已存本地草稿')
1299
1266
  })
1300
1267
  .catch(function () {
1301
1268
  saveModelDraft(values)
1302
- setModelStatus('宿主接口未开启:已存本地草稿')
1269
+ setModelStatus('接口未开启 已存本地草稿')
1303
1270
  })
1304
1271
  .then(function () { button.disabled = false })
1305
1272
  })
1306
1273
  }
1307
1274
 
1308
- function loadSwitches() {
1309
- if (!SESSION) return
1310
- fetch('/api/biocore/switch', { headers: { accept: 'application/json' } })
1311
- .then(function (response) { return response.json() })
1312
- .then(function (body) {
1313
- if (body && body.ok === true && body.switches) applySwitchValues(body.switches)
1314
- })
1315
- .catch(function () { /* keep defaults, POST still tries */ })
1316
- }
1317
-
1318
- var TRACE_LIMIT = 3
1319
-
1320
- function traceFormatter() {
1321
- return window.BioTraceView || null
1322
- }
1323
-
1324
- function renderTrace(body) {
1325
- var list = $('traceList')
1326
- var hint = $('traceHint')
1327
- if (!list) return
1328
- list.textContent = ''
1329
- var view = traceFormatter()
1330
- var formatted = view
1331
- ? view.formatPayload(body, TRACE_LIMIT)
1332
- : { rows: [], empty: true, error: false, hint: '还没有记录' }
1333
- setText('traceHint', formatted.hint)
1334
- if (formatted.empty) {
1335
- var empty = document.createElement('p')
1336
- empty.className = 'empty'
1337
- empty.textContent = formatted.error ? '读取失败' : '还没有记录'
1338
- list.appendChild(empty)
1339
- return
1340
- }
1341
- var labels = ['状态变化', '意图', '长度来源', '闸命中', '记忆']
1342
- for (var i = 0; i < formatted.rows.length; i += 1) {
1343
- var row = formatted.rows[i] || {}
1344
- var item = document.createElement('article')
1345
- item.className = 'trace-row'
1346
- var head = document.createElement('div')
1347
- head.className = 'trace-head'
1348
- var time = document.createElement('span')
1349
- time.textContent = row.time || '--:--'
1350
- var badge = document.createElement('span')
1351
- badge.className = 'trace-badge'
1352
- badge.textContent = '#' + (formatted.rows.length - i)
1353
- head.appendChild(time)
1354
- head.appendChild(badge)
1355
- var grid = document.createElement('dl')
1356
- grid.className = 'trace-grid'
1357
- var values = [row.changes, row.intent, row.length, row.gate, row.memory]
1358
- for (var index = 0; index < labels.length; index += 1) {
1359
- var dt = document.createElement('dt')
1360
- dt.textContent = labels[index]
1361
- var dd = document.createElement('dd')
1362
- dd.textContent = values[index] || '—'
1363
- grid.appendChild(dt)
1364
- grid.appendChild(dd)
1365
- }
1366
- item.appendChild(head)
1367
- item.appendChild(grid)
1368
- list.appendChild(item)
1369
- }
1370
- }
1371
-
1372
- function fetchTrace() {
1373
- if (!SESSION) {
1374
- renderTrace({ ok: true, rows: [] })
1375
- return Promise.resolve(null)
1376
- }
1377
- return fetch('/api/biocore/trace?session=' + encodeURIComponent(SESSION) + '&limit=' + TRACE_LIMIT, {
1378
- headers: { accept: 'application/json' },
1275
+ function bindAdvanced() {
1276
+ var reset = $('resetLayout')
1277
+ if (reset) reset.addEventListener('click', function () { resetLayout() })
1278
+ var clear = $('clearDraft')
1279
+ if (clear) clear.addEventListener('click', function () {
1280
+ try { window.localStorage.removeItem(MODEL_DRAFT_KEY) } catch { /* ignore */ }
1281
+ setText('advancedHint', '草稿已清空')
1282
+ layoutToast('已清空本地草稿')
1379
1283
  })
1380
- .then(function (response) { return response.json() })
1381
- .then(function (body) {
1382
- renderTrace(body)
1383
- return body
1384
- })
1385
- .catch(function () {
1386
- renderTrace(null)
1387
- return null
1388
- })
1389
1284
  }
1390
1285
 
1391
- var PAGE_START = Date.now()
1286
+ /* ------------------------------------------------------------ 布局/编辑 --- */
1287
+
1392
1288
  var layoutApi = typeof window !== 'undefined' ? window.BioCorePanelLayout : null
1393
1289
  var layoutState = null
1394
1290
  var layoutEditing = false
1395
- var toastTimer = null
1291
+ var activeTab = 'dashboard'
1396
1292
  var drag = null
1397
1293
  var pressTimer = null
1398
1294
  var pressStart = null
1399
- var LONG_PRESS_MS = 400
1400
- var ENTRY_LONG_PRESS_MS = 600
1401
- var MOVE_CANCEL_PX = 8
1402
- var entryTimer = null
1403
- var entryStart = null
1404
1295
 
1405
1296
  function layoutStorage() {
1406
1297
  try { return window.localStorage } catch { return null }
1407
1298
  }
1408
1299
 
1409
1300
  function layoutCards() {
1410
- var container = $('cards')
1301
+ var container = $('grid')
1411
1302
  if (!container) return []
1412
1303
  return Array.prototype.slice.call(container.querySelectorAll('[data-card-id]'))
1413
1304
  }
1414
1305
 
1415
- /** Movable DOM slot for a card: grouped cards share one slot (channel grid). */
1416
- function layoutSlotOf(card) {
1417
- var slot = card && card.closest ? card.closest('[data-layout-slot]') : null
1418
- return slot || card
1419
- }
1420
-
1421
1306
  function layoutToast(message) {
1422
1307
  var el = $('layoutToast')
1423
1308
  if (!el) return
1424
1309
  el.textContent = String(message || '')
1425
1310
  el.hidden = false
1426
1311
  if (toastTimer) window.clearTimeout(toastTimer)
1427
- toastTimer = window.setTimeout(function () { el.hidden = true }, 2600)
1312
+ toastTimer = window.setTimeout(function () { el.hidden = true }, 2400)
1428
1313
  }
1429
1314
 
1430
1315
  function ensureHideButton(card) {
@@ -1438,22 +1323,24 @@
1438
1323
  card.appendChild(hide)
1439
1324
  }
1440
1325
 
1441
- var activeTab = 'dashboard'
1326
+ function cardLabel(card) {
1327
+ var named = card.getAttribute('data-card-label')
1328
+ if (named) return named
1329
+ var title = card.querySelector('h2')
1330
+ return title ? title.textContent.trim() : card.getAttribute('data-card-id')
1331
+ }
1442
1332
 
1443
1333
  function applyTabGroup() {
1444
1334
  if (!layoutApi) return
1335
+ var body = document.body
1336
+ body.dataset.tab = activeTab
1337
+ body.classList.toggle('layout-editing', layoutEditing)
1338
+ setText('pageTitle', layoutApi.TAB_LABELS[activeTab] || 'BioCore')
1445
1339
  var group = layoutEditing ? null : (layoutApi.TAB_GROUPS[activeTab] || [])
1446
- var panel = $('panel')
1447
- if (panel) {
1448
- var tabsKnown = ['dashboard', 'memory', 'relation', 'settings', 'tools']
1449
- for (var t = 0; t < tabsKnown.length; t += 1) {
1450
- panel.classList.toggle('tab-' + tabsKnown[t], activeTab === tabsKnown[t])
1451
- }
1452
- }
1453
1340
  var cards = layoutCards()
1454
1341
  for (var i = 0; i < cards.length; i += 1) {
1455
1342
  var id = cards[i].getAttribute('data-card-id')
1456
- cards[i].classList.toggle('tab-hidden', !!group && group.indexOf(id) < 0)
1343
+ cards[i].classList.toggle('tab-off', !!group && group.indexOf(id) < 0)
1457
1344
  }
1458
1345
  var items = document.querySelectorAll('[data-tab]')
1459
1346
  for (var j = 0; j < items.length; j += 1) {
@@ -1467,6 +1354,8 @@
1467
1354
  if (!layoutApi || !layoutApi.TAB_GROUPS[tab]) return
1468
1355
  activeTab = tab
1469
1356
  applyTabGroup()
1357
+ if (tab === 'tools') loadStats()
1358
+ if (layoutEditing) renderAddCards()
1470
1359
  }
1471
1360
 
1472
1361
  function bindTabBar() {
@@ -1484,7 +1373,7 @@
1484
1373
  if (!layoutApi) return
1485
1374
  layoutState = layoutApi.normalizeLayout(next)
1486
1375
  if (persist !== false) layoutApi.writeLayout(layoutStorage(), layoutState)
1487
- var container = $('cards')
1376
+ var container = $('grid')
1488
1377
  if (!container) return
1489
1378
  var cards = layoutCards()
1490
1379
  var byId = {}
@@ -1494,14 +1383,11 @@
1494
1383
  byId[card.getAttribute('data-card-id')] = card
1495
1384
  }
1496
1385
  var ordered = layoutApi.sortCards(Object.keys(byId), layoutState.order)
1497
- var orderedSlots = []
1498
1386
  for (var j = 0; j < ordered.length; j += 1) {
1499
- var id = ordered[j]
1500
- var node = byId[id]
1387
+ var node = byId[ordered[j]]
1501
1388
  if (!node) continue
1502
- node.classList.toggle('is-hidden', layoutState.hidden.indexOf(id) >= 0)
1503
- var slot = layoutSlotOf(node)
1504
- if (orderedSlots.indexOf(slot) < 0) { orderedSlots.push(slot); container.appendChild(slot) }
1389
+ node.classList.toggle('is-hidden', layoutState.hidden.indexOf(ordered[j]) >= 0)
1390
+ container.appendChild(node)
1505
1391
  }
1506
1392
  renderAddCards()
1507
1393
  applyTabGroup()
@@ -1519,41 +1405,6 @@
1519
1405
  layoutApi.writeLayout(layoutStorage(), layoutState)
1520
1406
  }
1521
1407
 
1522
- function setLayoutEditing(on) {
1523
- layoutEditing = !!on
1524
- var panel = $('panel')
1525
- if (panel) panel.classList.toggle('layout-editing', layoutEditing)
1526
- var title = $('pageTitle')
1527
- if (title) title.textContent = layoutEditing ? '编辑仪表盘' : 'BioCore'
1528
- var fab = $('layoutFab')
1529
- if (fab) fab.hidden = layoutEditing
1530
- var done = $('layoutDone')
1531
- if (done) done.hidden = !layoutEditing
1532
- var reset = $('layoutReset')
1533
- if (reset) reset.hidden = !layoutEditing
1534
- var cards = layoutCards()
1535
- for (var i = 0; i < cards.length; i += 1) ensureHideButton(cards[i])
1536
- ensureDeskHides()
1537
- applyDeskHidden()
1538
- renderAddCards()
1539
- applyTabGroup()
1540
- if (!layoutEditing) persistDomOrder()
1541
- }
1542
-
1543
- function resetLayout(message) {
1544
- if (layoutApi) layoutApi.clearLayout(layoutStorage())
1545
- layoutState = layoutApi ? layoutApi.defaultLayout() : null
1546
- applyLayout(layoutState, false)
1547
- layoutToast(message || '已恢复默认布局')
1548
- }
1549
-
1550
- function cardLabel(card) {
1551
- var named = card.getAttribute('data-card-label')
1552
- if (named) return named
1553
- var title = card.querySelector('h1, h2')
1554
- return title ? title.textContent.trim() : card.getAttribute('data-card-id')
1555
- }
1556
-
1557
1408
  function hiddenCards() {
1558
1409
  var out = []
1559
1410
  if (!layoutApi || !layoutState) return out
@@ -1571,17 +1422,8 @@
1571
1422
  var section = $('addCards')
1572
1423
  var list = $('addList')
1573
1424
  if (!section || !list) return
1574
- if (!layoutEditing) {
1575
- section.hidden = true
1576
- list.textContent = ''
1577
- return
1578
- }
1579
- var hidden = hiddenCards()
1580
- var deskHidden = readDeskHidden()
1581
- var deskHiddenCards = deskCards().filter(function (card, index) {
1582
- return deskHidden.indexOf(deskCardId(card, index)) >= 0
1583
- })
1584
- if (!hidden.length && !deskHiddenCards.length) {
1425
+ var hidden = layoutEditing ? hiddenCards() : []
1426
+ if (!hidden.length) {
1585
1427
  section.hidden = true
1586
1428
  list.textContent = ''
1587
1429
  return
@@ -1596,14 +1438,6 @@
1596
1438
  chip.textContent = '+ ' + hidden[i].label
1597
1439
  list.appendChild(chip)
1598
1440
  }
1599
- for (var d = 0; d < deskHiddenCards.length; d += 1) {
1600
- var deskChip = document.createElement('button')
1601
- deskChip.type = 'button'
1602
- deskChip.className = 'add-chip'
1603
- deskChip.setAttribute('data-desk-add', deskCardId(deskHiddenCards[d], 0))
1604
- deskChip.textContent = '+ ' + deskLabel(deskHiddenCards[d])
1605
- list.appendChild(deskChip)
1606
- }
1607
1441
  }
1608
1442
 
1609
1443
  function showHiddenCard(id) {
@@ -1615,59 +1449,64 @@
1615
1449
  for (var i = 0; i < cards.length; i += 1) {
1616
1450
  if (cards[i].getAttribute('data-card-id') === id) label = cardLabel(cards[i])
1617
1451
  }
1618
- layoutToast('已添加「' + (label || id) + '」')
1452
+ layoutToast('已恢复「' + (label || id) + '」')
1619
1453
  }
1620
1454
 
1621
- function cancelEntryPress() {
1622
- if (entryTimer) {
1623
- window.clearTimeout(entryTimer)
1624
- entryTimer = null
1625
- }
1626
- entryStart = null
1455
+ function setLayoutEditing(on) {
1456
+ layoutEditing = !!on
1457
+ var fab = $('layoutFab')
1458
+ if (fab) fab.hidden = layoutEditing
1459
+ var done = $('layoutDone')
1460
+ if (done) done.hidden = !layoutEditing
1461
+ var reset = $('layoutReset')
1462
+ if (reset) reset.hidden = !layoutEditing
1463
+ var cards = layoutCards()
1464
+ for (var i = 0; i < cards.length; i += 1) ensureHideButton(cards[i])
1465
+ renderAddCards()
1466
+ applyTabGroup()
1467
+ if (layoutEditing) layoutToast('编辑模式:拖动排序,红色 − 隐藏卡片')
1468
+ else persistDomOrder()
1627
1469
  }
1628
1470
 
1629
- function onPagePointerDown(event) {
1630
- if (layoutEditing) return
1631
- if (event.button !== undefined && event.button !== 0) return
1632
- var target = event.target
1633
- if (target && target.closest
1634
- && target.closest('input, textarea, button, select, a, [contenteditable]')) return
1635
- entryStart = { x: event.clientX, y: event.clientY }
1636
- if (entryTimer) window.clearTimeout(entryTimer)
1637
- entryTimer = window.setTimeout(function () {
1638
- entryTimer = null
1639
- if (!entryStart) return
1640
- entryStart = null
1641
- setLayoutEditing(true)
1642
- layoutToast('已进入编辑模式,长按卡片可拖动')
1643
- }, ENTRY_LONG_PRESS_MS)
1644
- }
1645
-
1646
- function beginDrag(card, event) {
1471
+ function resetLayout(message) {
1472
+ if (layoutApi) layoutApi.clearLayout(layoutStorage())
1473
+ layoutState = layoutApi ? layoutApi.defaultLayout() : null
1474
+ applyLayout(layoutState, false)
1475
+ layoutToast(message || '已恢复默认布局')
1476
+ }
1477
+
1478
+ /* --- drag & drop ------------------------------------------------------- */
1479
+
1480
+ function beginDrag(card, pointerId) {
1647
1481
  if (!layoutEditing || !layoutApi || !card) return
1648
- drag = { card: card, pointerId: event.pointerId }
1482
+ drag = { card: card, pointerId: pointerId }
1649
1483
  card.classList.add('dragging')
1650
- try { if (card.setPointerCapture) card.setPointerCapture(event.pointerId) } catch { /* ignore */ }
1484
+ try { if (card.setPointerCapture) card.setPointerCapture(pointerId) } catch { /* ignore */ }
1651
1485
  }
1652
1486
 
1653
1487
  function updateDrag(event) {
1654
1488
  if (!drag || event.pointerId !== drag.pointerId) return
1489
+ var container = $('grid')
1490
+ if (!container) return
1655
1491
  var cards = layoutCards()
1492
+ var x = event.clientX
1656
1493
  var y = event.clientY
1494
+ var reference = null
1495
+ var last = null
1657
1496
  for (var i = 0; i < cards.length; i += 1) {
1658
1497
  var candidate = cards[i]
1659
- if (candidate === drag.card || candidate.classList.contains('is-hidden')) continue
1498
+ if (candidate === drag.card) continue
1499
+ if (candidate.classList.contains('tab-off') || candidate.classList.contains('is-hidden')) continue
1660
1500
  var rect = candidate.getBoundingClientRect()
1661
- if (y >= rect.top && y <= rect.bottom) {
1662
- var after = y > rect.top + rect.height / 2
1663
- var reference = after ? candidate.nextSibling : candidate
1664
- if (reference === drag.card) return
1665
- if (!after && candidate.previousSibling === drag.card) return
1666
- var container = $('cards')
1667
- if (container) container.insertBefore(drag.card, reference)
1668
- return
1669
- }
1501
+ last = rect
1502
+ if (y < rect.top) { reference = candidate; break }
1503
+ if (y <= rect.bottom && x < rect.left + rect.width / 2) { reference = candidate; break }
1504
+ }
1505
+ if (reference) {
1506
+ container.insertBefore(drag.card, reference)
1507
+ return
1670
1508
  }
1509
+ if (last && y > last.bottom) container.appendChild(drag.card)
1671
1510
  }
1672
1511
 
1673
1512
  function endDrag() {
@@ -1679,41 +1518,50 @@
1679
1518
  persistDomOrder()
1680
1519
  }
1681
1520
 
1682
- function onCardsPointerDown(event) {
1521
+ function onGridPointerDown(event) {
1683
1522
  if (!layoutEditing) return
1684
1523
  var target = event.target
1685
- var card = target && target.closest ? target.closest('[data-card-id]') : null
1686
- if (!card) return
1524
+ if (!target || !target.closest) return
1687
1525
  if (target.closest('[data-layout-hide]')) return
1526
+ var card = target.closest('[data-card-id]')
1527
+ if (!card) return
1688
1528
  var intent = layoutApi && layoutApi.dragIntent
1689
1529
  ? layoutApi.dragIntent({ editing: layoutEditing, pointerType: event.pointerType })
1690
1530
  : { hold: true }
1691
1531
  if (!intent.hold) return
1692
1532
  if (event.cancelable) event.preventDefault()
1693
- pressStart = { x: event.clientX, y: event.clientY, card: card, pointerId: event.pointerId }
1533
+ pressStart = {
1534
+ x: event.clientX,
1535
+ y: event.clientY,
1536
+ card: card,
1537
+ pointerId: event.pointerId,
1538
+ touch: event.pointerType !== 'mouse',
1539
+ }
1694
1540
  if (pressTimer) window.clearTimeout(pressTimer)
1695
- pressTimer = window.setTimeout(function () {
1696
- pressTimer = null
1697
- if (!pressStart) return
1698
- var pending = pressStart
1699
- pressStart = null
1700
- beginDrag(pending.card, { pointerId: pending.pointerId })
1701
- }, LONG_PRESS_MS)
1702
- }
1703
-
1704
- function onCardsPointerMove(event) {
1705
- if (entryTimer && entryStart) {
1706
- var edx = event.clientX - entryStart.x
1707
- var edy = event.clientY - entryStart.y
1708
- if (edx * edx + edy * edy > MOVE_CANCEL_PX * MOVE_CANCEL_PX) cancelEntryPress()
1541
+ if (pressStart.touch) {
1542
+ pressTimer = window.setTimeout(function () {
1543
+ pressTimer = null
1544
+ if (!pressStart) return
1545
+ var pending = pressStart
1546
+ pressStart = null
1547
+ beginDrag(pending.card, pending.pointerId)
1548
+ }, LONG_PRESS_MS)
1709
1549
  }
1710
- if (pressTimer && pressStart) {
1550
+ }
1551
+
1552
+ function onDocPointerMove(event) {
1553
+ if (pressStart && !drag) {
1711
1554
  var dx = event.clientX - pressStart.x
1712
1555
  var dy = event.clientY - pressStart.y
1713
1556
  if (dx * dx + dy * dy > MOVE_CANCEL_PX * MOVE_CANCEL_PX) {
1714
- window.clearTimeout(pressTimer)
1715
- pressTimer = null
1557
+ if (pressTimer) { window.clearTimeout(pressTimer); pressTimer = null }
1558
+ if (pressStart.touch) {
1559
+ pressStart = null
1560
+ return
1561
+ }
1562
+ var pending = pressStart
1716
1563
  pressStart = null
1564
+ beginDrag(pending.card, pending.pointerId)
1717
1565
  }
1718
1566
  return
1719
1567
  }
@@ -1723,25 +1571,22 @@
1723
1571
  }
1724
1572
  }
1725
1573
 
1726
- function onCardsPointerUp() {
1727
- cancelEntryPress()
1728
- if (pressTimer) {
1729
- window.clearTimeout(pressTimer)
1730
- pressTimer = null
1731
- }
1574
+ function onDocPointerUp() {
1575
+ if (pressTimer) { window.clearTimeout(pressTimer); pressTimer = null }
1732
1576
  pressStart = null
1733
1577
  endDrag()
1734
1578
  }
1735
1579
 
1736
- function onCardsClick(event) {
1580
+ function onGridClick(event) {
1737
1581
  var target = event.target
1738
- var add = target && target.closest ? target.closest('[data-add-card]') : null
1582
+ if (!target || !target.closest) return
1583
+ var add = target.closest('[data-add-card]')
1739
1584
  if (add) {
1740
1585
  event.preventDefault()
1741
1586
  showHiddenCard(add.getAttribute('data-add-card'))
1742
1587
  return
1743
1588
  }
1744
- var hide = target && target.closest ? target.closest('[data-layout-hide]') : null
1589
+ var hide = target.closest('[data-layout-hide]')
1745
1590
  if (!hide) return
1746
1591
  event.preventDefault()
1747
1592
  var card = hide.closest('[data-card-id]')
@@ -1753,7 +1598,7 @@
1753
1598
  return
1754
1599
  }
1755
1600
  applyLayout(next)
1756
- layoutToast('已隐藏「' + cardLabel(card) + '」,可在「添加卡片」恢复')
1601
+ layoutToast('已隐藏「' + cardLabel(card) + '」')
1757
1602
  }
1758
1603
 
1759
1604
  function initLayout() {
@@ -1766,23 +1611,48 @@
1766
1611
  if (fab) fab.addEventListener('click', function () { setLayoutEditing(true) })
1767
1612
  if (done) done.addEventListener('click', function () { setLayoutEditing(false) })
1768
1613
  if (reset) reset.addEventListener('click', function () { resetLayout() })
1769
- var cards = $('cards')
1770
- if (cards) {
1771
- cards.addEventListener('pointerdown', onCardsPointerDown)
1772
- cards.addEventListener('click', onCardsClick)
1773
- }
1614
+ var grid = $('grid')
1615
+ if (grid) grid.addEventListener('pointerdown', onGridPointerDown)
1616
+ document.addEventListener('click', onGridClick)
1774
1617
  bindTabBar()
1775
- document.addEventListener('pointerdown', onPagePointerDown)
1776
- document.addEventListener('pointermove', onCardsPointerMove, { passive: false })
1777
- document.addEventListener('pointerup', onCardsPointerUp)
1778
- document.addEventListener('pointercancel', onCardsPointerUp)
1779
- ensureDeskHides()
1780
- applyDeskHidden()
1781
- document.addEventListener('click', onDeskEditClick)
1618
+ document.addEventListener('pointermove', onDocPointerMove, { passive: false })
1619
+ document.addEventListener('pointerup', onDocPointerUp)
1620
+ document.addEventListener('pointercancel', onDocPointerUp)
1621
+ }
1622
+
1623
+ /* ---------------------------------------------------------------- 侧栏 --- */
1624
+
1625
+ function railApply(collapsed) {
1626
+ document.body.classList.toggle('rail-collapsed', !!collapsed)
1627
+ var button = $('railToggle')
1628
+ if (!button) return
1629
+ var label = collapsed ? '展开导航' : '收起导航'
1630
+ button.setAttribute('aria-expanded', String(!collapsed))
1631
+ button.setAttribute('aria-label', label)
1632
+ button.setAttribute('title', label)
1633
+ }
1634
+
1635
+ function railInit() {
1636
+ var storage = layoutStorage()
1637
+ var key = layoutApi && layoutApi.RAIL_KEY ? layoutApi.RAIL_KEY : 'biocore-panel-rail-v1'
1638
+ var collapsed = false
1639
+ try { collapsed = !!storage && storage.getItem(key) === '1' } catch { collapsed = false }
1640
+ railApply(collapsed)
1641
+ var button = $('railToggle')
1642
+ if (!button) return
1643
+ button.addEventListener('click', function () {
1644
+ var next = !document.body.classList.contains('rail-collapsed')
1645
+ railApply(next)
1646
+ try { if (storage) storage.setItem(key, next ? '1' : '0') } catch { /* ignore */ }
1647
+ })
1782
1648
  }
1783
1649
 
1650
+ /* ---------------------------------------------------------------- boot --- */
1651
+
1784
1652
  function boot() {
1653
+ applyTheme()
1785
1654
  railInit()
1655
+ if (layoutApi && layoutApi.TAB_GROUPS[INITIAL_TAB]) activeTab = INITIAL_TAB
1786
1656
  initLayout()
1787
1657
  bindAdvanced()
1788
1658
  bindPersona()
@@ -1791,17 +1661,21 @@
1791
1661
  bindExpression()
1792
1662
  loadModelSettings()
1793
1663
  buildSwitches()
1664
+
1794
1665
  if (!SESSION) {
1795
1666
  setStatus(false, '未连接')
1796
- renderEmpty('request_failed')
1797
- setText('chartHint', '无数据')
1667
+ render(null)
1668
+ setText('motivation', '没有会话参数,先在聊天会话里打开')
1798
1669
  renderTrace({ ok: true, rows: [] })
1670
+ window.setTimeout(finishBoot, 600)
1799
1671
  return
1800
1672
  }
1801
1673
  loadSwitches()
1802
1674
  loadSeries()
1803
1675
  fetchState()
1804
1676
  fetchTrace()
1677
+ loadStats()
1678
+
1805
1679
  function pollTick() {
1806
1680
  if (document.hidden) return
1807
1681
  fetchState()
@@ -1809,17 +1683,17 @@
1809
1683
  }
1810
1684
  window.setInterval(pollTick, POLL_MS)
1811
1685
  window.setInterval(updateStatusClock, 1000)
1686
+ window.setInterval(loadStats, STATS_MS)
1812
1687
  document.addEventListener('visibilitychange', function () {
1813
1688
  if (document.hidden) return
1814
1689
  fetchState()
1815
1690
  fetchTrace()
1691
+ loadStats()
1816
1692
  })
1817
1693
  window.addEventListener('resize', function () { renderChart(lastData) })
1694
+ window.setTimeout(finishBoot, 3000)
1818
1695
  }
1819
1696
 
1820
- if (document.readyState === 'loading') {
1821
- document.addEventListener('DOMContentLoaded', boot)
1822
- } else {
1823
- boot()
1824
- }
1825
- })();
1697
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot)
1698
+ else boot()
1699
+ })();