dsh-biocore 0.1.8 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-biocore",
3
- "version": "0.1.8",
3
+ "version": "0.1.91",
4
4
  "type": "module",
5
5
  "description": "BioCore DSH UI: per-session state tab, standalone instrument panel, /api/biocore/state, session/event one_turn updates, /bio sync.",
6
6
  "main": "lib/index.js",
@@ -10,15 +10,17 @@
10
10
  var STORAGE_KEY = 'biocore-panel-layout-v2'
11
11
  var DEFAULT_ORDER = [
12
12
  'mood', 'tension', 'warmth', 'heaviness', 'clarity', 'chart',
13
- 'memory', 'memory-summary', 'metrics', 'trace', 'runtime',
13
+ 'memory', 'memory-summary', 'metrics',
14
14
  'persona', 'model', 'switches', 'expression', 'advanced',
15
+ 'runtime', 'trace',
15
16
  ]
16
- var TAB_ORDER = ['dashboard', 'memory', 'settings', 'diagnosis']
17
+ var TAB_ORDER = ['dashboard', 'memory', 'relation', 'settings', 'tools']
17
18
  var TAB_GROUPS = Object.freeze({
18
19
  dashboard: ['mood', 'tension', 'warmth', 'heaviness', 'clarity', 'chart'],
19
20
  memory: ['memory', 'memory-summary'],
21
+ relation: ['metrics'],
20
22
  settings: ['persona', 'model', 'switches', 'expression', 'advanced'],
21
- diagnosis: ['metrics', 'trace', 'runtime'],
23
+ tools: ['runtime', 'trace'],
22
24
  })
23
25
 
24
26
  function defaultLayout() {
package/ui/panel.css CHANGED
@@ -1,4 +1,4 @@
1
- /* BioCore instrument panel — FlClash mobile spec. | layout v24
1
+ /* BioCore instrument panel — FlClash mobile spec. | layout v27
2
2
  1) tokens 2) base 3) topbar 4) cards 5) figures 6) lists 7) forms
3
3
  8) switches 9) nav 10) edit mode 11) desktop.
4
4
  Rule: one component rule per selector, px values from docs/DSH-PANEL-FLCLASH-SPEC.md. */
@@ -499,6 +499,9 @@ canvas#moodChart { display: block; width: 100%; height: 150px; }
499
499
  .expression-fields { grid-template-columns: 1fr; }
500
500
  }
501
501
 
502
+ /* bug fix: whole channels block (title included) leaves non-dashboard tabs */
503
+ .panel:not(.tab-dashboard):not(.layout-editing) .channels-wrap { display: none; }
504
+
502
505
  /* ---------- 13. layout guards (防止旧版/串版样式压扁版面) ---------- */
503
506
  html, body { overflow-x: clip; }
504
507
  body { max-width: 100%; }
@@ -556,3 +559,275 @@ body { max-width: 100%; }
556
559
  .model-card .model-section-title:first-of-type { margin-top: 0; }
557
560
  .rerank-toggle { margin: 0 0 10px; }
558
561
  .rerank-grid[hidden] { display: none !important; }
562
+
563
+ /* ---------- 15. desktop (>=1024): left rail + 12-col dashboard ---------- */
564
+ .rail, .desk { display: none; }
565
+
566
+ @media (min-width: 1024px) {
567
+ :root {
568
+ --rail-w: 220px;
569
+ --radius-card: 32px;
570
+ --pad-card: 20px;
571
+ --gap-card: 12px;
572
+ }
573
+ body { padding: 0; }
574
+ body.rail-collapsed { --rail-w: 64px; }
575
+
576
+ .panel {
577
+ width: auto;
578
+ max-width: none;
579
+ min-height: 100vh;
580
+ margin: 0 0 0 var(--rail-w);
581
+ padding: 16px 24px 24px 16px;
582
+ display: grid;
583
+ grid-template-columns: repeat(12, minmax(0, 1fr));
584
+ gap: 12px;
585
+ align-content: start;
586
+ transition: margin-left 160ms ease;
587
+ }
588
+
589
+ /* --- left rail --- */
590
+ .rail {
591
+ position: fixed;
592
+ inset: 0 auto 0 0;
593
+ z-index: 40;
594
+ width: var(--rail-w);
595
+ display: flex;
596
+ flex-direction: column;
597
+ gap: 6px;
598
+ padding: 12px 10px;
599
+ background: var(--page);
600
+ border-right: 1px solid var(--line);
601
+ overflow: hidden;
602
+ transition: width 160ms ease, padding 160ms ease;
603
+ }
604
+ .rail-toggle {
605
+ display: grid;
606
+ place-items: center;
607
+ width: 40px;
608
+ height: 40px;
609
+ margin: 0 0 8px 2px;
610
+ padding: 0;
611
+ border: 0;
612
+ border-radius: 10px;
613
+ background: transparent;
614
+ color: var(--text-2);
615
+ cursor: pointer;
616
+ transition: background 160ms ease, color 160ms ease;
617
+ }
618
+ .rail-toggle:hover { background: rgba(255, 255, 255, 0.04); color: var(--text); }
619
+ .rail-toggle svg { width: 20px; height: 20px; }
620
+ .rail-nav { display: flex; flex-direction: column; gap: 2px; }
621
+ .rail-item {
622
+ position: relative;
623
+ display: flex;
624
+ align-items: center;
625
+ gap: 10px;
626
+ height: 40px;
627
+ padding: 0 12px;
628
+ border: 0;
629
+ border-radius: 8px;
630
+ background: transparent;
631
+ color: var(--text-2);
632
+ font: inherit;
633
+ font-size: 14px;
634
+ line-height: 20px;
635
+ text-align: left;
636
+ white-space: nowrap;
637
+ overflow: hidden;
638
+ cursor: pointer;
639
+ transition: background 160ms ease, color 160ms ease;
640
+ }
641
+ .rail-item svg { flex: 0 0 20px; width: 20px; height: 20px; }
642
+ .rail-item:hover { background: rgba(255, 255, 255, 0.04); color: var(--text); }
643
+ .rail-item.active { background: rgba(255, 255, 255, 0.06); color: var(--text); }
644
+ .rail-item.active::before {
645
+ content: '';
646
+ position: absolute;
647
+ left: 0;
648
+ top: 8px;
649
+ bottom: 8px;
650
+ width: 2px;
651
+ border-radius: 2px;
652
+ background: var(--accent);
653
+ }
654
+ body.rail-collapsed .rail-item { justify-content: center; padding: 0; }
655
+ body.rail-collapsed .rail-item span { display: none; }
656
+
657
+ /* --- header --- */
658
+ .topbar { position: static; height: 56px; padding: 0; grid-column: 1 / -1; }
659
+ .topbar h1 { font-size: 22px; line-height: 28px; }
660
+ .masthead { display: none; }
661
+ .tabbar { display: none; }
662
+ .footer { grid-column: 1 / -1; padding: 0 4px; }
663
+
664
+ /* --- desktop dashboard: 12 col x 5 rows --- */
665
+ .desk {
666
+ display: grid;
667
+ grid-column: 1 / -1;
668
+ grid-template-columns: repeat(12, minmax(0, 1fr));
669
+ grid-template-rows: 208px 64px 176px minmax(200px, auto) minmax(120px, auto);
670
+ gap: 12px;
671
+ }
672
+ .desk-card {
673
+ position: relative;
674
+ min-width: 0;
675
+ padding: 20px;
676
+ border-radius: 32px;
677
+ overflow: hidden;
678
+ }
679
+ .desk-card-head {
680
+ display: flex;
681
+ align-items: baseline;
682
+ justify-content: space-between;
683
+ gap: 8px;
684
+ margin-bottom: 10px;
685
+ }
686
+ .desk-card-head h2 { margin: 0; font-size: 15px; line-height: 24px; font-weight: 600; }
687
+ .desk-status, .desk-seg, .desk-memory, .desk-chart { grid-column: span 6; }
688
+ .desk-metric, .desk-switch, .desk-tile { grid-column: span 3; }
689
+ .desk-stat { grid-column: span 4; }
690
+
691
+ .desk-status { display: flex; align-items: center; gap: 20px; }
692
+ .desk-orb { width: 88px; height: 88px; flex: 0 0 88px; display: grid; place-items: center; }
693
+ .desk-status-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
694
+ .desk-status-body h2 { margin: 0; font-size: 22px; line-height: 28px; font-weight: 600; }
695
+ .desk-status-body .motivation { margin: 0; color: var(--text-2); font-size: 15px; line-height: 22px; }
696
+
697
+ .desk-metric { display: flex; flex-direction: column; justify-content: center; gap: 10px; }
698
+ .desk-metric-head {
699
+ display: flex;
700
+ align-items: baseline;
701
+ justify-content: space-between;
702
+ gap: 8px;
703
+ color: var(--text-2);
704
+ font-size: 13px;
705
+ line-height: 18px;
706
+ }
707
+ .desk-metric-value { display: flex; align-items: baseline; gap: 10px; }
708
+ .desk-metric-value strong {
709
+ font-size: 40px;
710
+ line-height: 44px;
711
+ font-weight: 700;
712
+ letter-spacing: -0.02em;
713
+ font-variant-numeric: tabular-nums;
714
+ }
715
+ .desk-metric-value .sub-status { margin-left: auto; color: var(--text-2); font-size: 13px; }
716
+
717
+ .desk-seg { display: flex; align-items: center; gap: 6px; padding: 6px; border-radius: 32px; background: var(--tone); box-shadow: none; }
718
+ .seg-item {
719
+ flex: 1 1 0;
720
+ height: 52px;
721
+ border: 0;
722
+ border-radius: 16px;
723
+ background: transparent;
724
+ color: var(--text-2);
725
+ font: inherit;
726
+ font-size: 14px;
727
+ cursor: pointer;
728
+ transition: background 160ms ease, color 160ms ease;
729
+ }
730
+ .seg-item:hover { background: rgba(255, 255, 255, 0.04); color: var(--text); }
731
+ .seg-item.active { background: var(--accent); color: #ffffff; }
732
+
733
+ .desk-switch { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
734
+ .desk-switch-label { font-size: 15px; line-height: 24px; font-weight: 600; }
735
+ .desk-switch-control { display: flex; align-items: center; gap: 8px; }
736
+ .desk-switch-control input { width: 22px; height: 22px; accent-color: var(--accent); }
737
+ .desk-switch-state { color: var(--text-3); font-size: 12px; line-height: 16px; }
738
+
739
+ .desk-tile { display: flex; flex-direction: column; justify-content: center; gap: 8px; }
740
+ .desk-tile-head {
741
+ display: flex;
742
+ align-items: center;
743
+ justify-content: space-between;
744
+ gap: 8px;
745
+ color: var(--text-2);
746
+ font-size: 12px;
747
+ line-height: 16px;
748
+ }
749
+ .desk-tile strong {
750
+ font-size: 22px;
751
+ line-height: 28px;
752
+ font-weight: 600;
753
+ letter-spacing: -0.01em;
754
+ overflow: hidden;
755
+ text-overflow: ellipsis;
756
+ white-space: nowrap;
757
+ }
758
+ .desk-tile-note { color: var(--text-3); font-size: 12px; line-height: 16px; }
759
+ .desk-tile-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; color: var(--text-2); font-size: 13px; line-height: 18px; }
760
+ .desk-tile-row b { color: var(--text); font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
761
+ .desk-chevron { color: var(--text-3); font-size: 14px; }
762
+ .desk-copy {
763
+ height: 24px;
764
+ padding: 0 10px;
765
+ border: 0;
766
+ border-radius: 8px;
767
+ background: var(--block);
768
+ color: var(--text-2);
769
+ font: inherit;
770
+ font-size: 12px;
771
+ cursor: pointer;
772
+ }
773
+
774
+ .desk-memory { display: flex; flex-direction: column; }
775
+ .desk-memory .memory-list { max-height: none; }
776
+ .desk-chart { display: flex; flex-direction: column; }
777
+ #deskChart { width: 100%; height: 100%; min-height: 110px; display: block; }
778
+
779
+ .desk-stat { display: flex; flex-direction: column; justify-content: center; gap: 6px; }
780
+ .desk-stat-row {
781
+ display: flex;
782
+ align-items: baseline;
783
+ justify-content: space-between;
784
+ gap: 8px;
785
+ color: var(--text-2);
786
+ font-size: 13px;
787
+ line-height: 18px;
788
+ }
789
+ .desk-stat-row b { color: var(--text); font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
790
+
791
+ /* --- tab switching on desktop --- */
792
+ .panel.tab-dashboard #cards { display: none; }
793
+ .panel:not(.tab-dashboard) .desk { display: none; }
794
+ .panel:not(.tab-dashboard) #cards {
795
+ display: grid;
796
+ grid-column: 1 / -1;
797
+ grid-template-columns: repeat(12, minmax(0, 1fr));
798
+ gap: 12px;
799
+ }
800
+ .panel:not(.tab-dashboard) #cards { align-content: start; }
801
+ .panel:not(.tab-dashboard) #cards > * { grid-column: span 6; }
802
+ .panel:not(.tab-dashboard) #cards > [data-card-id="metrics"] { grid-column: span 8; }
803
+ .panel:not(.tab-dashboard) #cards > [data-card-id="trace"] { grid-column: span 8; }
804
+ .panel:not(.tab-dashboard) #cards > [data-card-id="runtime"] { grid-column: span 4; }
805
+ .panel:not(.tab-dashboard) #cards > [data-card-id="model"] { grid-column: span 12; }
806
+ .panel:not(.tab-dashboard) #cards > .channels-wrap { grid-column: span 12; }
807
+ .panel:not(.tab-dashboard) .channels-wrap .channel-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
808
+ .panel:not(.tab-dashboard) .add-cards { grid-column: 1 / -1; }
809
+ .layout-editing #cards { display: grid; grid-column: 1 / -1; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 12px; }
810
+ .layout-editing #cards > * { grid-column: span 6; }
811
+
812
+ /* --- desktop edit mode (hide chips on desk cards) --- */
813
+ .desk-hide {
814
+ display: none;
815
+ position: absolute;
816
+ top: -8px;
817
+ right: -8px;
818
+ z-index: 3;
819
+ place-items: center;
820
+ width: 28px;
821
+ height: 28px;
822
+ padding: 0;
823
+ border: 0;
824
+ border-radius: 50%;
825
+ background: var(--danger);
826
+ color: #ffffff;
827
+ font-size: 18px;
828
+ line-height: 1;
829
+ cursor: pointer;
830
+ }
831
+ .layout-editing .desk-hide { display: grid; }
832
+ .desk-card.is-desk-hidden { display: none !important; }
833
+ }
package/ui/panel.html CHANGED
@@ -1,13 +1,25 @@
1
1
  <!doctype html>
2
- <!-- BioCore panel layout v24 (FlClash mobile): masthead/orb centered, channels 2x2, pill tabbar -->
2
+ <!-- BioCore panel layout v27: mobile = FlClash phone (masthead/channels 2x2/pill tabbar); desktop >=1024 = left rail + 12-col 5-row dashboard -->
3
3
  <html lang="zh-CN">
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <title>生物核 · 仪器面板</title>
8
- <link rel="stylesheet" href="panel.css?v=24">
8
+ <link rel="stylesheet" href="panel.css?v=27">
9
9
  </head>
10
10
  <body>
11
+ <aside class="rail" id="rail" aria-label="主导航">
12
+ <button class="rail-toggle" id="railToggle" type="button" aria-label="收起导航" aria-expanded="true" title="收起导航">
13
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="3.5" width="15" height="13" rx="2.5"/><path d="M7.5 3.5v13M12.5 8l-2 2 2 2"/></svg>
14
+ </button>
15
+ <nav class="rail-nav" aria-label="页面导航">
16
+ <button class="rail-item active" type="button" data-tab="dashboard" title="仪表盘" aria-selected="true"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="6" height="6" rx="1.5"/><rect x="11" y="3" width="6" height="6" rx="1.5"/><rect x="3" y="11" width="6" height="6" rx="1.5"/><rect x="11" y="11" width="6" height="6" rx="1.5"/></svg><span>仪表盘</span></button>
17
+ <button class="rail-item" type="button" data-tab="memory" title="记忆" aria-selected="false"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 3.5h10v13l-5-3-5 3z"/></svg><span>记忆</span></button>
18
+ <button class="rail-item" type="button" data-tab="relation" title="关系" aria-selected="false"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 16.2s-5.4-3.4-5.4-7a2.9 2.9 0 015.4-1.7 2.9 2.9 0 015.4 1.7c0 3.6-5.4 7-5.4 7z"/></svg><span>关系</span></button>
19
+ <button class="rail-item" type="button" data-tab="settings" title="配置" aria-selected="false"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="2.6"/><path d="M10 3.2v2M10 14.8v2M3.2 10h2M14.8 10h2M5.2 5.2l1.4 1.4M13.4 13.4l1.4 1.4M14.8 5.2l-1.4 1.4M6.6 13.4l-1.4 1.4"/></svg><span>配置</span></button>
20
+ <button class="rail-item" type="button" data-tab="tools" title="工具" aria-selected="false"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.6 4.4a3.2 3.2 0 00-4 4l-4.1 4.1 2 2 4.1-4.1a3.2 3.2 0 004-4l-2 2-1.6-1.6 1.6-2.4z"/></svg><span>工具</span></button>
21
+ </nav>
22
+ </aside>
11
23
  <main class="panel" id="panel">
12
24
  <header class="topbar" id="topBar">
13
25
  <div class="topbar-title">
@@ -22,6 +34,102 @@
22
34
  </button>
23
35
  </header>
24
36
 
37
+ <section class="desk" id="deskGrid" aria-label="桌面仪表盘">
38
+ <header class="card desk-card desk-status">
39
+ <div class="orb-wrap desk-orb" aria-hidden="true"><div class="orb"><span class="orb-ring"></span></div></div>
40
+ <div class="desk-status-body">
41
+ <h2>她现在的样子</h2>
42
+ <p class="motivation" id="deskMotivation">正在读取…</p>
43
+ <div class="status" id="deskStatus"><i class="dot"></i><span id="deskStatusText">未连接</span></div>
44
+ </div>
45
+ </header>
46
+
47
+ <section class="card desk-card desk-metric" id="deskMoodCard" data-card-label="心情">
48
+ <div class="desk-metric-head"><span>心情</span><span class="sub-trend"><b id="deskArrowV">→</b><span class="delta" id="deskDeltaV">—</span></span></div>
49
+ <div class="desk-metric-value"><strong id="deskNumV">–</strong><span class="sub-status" id="deskWordV">—</span></div>
50
+ <div class="bar"><i id="deskBarV"></i></div>
51
+ </section>
52
+
53
+ <section class="card desk-card desk-metric" id="deskTensionCard" data-card-label="张力">
54
+ <div class="desk-metric-head"><span>张力</span><span class="sub-trend"><b id="deskArrowTension">→</b><span class="delta" id="deskDeltaTension">—</span></span></div>
55
+ <div class="desk-metric-value"><strong id="deskNumTension">–</strong><span class="sub-status" id="deskWordTension">—</span></div>
56
+ <div class="bar"><i id="deskBarTension"></i></div>
57
+ </section>
58
+
59
+ <nav class="card desk-card desk-seg" id="segTabs" aria-label="分组导航">
60
+ <button class="seg-item active" type="button" data-tab="dashboard" aria-selected="true">仪表盘</button>
61
+ <button class="seg-item" type="button" data-tab="memory" aria-selected="false">记忆</button>
62
+ <button class="seg-item" type="button" data-tab="relation" aria-selected="false">关系</button>
63
+ <button class="seg-item" type="button" data-tab="settings" aria-selected="false">配置</button>
64
+ <button class="seg-item" type="button" data-tab="tools" aria-selected="false">工具</button>
65
+ </nav>
66
+
67
+ <section class="card desk-card desk-switch" aria-label="安全闸">
68
+ <span class="desk-switch-label">安全闸</span>
69
+ <label class="desk-switch-control"><input type="checkbox" data-switch="safety_enabled" aria-label="安全闸"><span class="desk-switch-state" data-switch-state="safety_enabled">当前:关</span></label>
70
+ </section>
71
+
72
+ <section class="card desk-card desk-switch" aria-label="主动回复">
73
+ <span class="desk-switch-label">主动回复</span>
74
+ <label class="desk-switch-control"><input type="checkbox" data-switch="proactive_send" aria-label="主动回复"><span class="desk-switch-state" data-switch-state="proactive_send">当前:关</span></label>
75
+ </section>
76
+
77
+ <section class="card desk-card desk-tile">
78
+ <div class="desk-tile-head"><span>数据目录</span><button class="desk-copy" id="deskCopyDataDir" type="button">复制</button></div>
79
+ <strong id="deskDataDir">宿主托管</strong>
80
+ </section>
81
+
82
+ <section class="card desk-card desk-tile">
83
+ <div class="desk-tile-head"><span>人设</span><span class="desk-chevron" aria-hidden="true">›</span></div>
84
+ <strong id="deskPersona">未载入</strong>
85
+ <span class="desk-tile-note" id="deskPersonaNote">—</span>
86
+ </section>
87
+
88
+ <section class="card desk-card desk-tile">
89
+ <div class="desk-tile-head"><span>运行时</span><span class="desk-chevron" aria-hidden="true">›</span></div>
90
+ <div class="desk-tile-row"><span>运行时长</span><b id="deskUptime">—</b></div>
91
+ <div class="desk-tile-row"><span>轮次</span><b id="deskTurns">—</b></div>
92
+ <div class="desk-tile-row"><span>向量模式</span><b id="deskVector">—</b></div>
93
+ </section>
94
+
95
+ <section class="card desk-card desk-tile">
96
+ <div class="desk-tile-head"><span>记忆容量</span><span class="desk-tile-note" id="deskCapText">0 / 200</span></div>
97
+ <div class="bar"><i id="deskCapFill"></i></div>
98
+ </section>
99
+
100
+ <section class="card desk-card desk-memory">
101
+ <div class="desk-card-head"><h2>最近记忆</h2><span class="card-aside" id="deskMemoryHint">—</span></div>
102
+ <ul class="memory-list" id="deskMemoryList"><li class="empty">还没有记忆,聊几句就会记住</li></ul>
103
+ </section>
104
+
105
+ <section class="card desk-card desk-chart">
106
+ <div class="desk-card-head"><h2>心情 · 24 小时</h2><span class="card-aside" id="deskChartHint">单线 + 事件点</span></div>
107
+ <canvas id="deskChart" height="170"></canvas>
108
+ </section>
109
+
110
+ <section class="card desk-card desk-stat">
111
+ <div class="desk-card-head"><h2>四通道均值</h2></div>
112
+ <div class="desk-stat-row"><span>绷紧</span><b id="deskAvgTension">—</b></div>
113
+ <div class="desk-stat-row"><span>暖</span><b id="deskAvgWarmth">—</b></div>
114
+ <div class="desk-stat-row"><span>沉</span><b id="deskAvgHeaviness">—</b></div>
115
+ <div class="desk-stat-row"><span>清</span><b id="deskAvgClarity">—</b></div>
116
+ </section>
117
+
118
+ <section class="card desk-card desk-stat">
119
+ <div class="desk-card-head"><h2>关系</h2></div>
120
+ <div class="desk-stat-row"><span>亲近</span><b id="deskRelCloseness">—</b></div>
121
+ <div class="desk-stat-row"><span>信任</span><b id="deskRelTrust">—</b></div>
122
+ <div class="desk-stat-row"><span>冲突</span><b id="deskRelConflict">—</b></div>
123
+ </section>
124
+
125
+ <section class="card desk-card desk-stat">
126
+ <div class="desk-card-head"><h2>今日</h2></div>
127
+ <div class="desk-stat-row"><span>新增记忆</span><b id="deskTodayMem">—</b></div>
128
+ <div class="desk-stat-row"><span>轮次</span><b id="deskTodayTurns">—</b></div>
129
+ <div class="desk-stat-row"><span>主动发送</span><b id="deskTodayProactive">—</b></div>
130
+ </section>
131
+ </section>
132
+
25
133
  <header class="masthead" id="masthead">
26
134
  <div class="orb-wrap" aria-hidden="true">
27
135
  <div class="orb"><span class="orb-ring"></span></div>
@@ -270,15 +378,19 @@
270
378
  <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 3.5h10v13l-5-3-5 3z"/></svg>
271
379
  <span>记忆</span>
272
380
  </button>
381
+ <button class="tab-item" type="button" data-tab="relation" aria-selected="false">
382
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 16.2s-5.4-3.4-5.4-7a2.9 2.9 0 015.4-1.7 2.9 2.9 0 015.4 1.7c0 3.6-5.4 7-5.4 7z"/></svg>
383
+ <span>关系</span>
384
+ </button>
273
385
  <button class="tab-item" type="button" data-tab="settings" aria-selected="false">
274
386
  <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="2.6"/><path d="M10 3.2v2M10 14.8v2M3.2 10h2M14.8 10h2M5.2 5.2l1.4 1.4M13.4 13.4l1.4 1.4M14.8 5.2l-1.4 1.4M6.6 13.4l-1.4 1.4"/></svg>
275
- <span>设置</span>
387
+ <span>配置</span>
276
388
  </button>
277
- <button class="tab-item" type="button" data-tab="diagnosis" aria-selected="false">
278
- <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 11h3.5l2-5 3 10 2-5H17"/></svg>
279
- <span>诊断</span>
389
+ <button class="tab-item" type="button" data-tab="tools" aria-selected="false">
390
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.6 4.4a3.2 3.2 0 00-4 4l-4.1 4.1 2 2 4.1-4.1a3.2 3.2 0 004-4l-2 2-1.6-1.6 1.6-2.4z"/></svg>
391
+ <span>工具</span>
280
392
  </button>
281
- </nav>
393
+ </nav>
282
394
 
283
395
  <div class="layout-toast" id="layoutToast" role="status" hidden></div>
284
396
 
@@ -289,6 +401,6 @@
289
401
  </main>
290
402
  <script src="panel-layout.js?v=5"></script>
291
403
  <script src="trace-view.js?v=5"></script>
292
- <script src="panel.js?v=5"></script>
404
+ <script src="panel.js?v=6"></script>
293
405
  </body>
294
406
  </html>
package/ui/panel.js CHANGED
@@ -442,8 +442,7 @@
442
442
  return localPoints.slice()
443
443
  }
444
444
 
445
- function renderChart(data) {
446
- var canvas = $('moodChart')
445
+ function drawChartInto(canvas, data) {
447
446
  if (!canvas) return
448
447
  var points = mergePoints(data)
449
448
  var wrapWidth = canvas.clientWidth || canvas.parentElement.clientWidth || 600
@@ -523,7 +522,7 @@
523
522
  ctx.arc(toX(eventTs), eventY, 2.6, 0, Math.PI * 2)
524
523
  ctx.fill()
525
524
  }
526
- setText('chartHint', '单线 + ' + events.length + ' 个事件点')
525
+ setText(canvas.id === 'deskChart' ? 'deskChartHint' : 'chartHint', '单线 + ' + events.length + ' 个事件点')
527
526
  } else {
528
527
  ctx.strokeStyle = lineColor
529
528
  ctx.lineWidth = 2
@@ -538,12 +537,183 @@
538
537
  ctx.arc(padL + plotW - 3, padT + ((1 - Math.max(-1, Math.min(1, points[0].v))) / 2) * plotH, 3, 0, Math.PI * 2)
539
538
  ctx.fill()
540
539
  }
541
- setText('chartHint', '积累中 · 单点')
540
+ setText(canvas.id === 'deskChart' ? 'deskChartHint' : 'chartHint', '积累中 · 单点')
542
541
  }
543
542
  ctx.fillStyle = mutedColor
544
543
  ctx.fillText('24 小时', padL, wrapHeight - 5)
545
544
  }
546
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
558
+ }
559
+
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
597
+ 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
+ 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
+ }
614
+
615
+ function deskCards() {
616
+ return Array.prototype.slice.call(document.querySelectorAll('#deskGrid .desk-card'))
617
+ }
618
+
619
+ function deskCardId(card, index) {
620
+ if (!card.id) card.id = 'desk-card-' + index
621
+ return card.id
622
+ }
623
+
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 [] }
630
+ }
631
+
632
+ function writeDeskHidden(list) {
633
+ try { window.localStorage.setItem(DESK_HIDDEN_KEY, JSON.stringify(list)) } catch (e) { /* ignore */ }
634
+ }
635
+
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
+ })
641
+ }
642
+
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)
654
+ })
655
+ }
656
+
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
+ }
670
+
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'))
677
+ return
678
+ }
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 */ }
714
+ })
715
+ }
716
+
547
717
  function updateFooter(data) {
548
718
  var memory = data && data.available !== false && typeof data.memory_count === 'number'
549
719
  ? data.memory_count : '-'
@@ -560,6 +730,8 @@
560
730
  renderRuntime(data)
561
731
  renderChart(data)
562
732
  updateFooter(data)
733
+ deskSync(data)
734
+ deskSync(data)
563
735
  if (!data) renderEmpty('request_failed')
564
736
  else if (data.available === false) renderEmpty(String(data.reason || 'request_failed'))
565
737
  else clearEmpty()
@@ -611,16 +783,20 @@
611
783
  }
612
784
 
613
785
  function paintSwitchState(key, checked) {
614
- var state = document.querySelector('[data-switch-state="' + key + '"]')
615
- if (state) state.textContent = '当前:' + (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
+ function setSwitchInputs(key, checked) {
791
+ var inputs = document.querySelectorAll('input[data-switch="' + key + '"]')
792
+ for (var i = 0; i < inputs.length; i += 1) inputs[i].checked = !!checked
793
+ paintSwitchState(key, checked)
616
794
  }
617
795
 
618
796
  function applySwitchValues(values) {
619
797
  for (var i = 0; i < SWITCHES.length; i += 1) {
620
- var input = document.querySelector('input[data-switch="' + SWITCHES[i].key + '"]')
621
- if (input && values && typeof values[SWITCHES[i].key] === 'boolean') {
622
- input.checked = values[SWITCHES[i].key]
623
- paintSwitchState(SWITCHES[i].key, input.checked)
798
+ if (values && typeof values[SWITCHES[i].key] === 'boolean') {
799
+ setSwitchInputs(SWITCHES[i].key, values[SWITCHES[i].key])
624
800
  }
625
801
  }
626
802
  }
@@ -673,14 +849,12 @@
673
849
  applySwitchValues(body.switches)
674
850
  setText('switchHint', '已写入宿主设置')
675
851
  } else {
676
- input.checked = !next
677
- paintSwitchState(key, input.checked)
852
+ setSwitchInputs(key, !next)
678
853
  setText('switchHint', '写入失败:' + ((body && body.reason) || 'unknown'))
679
854
  }
680
855
  })
681
856
  .catch(function () {
682
- input.checked = !next
683
- paintSwitchState(key, input.checked)
857
+ setSwitchInputs(key, !next)
684
858
  setText('switchHint', '写入失败:未连接')
685
859
  })
686
860
  .then(function () {
@@ -1270,15 +1444,18 @@
1270
1444
  if (!layoutApi) return
1271
1445
  var group = layoutEditing ? null : (layoutApi.TAB_GROUPS[activeTab] || [])
1272
1446
  var panel = $('panel')
1273
- if (panel) panel.classList.toggle('tab-dashboard', activeTab === 'dashboard')
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
+ }
1274
1453
  var cards = layoutCards()
1275
1454
  for (var i = 0; i < cards.length; i += 1) {
1276
1455
  var id = cards[i].getAttribute('data-card-id')
1277
1456
  cards[i].classList.toggle('tab-hidden', !!group && group.indexOf(id) < 0)
1278
1457
  }
1279
- var bar = $('tabBar')
1280
- if (!bar) return
1281
- var items = bar.querySelectorAll('[data-tab]')
1458
+ var items = document.querySelectorAll('[data-tab]')
1282
1459
  for (var j = 0; j < items.length; j += 1) {
1283
1460
  var on = items[j].getAttribute('data-tab') === activeTab
1284
1461
  items[j].classList.toggle('active', on)
@@ -1286,17 +1463,19 @@
1286
1463
  }
1287
1464
  }
1288
1465
 
1466
+ function setActiveTab(tab) {
1467
+ if (!layoutApi || !layoutApi.TAB_GROUPS[tab]) return
1468
+ activeTab = tab
1469
+ applyTabGroup()
1470
+ }
1471
+
1289
1472
  function bindTabBar() {
1290
- var bar = $('tabBar')
1291
- if (!bar) return
1292
- bar.addEventListener('click', function (event) {
1473
+ document.addEventListener('click', function (event) {
1293
1474
  var target = event.target
1294
1475
  var button = target && target.closest ? target.closest('[data-tab]') : null
1295
1476
  if (!button) return
1296
- var tab = button.getAttribute('data-tab')
1297
- if (!layoutApi || !layoutApi.TAB_GROUPS[tab]) return
1298
- activeTab = tab
1299
- applyTabGroup()
1477
+ event.preventDefault()
1478
+ setActiveTab(button.getAttribute('data-tab'))
1300
1479
  })
1301
1480
  applyTabGroup()
1302
1481
  }
@@ -1354,6 +1533,8 @@
1354
1533
  if (reset) reset.hidden = !layoutEditing
1355
1534
  var cards = layoutCards()
1356
1535
  for (var i = 0; i < cards.length; i += 1) ensureHideButton(cards[i])
1536
+ ensureDeskHides()
1537
+ applyDeskHidden()
1357
1538
  renderAddCards()
1358
1539
  applyTabGroup()
1359
1540
  if (!layoutEditing) persistDomOrder()
@@ -1396,7 +1577,11 @@
1396
1577
  return
1397
1578
  }
1398
1579
  var hidden = hiddenCards()
1399
- if (!hidden.length) {
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) {
1400
1585
  section.hidden = true
1401
1586
  list.textContent = ''
1402
1587
  return
@@ -1411,6 +1596,14 @@
1411
1596
  chip.textContent = '+ ' + hidden[i].label
1412
1597
  list.appendChild(chip)
1413
1598
  }
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
+ }
1414
1607
  }
1415
1608
 
1416
1609
  function showHiddenCard(id) {
@@ -1583,9 +1776,13 @@
1583
1776
  document.addEventListener('pointermove', onCardsPointerMove, { passive: false })
1584
1777
  document.addEventListener('pointerup', onCardsPointerUp)
1585
1778
  document.addEventListener('pointercancel', onCardsPointerUp)
1779
+ ensureDeskHides()
1780
+ applyDeskHidden()
1781
+ document.addEventListener('click', onDeskEditClick)
1586
1782
  }
1587
1783
 
1588
1784
  function boot() {
1785
+ railInit()
1589
1786
  initLayout()
1590
1787
  bindAdvanced()
1591
1788
  bindPersona()