dsh-biocore 0.1.7 → 0.1.9
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/cordis.patch.yml +3 -1
- package/lib/index.js +31 -26
- package/lib/modules/core-turn.js +16 -3
- package/package.json +1 -1
- package/python/biocore_runtime/pipeline.py +36 -8
- package/ui/panel.css +268 -1
- package/ui/panel.html +108 -2
- package/ui/panel.js +235 -29
package/cordis.patch.yml
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
dataDir: ''
|
|
9
9
|
# Memory (task-203): capture both sides, inject <=420 chars, 20/hour.
|
|
10
10
|
memoryEnabled: true
|
|
11
|
+
# Standalone session/event capture listener (off: capture happens in core turn_start/end)
|
|
12
|
+
memoryCaptureEnabled: false
|
|
11
13
|
injectMaxChars: 420
|
|
12
14
|
hourlyCap: 20
|
|
13
15
|
# Proactive push (task-205): both switches must be true; default OFF.
|
|
@@ -19,4 +21,4 @@
|
|
|
19
21
|
proactivePerHour: 2
|
|
20
22
|
proactiveMinTrust: 0.6
|
|
21
23
|
proactiveMaxChars: 60
|
|
22
|
-
proactiveRequireOpenLoop: true
|
|
24
|
+
proactiveRequireOpenLoop: true
|
package/lib/index.js
CHANGED
|
@@ -665,7 +665,7 @@ export function makeSessionEventListener(options) {
|
|
|
665
665
|
}
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
-
const CORE_STATE_TTL_MS =
|
|
668
|
+
const CORE_STATE_TTL_MS = 5000
|
|
669
669
|
const CORE_STATE_CACHE = new Map()
|
|
670
670
|
|
|
671
671
|
/** Read the core pipeline state action (panel state, task-212), briefly cached. */
|
|
@@ -726,33 +726,34 @@ export async function handleState(req, res, stateDir, options) {
|
|
|
726
726
|
const sessionId = cleanSessionId(rawSession)
|
|
727
727
|
if (!sessionId) return sendJson(res, 200, unavailable('bad_session'))
|
|
728
728
|
const stateFile = sessionStateFile(stateDir, sessionId)
|
|
729
|
-
|
|
730
|
-
let
|
|
729
|
+
const read = readStateFile(stateFile)
|
|
730
|
+
let body = null
|
|
731
731
|
if (read.missing) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
try {
|
|
747
|
-
const getSwitches = typeof opts.getSwitches === 'function' ? opts.getSwitches : null
|
|
748
|
-
if (getSwitches && anySwitchOn(getSwitches())) {
|
|
749
|
-
const core = await fetchCoreState(sessionId, {
|
|
750
|
-
runAction: opts.runAction,
|
|
751
|
-
store: opts.store,
|
|
752
|
-
})
|
|
753
|
-
body = mergeCoreState(body, core)
|
|
732
|
+
// Fast path: never call Python when the session file exists. Only a
|
|
733
|
+
// missing file + an enabled switch may fall back to runtimeAction('state').
|
|
734
|
+
let core = null
|
|
735
|
+
try {
|
|
736
|
+
const getSwitches = typeof opts.getSwitches === 'function' ? opts.getSwitches : null
|
|
737
|
+
if (getSwitches && anySwitchOn(getSwitches())) {
|
|
738
|
+
core = await fetchCoreState(sessionId, {
|
|
739
|
+
runAction: opts.runAction,
|
|
740
|
+
store: opts.store,
|
|
741
|
+
})
|
|
742
|
+
}
|
|
743
|
+
} catch { core = null }
|
|
744
|
+
if (!core || core.ok !== true || !core.state || typeof core.state !== 'object') {
|
|
745
|
+
return sendJson(res, 200, unavailable('no_state_yet'))
|
|
754
746
|
}
|
|
755
|
-
|
|
747
|
+
body = projectState(core.state, { mtimeMs: 0, sessionId })
|
|
748
|
+
Object.assign(body, buildPanelExtras(core.state))
|
|
749
|
+
body.source = 'runtime'
|
|
750
|
+
} else if (read.error) {
|
|
751
|
+
return sendJson(res, 200, unavailable(read.error))
|
|
752
|
+
} else {
|
|
753
|
+
body = projectState(read.state, { mtimeMs: read.mtimeMs, sessionId })
|
|
754
|
+
Object.assign(body, buildPanelExtras(read.state))
|
|
755
|
+
body.source = 'file'
|
|
756
|
+
}
|
|
756
757
|
if (!payloadWithinLimit(body)) {
|
|
757
758
|
return sendJson(res, 200, unavailable('payload_too_large'))
|
|
758
759
|
}
|
|
@@ -867,10 +868,14 @@ export function apply(ctx, config) {
|
|
|
867
868
|
const memoryDir = resolveMemoryDir(config)
|
|
868
869
|
const injectionState = createInjectionState()
|
|
869
870
|
try {
|
|
871
|
+
// Normally capture happens inside core turn_start/turn_end. The standalone
|
|
872
|
+
// session/event capture listener stays available but is opt-in.
|
|
873
|
+
if (config && config.memoryCaptureEnabled === true) {
|
|
870
874
|
const capture = registerMemoryCapture(ctx, () => config, memoryDir, {
|
|
871
875
|
injectionState,
|
|
872
876
|
})
|
|
873
877
|
if (capture) keep(capture.dispose)
|
|
878
|
+
}
|
|
874
879
|
} catch { /* memory capture unavailable */ }
|
|
875
880
|
try {
|
|
876
881
|
const injection = registerMemoryInjection(ctx, () => config, memoryDir,
|
package/lib/modules/core-turn.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* unavailable it falls back to the old adapter bridge once and logs a single
|
|
15
15
|
* warning; all switches off means no python call, no prompt text, no gate.
|
|
16
16
|
*/
|
|
17
|
+
import { readFileSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
17
19
|
import { readStateFile, sessionStateFile } from './state.js'
|
|
18
20
|
import { pendingSessionTurn, queueSessionTurn } from './turn-queue.js'
|
|
19
21
|
import {
|
|
@@ -105,6 +107,13 @@ export function chooseCleanReply(original, coreCleanText, options) {
|
|
|
105
107
|
return String(sanitize(source))
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
export function personaApplied(dataDir) {
|
|
111
|
+
try {
|
|
112
|
+
const status = JSON.parse(readFileSync(join(String(dataDir || '').trim(), 'persona_preset.json'), 'utf8'))
|
|
113
|
+
return status && status.applied === true
|
|
114
|
+
} catch { return false }
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
export function turnStartPayload(options) {
|
|
109
118
|
const opts = options || {}
|
|
110
119
|
const expression = opts.expression && typeof opts.expression === 'object'
|
|
@@ -114,6 +123,9 @@ export function turnStartPayload(options) {
|
|
|
114
123
|
user_text: stringId(opts.userText).slice(0, 2000),
|
|
115
124
|
switches: opts.switches || {},
|
|
116
125
|
store: opts.store || defaultStoreDir(),
|
|
126
|
+
capture: true,
|
|
127
|
+
memoryEnabled: true,
|
|
128
|
+
persona_prompt: opts.personaPrompt !== false,
|
|
117
129
|
length_hint: expression.length_hint === true,
|
|
118
130
|
length_target: Number.isFinite(Number(expression.length_target))
|
|
119
131
|
? Number(expression.length_target) : EXPRESSION_DEFAULTS.length_target,
|
|
@@ -132,8 +144,8 @@ export function turnEndPayload(options) {
|
|
|
132
144
|
user_text: stringId(opts.userText).slice(0, 2000),
|
|
133
145
|
switches: opts.switches || {},
|
|
134
146
|
store: opts.store || defaultStoreDir(),
|
|
135
|
-
capture:
|
|
136
|
-
memoryEnabled:
|
|
147
|
+
capture: true,
|
|
148
|
+
memoryEnabled: true,
|
|
137
149
|
sanitize_reply: opts.sanitizeReply !== false,
|
|
138
150
|
diagnostics,
|
|
139
151
|
}, diagnostics, {
|
|
@@ -189,6 +201,7 @@ export function makeCoreSessionListener(options) {
|
|
|
189
201
|
try {
|
|
190
202
|
core = await runAction('turn_start', turnStartPayload({
|
|
191
203
|
sessionId, userText: text, switches, store, expression: getExpression(),
|
|
204
|
+
personaPrompt: !personaApplied(stateDir),
|
|
192
205
|
}))
|
|
193
206
|
} catch (error) {
|
|
194
207
|
core = { ok: false, reason: 'core_exception' }
|
|
@@ -347,4 +360,4 @@ function joinProfile(stateDir) {
|
|
|
347
360
|
try {
|
|
348
361
|
return stateDir ? String(stateDir).replace(/[\\/]+$/, '') + '/persona_profile.json' : ''
|
|
349
362
|
} catch { return '' }
|
|
350
|
-
}
|
|
363
|
+
}
|
package/package.json
CHANGED
|
@@ -1780,6 +1780,24 @@ def dispatch(action, payload=None):
|
|
|
1780
1780
|
state_store = _relationship_store(data)
|
|
1781
1781
|
if state_store is not None:
|
|
1782
1782
|
_save_session_state(state_store, session_id, session_state)
|
|
1783
|
+
capture_enabled = _as_bool(data.get("capture"), True)
|
|
1784
|
+
captured_user = {"captured": False, "entry_id": "", "reason": "disabled"}
|
|
1785
|
+
capture_store = data.get("store")
|
|
1786
|
+
if capture_enabled and capture_store:
|
|
1787
|
+
user_capture = capture(session_id, "user", user_text, capture_store, {
|
|
1788
|
+
"enabled": _as_bool(data.get("memoryEnabled", True), True),
|
|
1789
|
+
"hourly_cap": data.get("hourly_cap", HOURLY_CAP),
|
|
1790
|
+
"min_chars": data.get("min_chars", MIN_CHARS),
|
|
1791
|
+
})
|
|
1792
|
+
decision = user_capture.get("decision") or {}
|
|
1793
|
+
entry = user_capture.get("entry") or {}
|
|
1794
|
+
captured_user = {
|
|
1795
|
+
"captured": bool(decision.get("capture")),
|
|
1796
|
+
"entry_id": str(entry.get("id") or ""),
|
|
1797
|
+
"reason": str(decision.get("reason") or ""),
|
|
1798
|
+
}
|
|
1799
|
+
elif capture_enabled:
|
|
1800
|
+
captured_user["reason"] = "no_store"
|
|
1783
1801
|
block = prompt_block(dict(
|
|
1784
1802
|
data, phase="turn", session_id=session_id,
|
|
1785
1803
|
user_text=user_text,
|
|
@@ -1792,7 +1810,8 @@ def dispatch(action, payload=None):
|
|
|
1792
1810
|
return {"ok": True, "prompt_block": block["text"],
|
|
1793
1811
|
"constraints": self_layer_constraints(
|
|
1794
1812
|
dict(data, user_text=user_text)),
|
|
1795
|
-
"state": session_state
|
|
1813
|
+
"state": session_state,
|
|
1814
|
+
"captured_user": captured_user}
|
|
1796
1815
|
if action == "turn_end":
|
|
1797
1816
|
session_id = str(data.get("session_id") or "").strip()
|
|
1798
1817
|
if not session_id:
|
|
@@ -1828,13 +1847,22 @@ def dispatch(action, payload=None):
|
|
|
1828
1847
|
gate["text"] = _sanitize.clean(gate.get("text") or "")
|
|
1829
1848
|
except Exception:
|
|
1830
1849
|
pass
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1850
|
+
capture_enabled = _as_bool(data.get("capture"), True)
|
|
1851
|
+
capture_store = data.get("store")
|
|
1852
|
+
if capture_enabled and capture_store:
|
|
1853
|
+
captured = capture(session_id, "assistant", assistant_text,
|
|
1854
|
+
capture_store, {
|
|
1855
|
+
"enabled": _as_bool(data.get("memoryEnabled",
|
|
1856
|
+
switches.get("memoryEnabled", True)),
|
|
1857
|
+
True),
|
|
1858
|
+
"hourly_cap": data.get("hourly_cap", HOURLY_CAP),
|
|
1859
|
+
"min_chars": data.get("min_chars", MIN_CHARS),
|
|
1860
|
+
})
|
|
1861
|
+
else:
|
|
1862
|
+
captured = {"ok": True, "decision": {
|
|
1863
|
+
"capture": False,
|
|
1864
|
+
"reason": "disabled" if not capture_enabled else "no_store",
|
|
1865
|
+
}, "entry": None}
|
|
1838
1866
|
merge_state_relationship_into_shared(session_id, data)
|
|
1839
1867
|
decision = captured.get("decision") or {}
|
|
1840
1868
|
entry = captured.get("entry") or {}
|
package/ui/panel.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* BioCore instrument panel — FlClash mobile spec. | layout
|
|
1
|
+
/* BioCore instrument panel — FlClash mobile spec. | layout v26
|
|
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. */
|
|
@@ -556,3 +556,270 @@ body { max-width: 100%; }
|
|
|
556
556
|
.model-card .model-section-title:first-of-type { margin-top: 0; }
|
|
557
557
|
.rerank-toggle { margin: 0 0 10px; }
|
|
558
558
|
.rerank-grid[hidden] { display: none !important; }
|
|
559
|
+
|
|
560
|
+
/* ---------- 15. desktop (>=1024): left rail + 12-col dashboard ---------- */
|
|
561
|
+
.rail, .desk { display: none; }
|
|
562
|
+
|
|
563
|
+
@media (min-width: 1024px) {
|
|
564
|
+
:root {
|
|
565
|
+
--rail-w: 220px;
|
|
566
|
+
--radius-card: 32px;
|
|
567
|
+
--pad-card: 20px;
|
|
568
|
+
--gap-card: 12px;
|
|
569
|
+
}
|
|
570
|
+
body { padding: 0; }
|
|
571
|
+
body.rail-collapsed { --rail-w: 64px; }
|
|
572
|
+
|
|
573
|
+
.panel {
|
|
574
|
+
width: auto;
|
|
575
|
+
max-width: none;
|
|
576
|
+
min-height: 100vh;
|
|
577
|
+
margin: 0 0 0 var(--rail-w);
|
|
578
|
+
padding: 16px 24px 24px 16px;
|
|
579
|
+
display: grid;
|
|
580
|
+
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
581
|
+
gap: 12px;
|
|
582
|
+
align-content: start;
|
|
583
|
+
transition: margin-left 160ms ease;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* --- left rail --- */
|
|
587
|
+
.rail {
|
|
588
|
+
position: fixed;
|
|
589
|
+
inset: 0 auto 0 0;
|
|
590
|
+
z-index: 40;
|
|
591
|
+
width: var(--rail-w);
|
|
592
|
+
display: flex;
|
|
593
|
+
flex-direction: column;
|
|
594
|
+
gap: 6px;
|
|
595
|
+
padding: 12px 10px;
|
|
596
|
+
background: var(--page);
|
|
597
|
+
border-right: 1px solid var(--line);
|
|
598
|
+
overflow: hidden;
|
|
599
|
+
transition: width 160ms ease, padding 160ms ease;
|
|
600
|
+
}
|
|
601
|
+
.rail-toggle {
|
|
602
|
+
display: grid;
|
|
603
|
+
place-items: center;
|
|
604
|
+
width: 40px;
|
|
605
|
+
height: 40px;
|
|
606
|
+
margin: 0 0 8px 2px;
|
|
607
|
+
padding: 0;
|
|
608
|
+
border: 0;
|
|
609
|
+
border-radius: 10px;
|
|
610
|
+
background: transparent;
|
|
611
|
+
color: var(--text-2);
|
|
612
|
+
cursor: pointer;
|
|
613
|
+
transition: background 160ms ease, color 160ms ease;
|
|
614
|
+
}
|
|
615
|
+
.rail-toggle:hover { background: rgba(255, 255, 255, 0.04); color: var(--text); }
|
|
616
|
+
.rail-toggle svg { width: 20px; height: 20px; }
|
|
617
|
+
.rail-nav { display: flex; flex-direction: column; gap: 2px; }
|
|
618
|
+
.rail-item {
|
|
619
|
+
position: relative;
|
|
620
|
+
display: flex;
|
|
621
|
+
align-items: center;
|
|
622
|
+
gap: 10px;
|
|
623
|
+
height: 40px;
|
|
624
|
+
padding: 0 12px;
|
|
625
|
+
border: 0;
|
|
626
|
+
border-radius: 8px;
|
|
627
|
+
background: transparent;
|
|
628
|
+
color: var(--text-2);
|
|
629
|
+
font: inherit;
|
|
630
|
+
font-size: 14px;
|
|
631
|
+
line-height: 20px;
|
|
632
|
+
text-align: left;
|
|
633
|
+
white-space: nowrap;
|
|
634
|
+
overflow: hidden;
|
|
635
|
+
cursor: pointer;
|
|
636
|
+
transition: background 160ms ease, color 160ms ease;
|
|
637
|
+
}
|
|
638
|
+
.rail-item svg { flex: 0 0 20px; width: 20px; height: 20px; }
|
|
639
|
+
.rail-item:hover { background: rgba(255, 255, 255, 0.04); color: var(--text); }
|
|
640
|
+
.rail-item.active { background: rgba(255, 255, 255, 0.06); color: var(--text); }
|
|
641
|
+
.rail-item.active::before {
|
|
642
|
+
content: '';
|
|
643
|
+
position: absolute;
|
|
644
|
+
left: 0;
|
|
645
|
+
top: 8px;
|
|
646
|
+
bottom: 8px;
|
|
647
|
+
width: 2px;
|
|
648
|
+
border-radius: 2px;
|
|
649
|
+
background: var(--accent);
|
|
650
|
+
}
|
|
651
|
+
body.rail-collapsed .rail-item { justify-content: center; padding: 0; }
|
|
652
|
+
body.rail-collapsed .rail-item span { display: none; }
|
|
653
|
+
|
|
654
|
+
/* --- header --- */
|
|
655
|
+
.topbar { position: static; height: 56px; padding: 0; grid-column: 1 / -1; }
|
|
656
|
+
.topbar h1 { font-size: 22px; line-height: 28px; }
|
|
657
|
+
.masthead { display: none; }
|
|
658
|
+
.tabbar { display: none; }
|
|
659
|
+
.footer { grid-column: 1 / -1; padding: 0 4px; }
|
|
660
|
+
|
|
661
|
+
/* --- desktop dashboard: 12 col x 5 rows --- */
|
|
662
|
+
.desk {
|
|
663
|
+
display: grid;
|
|
664
|
+
grid-column: 1 / -1;
|
|
665
|
+
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
666
|
+
grid-template-rows: 208px 64px 176px minmax(200px, auto) minmax(120px, auto);
|
|
667
|
+
gap: 12px;
|
|
668
|
+
}
|
|
669
|
+
.desk-card {
|
|
670
|
+
position: relative;
|
|
671
|
+
min-width: 0;
|
|
672
|
+
padding: 20px;
|
|
673
|
+
border-radius: 32px;
|
|
674
|
+
overflow: hidden;
|
|
675
|
+
}
|
|
676
|
+
.desk-card-head {
|
|
677
|
+
display: flex;
|
|
678
|
+
align-items: baseline;
|
|
679
|
+
justify-content: space-between;
|
|
680
|
+
gap: 8px;
|
|
681
|
+
margin-bottom: 10px;
|
|
682
|
+
}
|
|
683
|
+
.desk-card-head h2 { margin: 0; font-size: 15px; line-height: 24px; font-weight: 600; }
|
|
684
|
+
.desk-status, .desk-seg, .desk-memory, .desk-chart { grid-column: span 6; }
|
|
685
|
+
.desk-metric, .desk-switch, .desk-tile { grid-column: span 3; }
|
|
686
|
+
.desk-stat { grid-column: span 4; }
|
|
687
|
+
|
|
688
|
+
.desk-status { display: flex; align-items: center; gap: 20px; }
|
|
689
|
+
.desk-orb { width: 88px; height: 88px; flex: 0 0 88px; display: grid; place-items: center; }
|
|
690
|
+
.desk-status-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
691
|
+
.desk-status-body h2 { margin: 0; font-size: 22px; line-height: 28px; font-weight: 600; }
|
|
692
|
+
.desk-status-body .motivation { margin: 0; color: var(--text-2); font-size: 15px; line-height: 22px; }
|
|
693
|
+
|
|
694
|
+
.desk-metric { display: flex; flex-direction: column; justify-content: center; gap: 10px; }
|
|
695
|
+
.desk-metric-head {
|
|
696
|
+
display: flex;
|
|
697
|
+
align-items: baseline;
|
|
698
|
+
justify-content: space-between;
|
|
699
|
+
gap: 8px;
|
|
700
|
+
color: var(--text-2);
|
|
701
|
+
font-size: 13px;
|
|
702
|
+
line-height: 18px;
|
|
703
|
+
}
|
|
704
|
+
.desk-metric-value { display: flex; align-items: baseline; gap: 10px; }
|
|
705
|
+
.desk-metric-value strong {
|
|
706
|
+
font-size: 40px;
|
|
707
|
+
line-height: 44px;
|
|
708
|
+
font-weight: 700;
|
|
709
|
+
letter-spacing: -0.02em;
|
|
710
|
+
font-variant-numeric: tabular-nums;
|
|
711
|
+
}
|
|
712
|
+
.desk-metric-value .sub-status { margin-left: auto; color: var(--text-2); font-size: 13px; }
|
|
713
|
+
|
|
714
|
+
.desk-seg { display: flex; align-items: center; gap: 6px; padding: 6px; border-radius: 32px; background: var(--tone); box-shadow: none; }
|
|
715
|
+
.seg-item {
|
|
716
|
+
flex: 1 1 0;
|
|
717
|
+
height: 52px;
|
|
718
|
+
border: 0;
|
|
719
|
+
border-radius: 16px;
|
|
720
|
+
background: transparent;
|
|
721
|
+
color: var(--text-2);
|
|
722
|
+
font: inherit;
|
|
723
|
+
font-size: 14px;
|
|
724
|
+
cursor: pointer;
|
|
725
|
+
transition: background 160ms ease, color 160ms ease;
|
|
726
|
+
}
|
|
727
|
+
.seg-item:hover { background: rgba(255, 255, 255, 0.04); color: var(--text); }
|
|
728
|
+
.seg-item.active { background: var(--accent); color: #ffffff; }
|
|
729
|
+
|
|
730
|
+
.desk-switch { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
731
|
+
.desk-switch-label { font-size: 15px; line-height: 24px; font-weight: 600; }
|
|
732
|
+
.desk-switch-control { display: flex; align-items: center; gap: 8px; }
|
|
733
|
+
.desk-switch-control input { width: 22px; height: 22px; accent-color: var(--accent); }
|
|
734
|
+
.desk-switch-state { color: var(--text-3); font-size: 12px; line-height: 16px; }
|
|
735
|
+
|
|
736
|
+
.desk-tile { display: flex; flex-direction: column; justify-content: center; gap: 8px; }
|
|
737
|
+
.desk-tile-head {
|
|
738
|
+
display: flex;
|
|
739
|
+
align-items: center;
|
|
740
|
+
justify-content: space-between;
|
|
741
|
+
gap: 8px;
|
|
742
|
+
color: var(--text-2);
|
|
743
|
+
font-size: 12px;
|
|
744
|
+
line-height: 16px;
|
|
745
|
+
}
|
|
746
|
+
.desk-tile strong {
|
|
747
|
+
font-size: 22px;
|
|
748
|
+
line-height: 28px;
|
|
749
|
+
font-weight: 600;
|
|
750
|
+
letter-spacing: -0.01em;
|
|
751
|
+
overflow: hidden;
|
|
752
|
+
text-overflow: ellipsis;
|
|
753
|
+
white-space: nowrap;
|
|
754
|
+
}
|
|
755
|
+
.desk-tile-note { color: var(--text-3); font-size: 12px; line-height: 16px; }
|
|
756
|
+
.desk-tile-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; color: var(--text-2); font-size: 13px; line-height: 18px; }
|
|
757
|
+
.desk-tile-row b { color: var(--text); font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
758
|
+
.desk-chevron { color: var(--text-3); font-size: 14px; }
|
|
759
|
+
.desk-copy {
|
|
760
|
+
height: 24px;
|
|
761
|
+
padding: 0 10px;
|
|
762
|
+
border: 0;
|
|
763
|
+
border-radius: 8px;
|
|
764
|
+
background: var(--block);
|
|
765
|
+
color: var(--text-2);
|
|
766
|
+
font: inherit;
|
|
767
|
+
font-size: 12px;
|
|
768
|
+
cursor: pointer;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.desk-memory { display: flex; flex-direction: column; }
|
|
772
|
+
.desk-memory .memory-list { max-height: none; }
|
|
773
|
+
.desk-chart { display: flex; flex-direction: column; }
|
|
774
|
+
#deskChart { width: 100%; height: 100%; min-height: 110px; display: block; }
|
|
775
|
+
|
|
776
|
+
.desk-stat { display: flex; flex-direction: column; justify-content: center; gap: 6px; }
|
|
777
|
+
.desk-stat-row {
|
|
778
|
+
display: flex;
|
|
779
|
+
align-items: baseline;
|
|
780
|
+
justify-content: space-between;
|
|
781
|
+
gap: 8px;
|
|
782
|
+
color: var(--text-2);
|
|
783
|
+
font-size: 13px;
|
|
784
|
+
line-height: 18px;
|
|
785
|
+
}
|
|
786
|
+
.desk-stat-row b { color: var(--text); font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
787
|
+
|
|
788
|
+
/* --- tab switching on desktop --- */
|
|
789
|
+
.panel.tab-dashboard #cards { display: none; }
|
|
790
|
+
.panel:not(.tab-dashboard) .desk { display: none; }
|
|
791
|
+
.panel:not(.tab-dashboard) #cards {
|
|
792
|
+
display: grid;
|
|
793
|
+
grid-column: 1 / -1;
|
|
794
|
+
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
795
|
+
gap: 12px;
|
|
796
|
+
}
|
|
797
|
+
.panel:not(.tab-dashboard) #cards > * { grid-column: span 6; }
|
|
798
|
+
.panel:not(.tab-dashboard) #cards > .channels-wrap { grid-column: span 12; }
|
|
799
|
+
.panel:not(.tab-dashboard) .channels-wrap .channel-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
800
|
+
.panel:not(.tab-dashboard) .add-cards { grid-column: 1 / -1; }
|
|
801
|
+
.layout-editing #cards { display: grid; grid-column: 1 / -1; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 12px; }
|
|
802
|
+
.layout-editing #cards > * { grid-column: span 6; }
|
|
803
|
+
|
|
804
|
+
/* --- desktop edit mode (hide chips on desk cards) --- */
|
|
805
|
+
.desk-hide {
|
|
806
|
+
display: none;
|
|
807
|
+
position: absolute;
|
|
808
|
+
top: -8px;
|
|
809
|
+
right: -8px;
|
|
810
|
+
z-index: 3;
|
|
811
|
+
place-items: center;
|
|
812
|
+
width: 28px;
|
|
813
|
+
height: 28px;
|
|
814
|
+
padding: 0;
|
|
815
|
+
border: 0;
|
|
816
|
+
border-radius: 50%;
|
|
817
|
+
background: var(--danger);
|
|
818
|
+
color: #ffffff;
|
|
819
|
+
font-size: 18px;
|
|
820
|
+
line-height: 1;
|
|
821
|
+
cursor: pointer;
|
|
822
|
+
}
|
|
823
|
+
.layout-editing .desk-hide { display: grid; }
|
|
824
|
+
.desk-card.is-desk-hidden { display: none !important; }
|
|
825
|
+
}
|
package/ui/panel.html
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<!-- BioCore panel layout
|
|
2
|
+
<!-- BioCore panel layout v26: 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=
|
|
8
|
+
<link rel="stylesheet" href="panel.css?v=26">
|
|
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="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>
|
|
19
|
+
<button class="rail-item" type="button" data-tab="diagnosis" 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="M3 11h3.5l2-5 3 10 2-5H17"/></svg><span>诊断</span></button>
|
|
20
|
+
</nav>
|
|
21
|
+
</aside>
|
|
11
22
|
<main class="panel" id="panel">
|
|
12
23
|
<header class="topbar" id="topBar">
|
|
13
24
|
<div class="topbar-title">
|
|
@@ -22,6 +33,101 @@
|
|
|
22
33
|
</button>
|
|
23
34
|
</header>
|
|
24
35
|
|
|
36
|
+
<section class="desk" id="deskGrid" aria-label="桌面仪表盘">
|
|
37
|
+
<header class="card desk-card desk-status">
|
|
38
|
+
<div class="orb-wrap desk-orb" aria-hidden="true"><div class="orb"><span class="orb-ring"></span></div></div>
|
|
39
|
+
<div class="desk-status-body">
|
|
40
|
+
<h2>她现在的样子</h2>
|
|
41
|
+
<p class="motivation" id="deskMotivation">正在读取…</p>
|
|
42
|
+
<div class="status" id="deskStatus"><i class="dot"></i><span id="deskStatusText">未连接</span></div>
|
|
43
|
+
</div>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<section class="card desk-card desk-metric" id="deskMoodCard" data-card-label="心情">
|
|
47
|
+
<div class="desk-metric-head"><span>心情</span><span class="sub-trend"><b id="deskArrowV">→</b><span class="delta" id="deskDeltaV">—</span></span></div>
|
|
48
|
+
<div class="desk-metric-value"><strong id="deskNumV">–</strong><span class="sub-status" id="deskWordV">—</span></div>
|
|
49
|
+
<div class="bar"><i id="deskBarV"></i></div>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<section class="card desk-card desk-metric" id="deskTensionCard" data-card-label="张力">
|
|
53
|
+
<div class="desk-metric-head"><span>张力</span><span class="sub-trend"><b id="deskArrowTension">→</b><span class="delta" id="deskDeltaTension">—</span></span></div>
|
|
54
|
+
<div class="desk-metric-value"><strong id="deskNumTension">–</strong><span class="sub-status" id="deskWordTension">—</span></div>
|
|
55
|
+
<div class="bar"><i id="deskBarTension"></i></div>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<nav class="card desk-card desk-seg" id="segTabs" aria-label="分组导航">
|
|
59
|
+
<button class="seg-item active" type="button" data-tab="dashboard" aria-selected="true">仪表盘</button>
|
|
60
|
+
<button class="seg-item" type="button" data-tab="memory" aria-selected="false">记忆</button>
|
|
61
|
+
<button class="seg-item" type="button" data-tab="settings" aria-selected="false">设置</button>
|
|
62
|
+
<button class="seg-item" type="button" data-tab="diagnosis" aria-selected="false">诊断</button>
|
|
63
|
+
</nav>
|
|
64
|
+
|
|
65
|
+
<section class="card desk-card desk-switch" aria-label="安全闸">
|
|
66
|
+
<span class="desk-switch-label">安全闸</span>
|
|
67
|
+
<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>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<section class="card desk-card desk-switch" aria-label="主动回复">
|
|
71
|
+
<span class="desk-switch-label">主动回复</span>
|
|
72
|
+
<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>
|
|
73
|
+
</section>
|
|
74
|
+
|
|
75
|
+
<section class="card desk-card desk-tile">
|
|
76
|
+
<div class="desk-tile-head"><span>数据目录</span><button class="desk-copy" id="deskCopyDataDir" type="button">复制</button></div>
|
|
77
|
+
<strong id="deskDataDir">宿主托管</strong>
|
|
78
|
+
</section>
|
|
79
|
+
|
|
80
|
+
<section class="card desk-card desk-tile">
|
|
81
|
+
<div class="desk-tile-head"><span>人设</span><span class="desk-chevron" aria-hidden="true">›</span></div>
|
|
82
|
+
<strong id="deskPersona">未载入</strong>
|
|
83
|
+
<span class="desk-tile-note" id="deskPersonaNote">—</span>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<section class="card desk-card desk-tile">
|
|
87
|
+
<div class="desk-tile-head"><span>运行时</span><span class="desk-chevron" aria-hidden="true">›</span></div>
|
|
88
|
+
<div class="desk-tile-row"><span>运行时长</span><b id="deskUptime">—</b></div>
|
|
89
|
+
<div class="desk-tile-row"><span>轮次</span><b id="deskTurns">—</b></div>
|
|
90
|
+
<div class="desk-tile-row"><span>向量模式</span><b id="deskVector">—</b></div>
|
|
91
|
+
</section>
|
|
92
|
+
|
|
93
|
+
<section class="card desk-card desk-tile">
|
|
94
|
+
<div class="desk-tile-head"><span>记忆容量</span><span class="desk-tile-note" id="deskCapText">0 / 200</span></div>
|
|
95
|
+
<div class="bar"><i id="deskCapFill"></i></div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<section class="card desk-card desk-memory">
|
|
99
|
+
<div class="desk-card-head"><h2>最近记忆</h2><span class="card-aside" id="deskMemoryHint">—</span></div>
|
|
100
|
+
<ul class="memory-list" id="deskMemoryList"><li class="empty">还没有记忆,聊几句就会记住</li></ul>
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<section class="card desk-card desk-chart">
|
|
104
|
+
<div class="desk-card-head"><h2>心情 · 24 小时</h2><span class="card-aside" id="deskChartHint">单线 + 事件点</span></div>
|
|
105
|
+
<canvas id="deskChart" height="170"></canvas>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<section class="card desk-card desk-stat">
|
|
109
|
+
<div class="desk-card-head"><h2>四通道均值</h2></div>
|
|
110
|
+
<div class="desk-stat-row"><span>绷紧</span><b id="deskAvgTension">—</b></div>
|
|
111
|
+
<div class="desk-stat-row"><span>暖</span><b id="deskAvgWarmth">—</b></div>
|
|
112
|
+
<div class="desk-stat-row"><span>沉</span><b id="deskAvgHeaviness">—</b></div>
|
|
113
|
+
<div class="desk-stat-row"><span>清</span><b id="deskAvgClarity">—</b></div>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
<section class="card desk-card desk-stat">
|
|
117
|
+
<div class="desk-card-head"><h2>关系</h2></div>
|
|
118
|
+
<div class="desk-stat-row"><span>亲近</span><b id="deskRelCloseness">—</b></div>
|
|
119
|
+
<div class="desk-stat-row"><span>信任</span><b id="deskRelTrust">—</b></div>
|
|
120
|
+
<div class="desk-stat-row"><span>冲突</span><b id="deskRelConflict">—</b></div>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<section class="card desk-card desk-stat">
|
|
124
|
+
<div class="desk-card-head"><h2>今日</h2></div>
|
|
125
|
+
<div class="desk-stat-row"><span>新增记忆</span><b id="deskTodayMem">—</b></div>
|
|
126
|
+
<div class="desk-stat-row"><span>轮次</span><b id="deskTodayTurns">—</b></div>
|
|
127
|
+
<div class="desk-stat-row"><span>主动发送</span><b id="deskTodayProactive">—</b></div>
|
|
128
|
+
</section>
|
|
129
|
+
</section>
|
|
130
|
+
|
|
25
131
|
<header class="masthead" id="masthead">
|
|
26
132
|
<div class="orb-wrap" aria-hidden="true">
|
|
27
133
|
<div class="orb"><span class="orb-ring"></span></div>
|
package/ui/panel.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
(function () {
|
|
8
8
|
'use strict'
|
|
9
9
|
|
|
10
|
-
var POLL_MS =
|
|
10
|
+
var POLL_MS = 5000
|
|
11
11
|
var SESSION = (new URLSearchParams(window.location.search).get('session') || '').trim()
|
|
12
12
|
var SWITCHES = [
|
|
13
13
|
{ key: 'safety_enabled', label: '安全闸', desc: '打开后可能拦截/改写回复' },
|
|
@@ -442,8 +442,7 @@
|
|
|
442
442
|
return localPoints.slice()
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
function
|
|
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
|
|
615
|
-
|
|
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
|
-
|
|
621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
1447
|
+
if (panel) {
|
|
1448
|
+
var tabsKnown = ['dashboard', 'memory', 'settings', 'diagnosis']
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
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()
|
|
@@ -1605,9 +1802,18 @@
|
|
|
1605
1802
|
loadSeries()
|
|
1606
1803
|
fetchState()
|
|
1607
1804
|
fetchTrace()
|
|
1608
|
-
|
|
1609
|
-
|
|
1805
|
+
function pollTick() {
|
|
1806
|
+
if (document.hidden) return
|
|
1807
|
+
fetchState()
|
|
1808
|
+
fetchTrace()
|
|
1809
|
+
}
|
|
1810
|
+
window.setInterval(pollTick, POLL_MS)
|
|
1610
1811
|
window.setInterval(updateStatusClock, 1000)
|
|
1812
|
+
document.addEventListener('visibilitychange', function () {
|
|
1813
|
+
if (document.hidden) return
|
|
1814
|
+
fetchState()
|
|
1815
|
+
fetchTrace()
|
|
1816
|
+
})
|
|
1611
1817
|
window.addEventListener('resize', function () { renderChart(lastData) })
|
|
1612
1818
|
}
|
|
1613
1819
|
|