kyp-mem 0.8.0 → 0.9.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.
- package/kyp_mem/static/index.html +1147 -796
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -6,64 +6,86 @@
|
|
|
6
6
|
<title>KYP-MEM</title>
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
-
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
10
10
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
11
11
|
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
|
12
12
|
<style>
|
|
13
13
|
:root {
|
|
14
|
-
|
|
15
|
-
--bg
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
14
|
+
/* Surfaces */
|
|
15
|
+
--bg: #0a0a0c;
|
|
16
|
+
--bg-2: #0b0b0e;
|
|
17
|
+
--bg-grad-top: #101015;
|
|
18
|
+
--panel: rgba(255,255,255,0.04);
|
|
19
|
+
--panel-2: rgba(255,255,255,0.065);
|
|
20
|
+
--panel-solid: #15151a;
|
|
21
|
+
--panel-solid-2: #1b1b22;
|
|
22
|
+
--glass: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.015));
|
|
23
|
+
--line: rgba(255,255,255,0.07);
|
|
24
|
+
--line-2: rgba(255,255,255,0.12);
|
|
25
|
+
--line-3: rgba(255,255,255,0.18);
|
|
26
|
+
/* Text */
|
|
27
|
+
--fg: #ededf2;
|
|
28
|
+
--muted: #a4a4b0;
|
|
29
|
+
--dim: #6c6c78;
|
|
30
|
+
/* Neon accent trio */
|
|
31
|
+
--c-violet: #8b7ff5;
|
|
32
|
+
--c-teal: #5eead4;
|
|
33
|
+
--c-amber: #e6b35c;
|
|
34
|
+
--c-violet-soft: rgba(139,127,245,0.16);
|
|
35
|
+
--c-teal-soft: rgba(94,234,212,0.16);
|
|
36
|
+
--c-amber-soft: rgba(230,179,92,0.16);
|
|
37
|
+
--accent: #8b7ff5;
|
|
38
|
+
--accent-dim: rgba(139,127,245,0.5);
|
|
39
|
+
--accent-soft: rgba(139,127,245,0.14);
|
|
40
|
+
--link: #7fb8f5;
|
|
41
|
+
--warn: #e6b35c;
|
|
42
|
+
/* Radii */
|
|
43
|
+
--r-sm: 6px;
|
|
44
|
+
--r: 10px;
|
|
45
|
+
--r-lg: 16px;
|
|
46
|
+
--r-xl: 20px;
|
|
47
|
+
/* Rhythm */
|
|
48
|
+
--pad-y: 18px;
|
|
49
|
+
--pad-x: 22px;
|
|
50
|
+
--row-y: 8px;
|
|
51
|
+
--gap: 16px;
|
|
52
|
+
--fz: 13.5px;
|
|
53
|
+
--fz-sm: 12px;
|
|
37
54
|
--fz-xs: 10.5px;
|
|
38
55
|
--fz-lg: 15px;
|
|
39
56
|
--fz-xl: 22px;
|
|
57
|
+
--shadow-card: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 28px rgba(0,0,0,0.45);
|
|
40
58
|
}
|
|
41
59
|
html[data-density="dense"] {
|
|
42
|
-
--pad-y:
|
|
43
|
-
--fz:
|
|
60
|
+
--pad-y: 12px; --pad-x: 16px; --row-y: 6px; --gap: 12px;
|
|
61
|
+
--fz: 12.5px; --fz-sm: 11.5px; --fz-xs: 10px; --fz-lg: 14px; --fz-xl: 20px;
|
|
44
62
|
}
|
|
45
63
|
html[data-density="comfy"] {
|
|
46
|
-
--pad-y:
|
|
47
|
-
--fz:
|
|
64
|
+
--pad-y: 24px; --pad-x: 28px; --row-y: 12px; --gap: 20px;
|
|
65
|
+
--fz: 14.5px; --fz-sm: 12.5px; --fz-xs: 11px; --fz-lg: 16px; --fz-xl: 24px;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
51
69
|
html, body, #app { height: 100%; }
|
|
52
70
|
body {
|
|
53
|
-
background:
|
|
71
|
+
background:
|
|
72
|
+
radial-gradient(1200px 700px at 70% -10%, rgba(139,127,245,0.07), transparent 60%),
|
|
73
|
+
radial-gradient(900px 600px at 10% 110%, rgba(94,234,212,0.045), transparent 55%),
|
|
74
|
+
linear-gradient(180deg, var(--bg-grad-top), var(--bg) 30%);
|
|
75
|
+
background-attachment: fixed;
|
|
54
76
|
color: var(--fg);
|
|
55
|
-
font: 400 var(--fz)/1.
|
|
56
|
-
font-feature-settings: "
|
|
77
|
+
font: 400 var(--fz)/1.55 "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
78
|
+
font-feature-settings: "cv11" 1, "ss01" 1;
|
|
57
79
|
-webkit-font-smoothing: antialiased;
|
|
80
|
+
text-rendering: optimizeLegibility;
|
|
58
81
|
overflow: hidden;
|
|
59
82
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@keyframes kypCaret { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
|
|
83
|
+
.mono { font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
84
|
+
::selection { background: rgba(139,127,245,0.32); color: #fff; }
|
|
63
85
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
64
86
|
::-webkit-scrollbar-track { background: transparent; }
|
|
65
|
-
::-webkit-scrollbar-thumb { background:
|
|
66
|
-
::-webkit-scrollbar-thumb:hover { background:
|
|
87
|
+
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; border: 2px solid transparent; background-clip: padding-box; }
|
|
88
|
+
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); background-clip: padding-box; }
|
|
67
89
|
button, input, select, textarea { font: inherit; color: inherit; }
|
|
68
90
|
button { background: none; border: 0; padding: 0; cursor: pointer; }
|
|
69
91
|
a, .lnk { color: var(--link); text-decoration: none; }
|
|
@@ -71,24 +93,21 @@ a:hover, .lnk:hover { text-decoration: underline; text-underline-offset: 3px; }
|
|
|
71
93
|
.acc { color: var(--accent); }
|
|
72
94
|
.mut { color: var(--muted); }
|
|
73
95
|
.dim { color: var(--dim); }
|
|
74
|
-
.tab-nums { font-variant-numeric: tabular-nums; }
|
|
96
|
+
.tab-nums { font-variant-numeric: tabular-nums; font-family: "JetBrains Mono", monospace; }
|
|
75
97
|
|
|
76
98
|
/* Layout */
|
|
77
99
|
#app {
|
|
78
100
|
display: grid;
|
|
79
|
-
grid-template-rows:
|
|
101
|
+
grid-template-rows: 52px 1fr 28px;
|
|
80
102
|
height: 100vh;
|
|
81
103
|
min-height: 0;
|
|
82
104
|
}
|
|
83
105
|
#main {
|
|
84
106
|
display: grid;
|
|
85
|
-
grid-template-columns: var(--sidebar-w,
|
|
107
|
+
grid-template-columns: var(--sidebar-w, 248px) auto minmax(0, 1fr);
|
|
86
108
|
min-height: 0;
|
|
87
|
-
border-top: 1px solid var(--line);
|
|
88
|
-
}
|
|
89
|
-
#main.sidebar-hidden {
|
|
90
|
-
grid-template-columns: 0 0 minmax(0, 1fr);
|
|
91
109
|
}
|
|
110
|
+
#main.sidebar-hidden { grid-template-columns: 0 0 minmax(0, 1fr); }
|
|
92
111
|
#main.sidebar-hidden #sidebar,
|
|
93
112
|
#main.sidebar-hidden #resize-handle { display: none; }
|
|
94
113
|
|
|
@@ -99,17 +118,12 @@ a:hover, .lnk:hover { text-decoration: underline; text-underline-offset: 3px; }
|
|
|
99
118
|
cursor: col-resize;
|
|
100
119
|
position: relative;
|
|
101
120
|
z-index: 10;
|
|
102
|
-
transition: background 0.
|
|
103
|
-
}
|
|
104
|
-
#resize-handle::before {
|
|
105
|
-
content: '';
|
|
106
|
-
position: absolute;
|
|
107
|
-
inset: 0 -4px;
|
|
108
|
-
z-index: 1;
|
|
121
|
+
transition: background 0.18s;
|
|
109
122
|
}
|
|
123
|
+
#resize-handle::before { content: ''; position: absolute; inset: 0 -5px; z-index: 1; }
|
|
110
124
|
#resize-handle:hover, #resize-handle.dragging {
|
|
111
125
|
background: var(--accent);
|
|
112
|
-
box-shadow: 0 0
|
|
126
|
+
box-shadow: 0 0 12px rgba(139,127,245,0.5);
|
|
113
127
|
}
|
|
114
128
|
body.resizing { cursor: col-resize !important; user-select: none !important; }
|
|
115
129
|
body.resizing * { cursor: col-resize !important; pointer-events: none !important; }
|
|
@@ -120,471 +134,630 @@ body.resizing #resize-handle { pointer-events: auto !important; }
|
|
|
120
134
|
display: grid;
|
|
121
135
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
122
136
|
align-items: center;
|
|
123
|
-
padding: 0
|
|
124
|
-
gap:
|
|
137
|
+
padding: 0 16px;
|
|
138
|
+
gap: 14px;
|
|
139
|
+
border-bottom: 1px solid var(--line);
|
|
140
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.025), transparent);
|
|
125
141
|
}
|
|
126
142
|
.topbar-left { display: flex; align-items: center; gap: 14px; min-width: 0; }
|
|
127
|
-
.topbar-right { display: flex; align-items: center; gap:
|
|
143
|
+
.topbar-right { display: flex; align-items: center; gap: 10px; }
|
|
128
144
|
.toggle-btn {
|
|
129
|
-
width:
|
|
145
|
+
width: 30px; height: 30px;
|
|
130
146
|
display: inline-flex; align-items: center; justify-content: center;
|
|
131
147
|
color: var(--muted); border-radius: var(--r-sm);
|
|
132
|
-
border: 1px solid transparent;
|
|
148
|
+
border: 1px solid transparent; transition: all 0.15s;
|
|
133
149
|
}
|
|
134
|
-
.toggle-btn:hover { border-color: var(--line); color: var(--fg); }
|
|
150
|
+
.toggle-btn:hover { border-color: var(--line-2); background: var(--panel); color: var(--fg); }
|
|
135
151
|
|
|
136
152
|
/* Logo */
|
|
137
153
|
.logo {
|
|
138
|
-
display: inline-flex; align-items: center; gap:
|
|
139
|
-
font-weight: 700; letter-spacing: 0.
|
|
140
|
-
color: var(--
|
|
154
|
+
display: inline-flex; align-items: center; gap: 9px;
|
|
155
|
+
font-weight: 700; letter-spacing: -0.01em;
|
|
156
|
+
color: var(--fg); font-size: var(--fz-lg);
|
|
141
157
|
white-space: nowrap;
|
|
142
158
|
}
|
|
143
159
|
.logo-mark {
|
|
144
|
-
display: inline-
|
|
145
|
-
|
|
146
|
-
|
|
160
|
+
display: inline-flex; width: 22px; height: 22px;
|
|
161
|
+
border-radius: 7px;
|
|
162
|
+
background: linear-gradient(145deg, var(--c-violet), #6b5fe0);
|
|
163
|
+
box-shadow: 0 0 0 1px rgba(139,127,245,0.4), 0 4px 14px rgba(139,127,245,0.45);
|
|
164
|
+
align-items: center; justify-content: center;
|
|
147
165
|
}
|
|
166
|
+
.logo-mark svg { width: 13px; height: 13px; }
|
|
167
|
+
.logo .lo-sub { color: var(--dim); font-weight: 500; font-size: var(--fz-sm); letter-spacing: 0; margin-left: 2px; }
|
|
148
168
|
|
|
149
169
|
/* Breadcrumb */
|
|
150
|
-
.breadcrumb { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fz-sm); color: var(--muted); }
|
|
151
|
-
.breadcrumb .bc-last { color: var(--fg); }
|
|
170
|
+
.breadcrumb { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fz-sm); color: var(--muted); min-width: 0; overflow: hidden; }
|
|
171
|
+
.breadcrumb .bc-last { color: var(--fg); font-weight: 500; }
|
|
152
172
|
.breadcrumb .bc-sep { color: var(--dim); }
|
|
153
173
|
|
|
154
174
|
/* View switcher */
|
|
155
175
|
.view-switch {
|
|
156
176
|
display: inline-flex; border: 1px solid var(--line);
|
|
157
|
-
border-radius:
|
|
177
|
+
border-radius: 9px; padding: 3px; background: rgba(0,0,0,0.25);
|
|
178
|
+
gap: 2px;
|
|
158
179
|
}
|
|
159
180
|
.view-switch button {
|
|
160
|
-
padding:
|
|
161
|
-
color: var(--muted); background: transparent; border-radius:
|
|
181
|
+
padding: 5px 12px; font-size: var(--fz-sm); font-weight: 500;
|
|
182
|
+
color: var(--muted); background: transparent; border-radius: 6px;
|
|
183
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
184
|
+
transition: all 0.15s;
|
|
162
185
|
}
|
|
186
|
+
.view-switch button svg { width: 13px; height: 13px; opacity: 0.85; }
|
|
187
|
+
.view-switch button:hover { color: var(--fg); }
|
|
163
188
|
.view-switch button.active {
|
|
164
|
-
color:
|
|
165
|
-
background:
|
|
189
|
+
color: #fff;
|
|
190
|
+
background: linear-gradient(180deg, rgba(139,127,245,0.28), rgba(139,127,245,0.16));
|
|
191
|
+
box-shadow: 0 0 0 1px rgba(139,127,245,0.3), 0 2px 8px rgba(139,127,245,0.2);
|
|
166
192
|
}
|
|
167
193
|
|
|
168
194
|
/* Ghost button */
|
|
169
195
|
.ghost-btn {
|
|
170
|
-
padding:
|
|
196
|
+
padding: 5px 11px; border: 1px solid var(--line);
|
|
171
197
|
background: var(--panel); color: var(--muted);
|
|
172
|
-
border-radius: var(--r-sm); font-size: var(--fz-sm);
|
|
173
|
-
white-space: nowrap; transition: all 0.
|
|
198
|
+
border-radius: var(--r-sm); font-size: var(--fz-sm); font-weight: 500;
|
|
199
|
+
white-space: nowrap; transition: all 0.15s;
|
|
200
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
174
201
|
}
|
|
202
|
+
.ghost-btn svg { width: 12px; height: 12px; }
|
|
175
203
|
.ghost-btn:hover { border-color: var(--line-2); background: var(--panel-2); color: var(--fg); }
|
|
204
|
+
.ghost-btn.primary { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-soft); }
|
|
205
|
+
.ghost-btn.primary:hover { background: rgba(139,127,245,0.22); }
|
|
176
206
|
|
|
177
207
|
/* Search bar */
|
|
178
208
|
.search-bar {
|
|
179
209
|
display: inline-flex; align-items: center; gap: 8px;
|
|
180
|
-
padding:
|
|
181
|
-
background:
|
|
182
|
-
width:
|
|
183
|
-
|
|
184
|
-
|
|
210
|
+
padding: 6px 10px; border: 1px solid var(--line);
|
|
211
|
+
background: rgba(0,0,0,0.25); border-radius: 9px;
|
|
212
|
+
width: 244px; font-size: var(--fz-sm); position: relative;
|
|
213
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
214
|
+
}
|
|
215
|
+
.search-bar:focus-within { border-color: var(--accent-dim); box-shadow: 0 0 0 3px rgba(139,127,245,0.12); }
|
|
216
|
+
.search-bar .s-ico { color: var(--dim); display: inline-flex; }
|
|
217
|
+
.search-bar .s-ico svg { width: 14px; height: 14px; }
|
|
185
218
|
.search-bar input {
|
|
186
219
|
flex: 1; min-width: 0; background: transparent; border: 0; outline: 0;
|
|
187
220
|
color: var(--fg); font-size: var(--fz-sm); caret-color: var(--accent);
|
|
188
221
|
}
|
|
189
222
|
.search-bar input::placeholder { color: var(--dim); }
|
|
190
|
-
.search-bar .s-caret {
|
|
191
|
-
width: 6px; height: 12px; background: var(--accent);
|
|
192
|
-
opacity: 0.6; margin-right: 2px;
|
|
193
|
-
animation: kypCaret 1.05s steps(1) infinite;
|
|
194
|
-
}
|
|
195
223
|
.search-bar .s-hint {
|
|
196
224
|
font-size: var(--fz-xs); border: 1px solid var(--line);
|
|
197
|
-
border-radius:
|
|
225
|
+
border-radius: 5px; padding: 1px 6px; color: var(--dim);
|
|
198
226
|
font-variant-numeric: tabular-nums;
|
|
199
227
|
}
|
|
200
228
|
.search-dropdown {
|
|
201
|
-
position: absolute; top: calc(100% +
|
|
202
|
-
background: var(--panel); border: 1px solid var(--line);
|
|
203
|
-
border-radius: var(--r); max-height:
|
|
204
|
-
z-index: 100; display: none; box-shadow: 0
|
|
229
|
+
position: absolute; top: calc(100% + 8px); left: 0; right: 0;
|
|
230
|
+
background: var(--panel-solid); border: 1px solid var(--line-2);
|
|
231
|
+
border-radius: var(--r); max-height: 360px; overflow-y: auto;
|
|
232
|
+
z-index: 100; display: none; box-shadow: 0 16px 48px rgba(0,0,0,0.6);
|
|
233
|
+
backdrop-filter: blur(12px);
|
|
205
234
|
}
|
|
206
235
|
.search-dropdown.active { display: block; }
|
|
207
236
|
.search-result {
|
|
208
|
-
padding:
|
|
209
|
-
transition: background 0.
|
|
237
|
+
padding: 10px 14px; cursor: pointer; border-bottom: 1px solid var(--line);
|
|
238
|
+
transition: background 0.12s;
|
|
210
239
|
}
|
|
211
240
|
.search-result:last-child { border-bottom: none; }
|
|
212
241
|
.search-result:hover { background: var(--panel-2); }
|
|
213
|
-
.search-result .sr-title { font-size: var(--fz-sm); color: var(--
|
|
214
|
-
.search-result .sr-path { font-size: var(--fz-xs); color: var(--dim); margin-top:
|
|
215
|
-
.search-result .sr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top:
|
|
242
|
+
.search-result .sr-title { font-size: var(--fz-sm); color: var(--fg); font-weight: 600; }
|
|
243
|
+
.search-result .sr-path { font-size: var(--fz-xs); color: var(--dim); margin-top: 3px; font-family: "JetBrains Mono", monospace; }
|
|
244
|
+
.search-result .sr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top: 4px; }
|
|
216
245
|
|
|
217
246
|
/* Sidebar */
|
|
218
247
|
#sidebar {
|
|
219
248
|
display: flex; flex-direction: column;
|
|
220
|
-
background:
|
|
249
|
+
background: rgba(255,255,255,0.012);
|
|
250
|
+
border-right: 1px solid var(--line);
|
|
251
|
+
overflow: hidden; min-height: 0;
|
|
221
252
|
}
|
|
222
253
|
.sidebar-scroll {
|
|
223
|
-
overflow-y: auto; padding:
|
|
224
|
-
display: flex; flex-direction: column; gap:
|
|
254
|
+
overflow-y: auto; padding: 16px 8px 16px 16px;
|
|
255
|
+
display: flex; flex-direction: column; gap: 22px;
|
|
225
256
|
min-height: 0; flex: 1;
|
|
226
257
|
}
|
|
227
258
|
.side-section-header {
|
|
228
259
|
display: flex; align-items: center; justify-content: space-between;
|
|
229
|
-
padding: 0 0
|
|
260
|
+
padding: 0 0 10px;
|
|
230
261
|
}
|
|
231
262
|
.side-section-title {
|
|
232
|
-
display: inline-flex; align-items: center; gap:
|
|
233
|
-
font-size: var(--fz-xs); letter-spacing: 0.
|
|
234
|
-
text-transform: uppercase; color: var(--dim);
|
|
235
|
-
}
|
|
236
|
-
.side-dot {
|
|
237
|
-
display: inline-block; width: 6px; height: 6px; border-radius: 999px;
|
|
263
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
264
|
+
font-size: var(--fz-xs); letter-spacing: 0.12em;
|
|
265
|
+
text-transform: uppercase; color: var(--dim); font-weight: 600;
|
|
238
266
|
}
|
|
267
|
+
.side-dot { display: inline-block; width: 7px; height: 7px; border-radius: 999px; }
|
|
239
268
|
.side-add-btn {
|
|
240
|
-
font-size: var(--fz-xs); color: var(--dim); padding:
|
|
269
|
+
font-size: var(--fz-xs); color: var(--dim); padding: 2px 7px;
|
|
241
270
|
border-radius: var(--r-sm); border: 1px solid transparent;
|
|
242
|
-
transition: all 0.
|
|
243
|
-
}
|
|
244
|
-
.side-add-btn:hover { border-color: var(--line); color: var(--fg); }
|
|
245
|
-
.side-collapse-arrow {
|
|
246
|
-
display: inline-block; transition: transform 0.15s; margin-left: 4px;
|
|
271
|
+
transition: all 0.15s; font-weight: 500;
|
|
247
272
|
}
|
|
273
|
+
.side-add-btn:hover { border-color: var(--line-2); background: var(--panel); color: var(--fg); }
|
|
274
|
+
.side-collapse-arrow { display: inline-block; transition: transform 0.18s; margin-left: 4px; }
|
|
248
275
|
.side-collapse-arrow.collapsed { transform: rotate(-90deg); }
|
|
276
|
+
.side-collapse-arrow svg { width: 11px; height: 11px; }
|
|
249
277
|
|
|
250
278
|
/* Sidebar search */
|
|
251
279
|
.side-search {
|
|
252
|
-
width: 100%; background:
|
|
280
|
+
width: 100%; background: rgba(0,0,0,0.22);
|
|
253
281
|
border: 1px solid var(--line); border-radius: var(--r-sm);
|
|
254
|
-
padding:
|
|
255
|
-
color: var(--fg); outline: 0; margin-bottom:
|
|
282
|
+
padding: 7px 10px; font-size: var(--fz-sm);
|
|
283
|
+
color: var(--fg); outline: 0; margin-bottom: 8px;
|
|
284
|
+
transition: border-color 0.15s;
|
|
256
285
|
}
|
|
257
286
|
.side-search::placeholder { color: var(--dim); }
|
|
258
287
|
.side-search:focus { border-color: var(--accent-dim); }
|
|
259
288
|
|
|
260
289
|
/* Tree folder header */
|
|
261
290
|
.tree-folder {
|
|
262
|
-
display: flex; align-items: center; gap:
|
|
263
|
-
padding:
|
|
264
|
-
user-select: none;
|
|
291
|
+
display: flex; align-items: center; gap: 7px;
|
|
292
|
+
padding: 5px 6px; font-size: var(--fz-sm); cursor: pointer;
|
|
293
|
+
user-select: none; border-radius: var(--r-sm); transition: background 0.12s;
|
|
265
294
|
}
|
|
266
|
-
.tree-folder
|
|
295
|
+
.tree-folder:hover { background: var(--panel); }
|
|
296
|
+
.tree-folder .tf-arrow { color: var(--dim); transition: transform 0.18s; display: inline-flex; }
|
|
297
|
+
.tree-folder .tf-arrow svg { width: 10px; height: 10px; }
|
|
267
298
|
.tree-folder .tf-arrow.closed { transform: rotate(-90deg); }
|
|
268
|
-
.tree-folder .tf-icon { color: var(--muted);
|
|
269
|
-
.tree-folder .tf-
|
|
299
|
+
.tree-folder .tf-icon { color: var(--muted); display: inline-flex; }
|
|
300
|
+
.tree-folder .tf-icon svg { width: 13px; height: 13px; }
|
|
301
|
+
.tree-folder .tf-label { flex: 1; color: var(--fg); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
270
302
|
.tree-folder .tf-count { font-size: var(--fz-xs); color: var(--dim); font-variant-numeric: tabular-nums; }
|
|
271
|
-
.tree-folder .tf-graph-btn {
|
|
303
|
+
.tree-folder .tf-graph-btn { color: var(--dim); opacity: 0; transition: opacity 0.15s; padding: 0 2px; display: inline-flex; }
|
|
304
|
+
.tree-folder .tf-graph-btn svg { width: 13px; height: 13px; }
|
|
272
305
|
.tree-folder:hover .tf-graph-btn { opacity: 1; }
|
|
273
306
|
.tree-folder .tf-graph-btn:hover { color: var(--accent); }
|
|
274
307
|
|
|
275
308
|
/* Sidebar rows */
|
|
276
309
|
.sidebar-row {
|
|
277
|
-
display: flex; align-items: center; gap:
|
|
278
|
-
width: 100%; text-align: left; padding:
|
|
310
|
+
display: flex; align-items: center; gap: 9px;
|
|
311
|
+
width: 100%; text-align: left; padding: 5px 8px 5px 8px;
|
|
279
312
|
border-radius: var(--r-sm); border-left: 2px solid transparent;
|
|
280
|
-
background: transparent; transition: background 0.
|
|
313
|
+
background: transparent; transition: background 0.12s, border-color 0.12s;
|
|
281
314
|
}
|
|
282
315
|
.sidebar-row:hover { background: var(--panel); }
|
|
283
316
|
.sidebar-row.active {
|
|
284
317
|
border-left-color: var(--accent);
|
|
285
|
-
background:
|
|
318
|
+
background: var(--accent-soft);
|
|
286
319
|
}
|
|
287
|
-
.sidebar-row .sr-
|
|
320
|
+
.sidebar-row .sr-ico { display: inline-flex; color: var(--dim); }
|
|
321
|
+
.sidebar-row .sr-ico svg { width: 12px; height: 12px; }
|
|
288
322
|
.sidebar-row .sr-label { flex: 1; font-size: var(--fz-sm); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
289
|
-
.sidebar-row.active .sr-label { color: var(--
|
|
323
|
+
.sidebar-row.active .sr-label { color: var(--fg); }
|
|
324
|
+
.sidebar-row.active .sr-ico { color: var(--accent); }
|
|
290
325
|
|
|
291
326
|
/* Tag chip */
|
|
292
327
|
.tag-chip {
|
|
293
|
-
display: inline-flex; align-items: center; gap:
|
|
294
|
-
padding:
|
|
328
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
329
|
+
padding: 3px 9px; border: 1px solid var(--line);
|
|
295
330
|
background: var(--panel); color: var(--muted);
|
|
296
|
-
border-radius:
|
|
297
|
-
white-space: nowrap; transition:
|
|
331
|
+
border-radius: 999px; font-size: var(--fz-sm); font-weight: 500;
|
|
332
|
+
white-space: nowrap; transition: all 0.15s;
|
|
298
333
|
}
|
|
299
334
|
.tag-chip:hover { border-color: var(--line-2); color: var(--fg); }
|
|
300
|
-
.tag-chip.active { border-color: var(--accent-dim); background:
|
|
335
|
+
.tag-chip.active { border-color: var(--accent-dim); background: var(--accent-soft); color: var(--accent); }
|
|
301
336
|
.tag-chip .tc-hash { color: var(--dim); }
|
|
337
|
+
.tag-chip.active .tc-hash { color: var(--accent); }
|
|
302
338
|
.tag-chip .tc-count { margin-left: 2px; font-size: var(--fz-xs); color: var(--dim); font-variant-numeric: tabular-nums; }
|
|
303
339
|
|
|
304
340
|
/* StatusBar */
|
|
305
341
|
#statusbar {
|
|
306
342
|
display: flex; align-items: center; justify-content: space-between;
|
|
307
|
-
padding: 0
|
|
308
|
-
border-top: 1px solid var(--line); background:
|
|
343
|
+
padding: 0 16px; font-size: var(--fz-xs); color: var(--dim);
|
|
344
|
+
border-top: 1px solid var(--line); background: rgba(0,0,0,0.2);
|
|
309
345
|
}
|
|
310
346
|
.sb-left, .sb-right, .sb-center { display: flex; gap: 14px; align-items: center; }
|
|
311
|
-
.sb-
|
|
347
|
+
.sb-left .mut, .sb-right .mut, .sb-center .mut { color: var(--muted); }
|
|
348
|
+
.sb-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--c-teal); display: inline-block; margin-right: 6px; box-shadow: 0 0 6px rgba(94,234,212,0.6); }
|
|
312
349
|
|
|
313
350
|
/* Content area */
|
|
314
351
|
#content-area {
|
|
315
352
|
padding: var(--pad-y) var(--pad-x);
|
|
316
353
|
min-height: 0; min-width: 0;
|
|
317
|
-
border-left: 1px solid var(--line);
|
|
318
354
|
overflow: hidden; display: flex; flex-direction: column;
|
|
319
355
|
}
|
|
320
356
|
|
|
357
|
+
/* ===== DASHBOARD ===== */
|
|
358
|
+
.dash {
|
|
359
|
+
overflow-y: auto; height: 100%; min-height: 0; padding-right: 6px;
|
|
360
|
+
}
|
|
361
|
+
.dash-head {
|
|
362
|
+
display: flex; align-items: flex-end; justify-content: space-between;
|
|
363
|
+
margin-bottom: 20px; gap: 16px; flex-wrap: wrap;
|
|
364
|
+
}
|
|
365
|
+
.dash-title { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; }
|
|
366
|
+
.dash-sub { font-size: var(--fz-sm); color: var(--dim); margin-top: 4px; }
|
|
367
|
+
.dash-head-actions { display: flex; gap: 8px; }
|
|
368
|
+
|
|
369
|
+
.hero-grid {
|
|
370
|
+
display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap);
|
|
371
|
+
margin-bottom: var(--gap);
|
|
372
|
+
}
|
|
373
|
+
@media (max-width: 1100px) { .hero-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
374
|
+
|
|
375
|
+
.stat-card {
|
|
376
|
+
position: relative; overflow: hidden;
|
|
377
|
+
background: var(--glass);
|
|
378
|
+
border: 1px solid var(--line);
|
|
379
|
+
border-radius: var(--r-lg);
|
|
380
|
+
padding: 18px 18px 14px;
|
|
381
|
+
box-shadow: var(--shadow-card);
|
|
382
|
+
transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
|
|
383
|
+
}
|
|
384
|
+
.stat-card:hover { transform: translateY(-3px); border-color: var(--line-2); box-shadow: 0 1px 0 rgba(255,255,255,0.05) inset, 0 14px 38px rgba(0,0,0,0.5); }
|
|
385
|
+
.stat-card .glow {
|
|
386
|
+
position: absolute; left: 50%; bottom: -60%; transform: translateX(-50%);
|
|
387
|
+
width: 150%; height: 130%; pointer-events: none; opacity: 0.9;
|
|
388
|
+
border-radius: 50%;
|
|
389
|
+
}
|
|
390
|
+
.stat-card[data-accent="violet"] .glow { background: radial-gradient(closest-side, rgba(139,127,245,0.4), transparent 70%); }
|
|
391
|
+
.stat-card[data-accent="teal"] .glow { background: radial-gradient(closest-side, rgba(94,234,212,0.32), transparent 70%); }
|
|
392
|
+
.stat-card[data-accent="amber"] .glow { background: radial-gradient(closest-side, rgba(230,179,92,0.32), transparent 70%); }
|
|
393
|
+
.stat-card .sc-head { display: flex; align-items: center; justify-content: space-between; position: relative; z-index: 1; }
|
|
394
|
+
.stat-card .sc-label { font-size: var(--fz-xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
|
|
395
|
+
.stat-card .sc-ico {
|
|
396
|
+
width: 30px; height: 30px; border-radius: 9px;
|
|
397
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
398
|
+
border: 1px solid var(--line-2);
|
|
399
|
+
}
|
|
400
|
+
.stat-card[data-accent="violet"] .sc-ico { color: var(--c-violet); background: var(--c-violet-soft); }
|
|
401
|
+
.stat-card[data-accent="teal"] .sc-ico { color: var(--c-teal); background: var(--c-teal-soft); }
|
|
402
|
+
.stat-card[data-accent="amber"] .sc-ico { color: var(--c-amber); background: var(--c-amber-soft); }
|
|
403
|
+
.stat-card .sc-ico svg { width: 15px; height: 15px; }
|
|
404
|
+
.stat-card .sc-value {
|
|
405
|
+
position: relative; z-index: 1;
|
|
406
|
+
font-size: 46px; font-weight: 700; line-height: 1.05; letter-spacing: -0.03em;
|
|
407
|
+
margin: 14px 0 2px; font-family: "JetBrains Mono", monospace; font-feature-settings: "tnum" 1;
|
|
408
|
+
}
|
|
409
|
+
.stat-card[data-accent="violet"] .sc-value { color: var(--c-violet); }
|
|
410
|
+
.stat-card[data-accent="teal"] .sc-value { color: var(--c-teal); }
|
|
411
|
+
.stat-card[data-accent="amber"] .sc-value { color: var(--c-amber); }
|
|
412
|
+
.stat-card .sc-foot { position: relative; z-index: 1; display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; margin-top: 6px; }
|
|
413
|
+
.stat-card .sc-meta { font-size: var(--fz-xs); color: var(--dim); }
|
|
414
|
+
.stat-card .sc-spark { width: 96px; height: 34px; flex-shrink: 0; }
|
|
415
|
+
|
|
416
|
+
/* Dashboard panels */
|
|
417
|
+
.dash-grid {
|
|
418
|
+
display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: var(--gap);
|
|
419
|
+
margin-bottom: var(--gap);
|
|
420
|
+
}
|
|
421
|
+
@media (max-width: 1100px) { .dash-grid { grid-template-columns: 1fr; } }
|
|
422
|
+
|
|
423
|
+
.panel {
|
|
424
|
+
background: var(--glass);
|
|
425
|
+
border: 1px solid var(--line);
|
|
426
|
+
border-radius: var(--r-lg);
|
|
427
|
+
box-shadow: var(--shadow-card);
|
|
428
|
+
overflow: hidden;
|
|
429
|
+
}
|
|
430
|
+
.panel-head {
|
|
431
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
432
|
+
padding: 16px 18px 12px;
|
|
433
|
+
}
|
|
434
|
+
.panel-title { font-size: var(--fz); font-weight: 600; display: flex; align-items: center; gap: 9px; }
|
|
435
|
+
.panel-title .pt-ico { color: var(--muted); display: inline-flex; }
|
|
436
|
+
.panel-title .pt-ico svg { width: 15px; height: 15px; }
|
|
437
|
+
.panel-body { padding: 0 18px 18px; }
|
|
438
|
+
|
|
439
|
+
/* Chart */
|
|
440
|
+
.chart-wrap { position: relative; width: 100%; height: 230px; }
|
|
441
|
+
.chart-wrap svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
442
|
+
.chart-legend { display: flex; gap: 16px; padding: 0 18px 14px; }
|
|
443
|
+
.chart-legend .cl-item { display: flex; align-items: center; gap: 7px; font-size: var(--fz-xs); color: var(--muted); }
|
|
444
|
+
.chart-legend .cl-swatch { width: 16px; height: 3px; border-radius: 3px; }
|
|
445
|
+
.chart-tooltip {
|
|
446
|
+
position: absolute; pointer-events: none; opacity: 0; transition: opacity 0.12s;
|
|
447
|
+
background: var(--panel-solid-2); border: 1px solid var(--line-2);
|
|
448
|
+
border-radius: 8px; padding: 7px 10px; font-size: var(--fz-xs);
|
|
449
|
+
white-space: nowrap; z-index: 5; box-shadow: 0 8px 20px rgba(0,0,0,0.5);
|
|
450
|
+
}
|
|
451
|
+
.chart-tooltip .tt-title { color: var(--fg); font-weight: 600; margin-bottom: 3px; }
|
|
452
|
+
.chart-tooltip .tt-row { color: var(--muted); display: flex; align-items: center; gap: 6px; }
|
|
453
|
+
.chart-tooltip .tt-dot { width: 7px; height: 7px; border-radius: 999px; }
|
|
454
|
+
|
|
455
|
+
/* Token economics bars */
|
|
456
|
+
.econ-row { margin-bottom: 16px; }
|
|
457
|
+
.econ-row:last-child { margin-bottom: 0; }
|
|
458
|
+
.econ-top { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 7px; }
|
|
459
|
+
.econ-label { font-size: var(--fz-sm); color: var(--muted); font-weight: 500; }
|
|
460
|
+
.econ-val { font-size: var(--fz-sm); color: var(--fg); font-weight: 600; font-variant-numeric: tabular-nums; font-family: "JetBrains Mono", monospace; }
|
|
461
|
+
.bar-track { height: 7px; border-radius: 999px; background: rgba(255,255,255,0.06); overflow: hidden; }
|
|
462
|
+
.bar-fill { height: 100%; border-radius: 999px; transition: width 0.6s cubic-bezier(.22,1,.36,1); }
|
|
463
|
+
.bar-fill.violet { background: linear-gradient(90deg, #6b5fe0, var(--c-violet)); }
|
|
464
|
+
.bar-fill.teal { background: linear-gradient(90deg, #2bb59c, var(--c-teal)); }
|
|
465
|
+
.bar-fill.amber { background: linear-gradient(90deg, #c9913f, var(--c-amber)); }
|
|
466
|
+
|
|
467
|
+
/* Recent list */
|
|
468
|
+
.recent-list { display: flex; flex-direction: column; }
|
|
469
|
+
.recent-item {
|
|
470
|
+
display: flex; align-items: center; gap: 12px;
|
|
471
|
+
padding: 11px 8px; border-radius: var(--r-sm);
|
|
472
|
+
cursor: pointer; transition: background 0.12s; text-align: left; width: 100%;
|
|
473
|
+
}
|
|
474
|
+
.recent-item:hover { background: var(--panel); }
|
|
475
|
+
.recent-item .ri-ico {
|
|
476
|
+
width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
|
|
477
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
478
|
+
border: 1px solid var(--line-2); color: var(--c-teal); background: var(--c-teal-soft);
|
|
479
|
+
}
|
|
480
|
+
.recent-item .ri-ico.proj { color: var(--c-violet); background: var(--c-violet-soft); }
|
|
481
|
+
.recent-item .ri-ico svg { width: 15px; height: 15px; }
|
|
482
|
+
.recent-item .ri-body { flex: 1; min-width: 0; }
|
|
483
|
+
.recent-item .ri-title { font-size: var(--fz-sm); color: var(--fg); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
484
|
+
.recent-item .ri-sub { font-size: var(--fz-xs); color: var(--dim); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
485
|
+
.recent-item .ri-meta { font-size: var(--fz-xs); color: var(--dim); flex-shrink: 0; font-variant-numeric: tabular-nums; }
|
|
486
|
+
.dash-empty { padding: 28px; text-align: center; color: var(--dim); font-size: var(--fz-sm); }
|
|
487
|
+
|
|
321
488
|
/* Note view */
|
|
322
489
|
.note-view {
|
|
323
|
-
display: grid; grid-template-columns: minmax(0,1fr)
|
|
490
|
+
display: grid; grid-template-columns: minmax(0,1fr) 312px;
|
|
324
491
|
gap: var(--gap); height: 100%; min-height: 0;
|
|
325
492
|
}
|
|
326
493
|
.note-view.no-rail { grid-template-columns: minmax(0,1fr); }
|
|
327
|
-
.note-article { overflow-y: auto; min-width: 0; padding-right:
|
|
494
|
+
.note-article { overflow-y: auto; min-width: 0; padding-right: 8px; }
|
|
328
495
|
.note-rail { display: flex; flex-direction: column; gap: var(--gap); overflow-y: auto; padding-bottom: 8px; }
|
|
329
496
|
|
|
330
|
-
/* Command prompt strip */
|
|
331
|
-
.cmd-strip {
|
|
332
|
-
display: flex; align-items: center; gap: 8px;
|
|
333
|
-
font-size: var(--fz-xs); color: var(--dim);
|
|
334
|
-
padding: 2px 0 10px; font-variant-numeric: tabular-nums;
|
|
335
|
-
}
|
|
336
|
-
.cmd-strip .cs-right { margin-left: auto; }
|
|
337
|
-
|
|
338
497
|
/* Note title */
|
|
339
498
|
.note-title {
|
|
340
|
-
margin:
|
|
341
|
-
letter-spacing: -0.
|
|
342
|
-
}
|
|
343
|
-
.note-title .title-caret {
|
|
344
|
-
display: inline-block; width: 0.5em; height: 0.85em;
|
|
345
|
-
background: var(--accent); margin-left: 6px;
|
|
346
|
-
vertical-align: -0.08em; opacity: 0.6;
|
|
347
|
-
animation: kypBlink 1.05s steps(1) infinite;
|
|
499
|
+
margin: 14px 0 6px; font-size: calc(var(--fz-xl) + 12px);
|
|
500
|
+
letter-spacing: -0.025em; font-weight: 700; line-height: 1.12;
|
|
348
501
|
}
|
|
349
502
|
.note-meta {
|
|
350
503
|
font-size: var(--fz-xs); color: var(--dim);
|
|
351
|
-
margin-bottom:
|
|
504
|
+
margin-bottom: 26px; display: flex; gap: 16px; align-items: center;
|
|
352
505
|
}
|
|
353
506
|
.note-tags-strip {
|
|
354
507
|
display: flex; align-items: center; justify-content: space-between;
|
|
355
|
-
gap: 10px; padding: 0 0
|
|
508
|
+
gap: 10px; padding: 0 0 16px; flex-wrap: wrap;
|
|
356
509
|
}
|
|
357
|
-
.note-tags-left { display: flex; gap:
|
|
358
|
-
.note-tags-right { display: flex; gap:
|
|
510
|
+
.note-tags-left { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }
|
|
511
|
+
.note-tags-right { display: flex; gap: 7px; }
|
|
359
512
|
.note-dates { font-size: var(--fz-xs); color: var(--dim); margin-left: 8px; }
|
|
360
513
|
|
|
361
514
|
/* Markdown body */
|
|
362
|
-
.md-body h1 { font-size: calc(var(--fz-xl) +
|
|
363
|
-
.md-body h2 {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
.md-body
|
|
368
|
-
.md-body
|
|
369
|
-
.md-body ul, .md-body ol { padding-left: 22px; margin: 0 0 14px; }
|
|
370
|
-
.md-body li { line-height: 1.65; color: var(--muted); font-size: var(--fz); margin: 4px 0; }
|
|
371
|
-
.md-body li::marker { color: var(--dim); }
|
|
515
|
+
.md-body h1 { font-size: calc(var(--fz-xl) + 6px); font-weight: 700; margin: 34px 0 16px; color: var(--fg); letter-spacing: -0.02em; }
|
|
516
|
+
.md-body h2 { margin: 34px 0 16px; font-size: calc(var(--fz-lg) + 5px); font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
|
|
517
|
+
.md-body h3 { margin: 28px 0 12px; font-size: var(--fz-lg); font-weight: 600; color: var(--accent); }
|
|
518
|
+
.md-body p { margin: 0 0 16px; line-height: 1.72; color: var(--muted); font-size: var(--fz); }
|
|
519
|
+
.md-body ul, .md-body ol { padding-left: 24px; margin: 0 0 16px; }
|
|
520
|
+
.md-body li { line-height: 1.72; color: var(--muted); font-size: var(--fz); margin: 5px 0; }
|
|
521
|
+
.md-body li::marker { color: var(--accent-dim); }
|
|
372
522
|
.md-body a { color: var(--link); text-decoration: none; }
|
|
373
523
|
.md-body a:hover { text-decoration: underline; text-underline-offset: 3px; }
|
|
374
|
-
.md-body strong { color: var(--fg); font-weight:
|
|
524
|
+
.md-body strong { color: var(--fg); font-weight: 600; }
|
|
375
525
|
.md-body code {
|
|
376
|
-
font-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
526
|
+
font-family: "JetBrains Mono", monospace;
|
|
527
|
+
font-size: 0.88em; padding: 2px 6px; border-radius: var(--r-sm);
|
|
528
|
+
background: rgba(139,127,245,0.12);
|
|
529
|
+
color: #b7adfb;
|
|
530
|
+
border: 1px solid rgba(139,127,245,0.22);
|
|
380
531
|
}
|
|
381
532
|
.md-body pre {
|
|
382
|
-
background:
|
|
383
|
-
border-radius: var(--r); padding:
|
|
384
|
-
overflow-x: auto; margin:
|
|
385
|
-
}
|
|
386
|
-
.md-body pre code { background: none; padding: 0; border: none; color: var(--muted); font-size:
|
|
387
|
-
.md-body table { width: 100%; border-collapse: collapse; margin:
|
|
388
|
-
.md-body th { text-align: left; padding:
|
|
389
|
-
.md-body td { padding:
|
|
533
|
+
background: rgba(0,0,0,0.32); border: 1px solid var(--line);
|
|
534
|
+
border-radius: var(--r); padding: 16px 18px;
|
|
535
|
+
overflow-x: auto; margin: 16px 0;
|
|
536
|
+
}
|
|
537
|
+
.md-body pre code { font-family: "JetBrains Mono", monospace; background: none; padding: 0; border: none; color: var(--muted); font-size: 0.86em; line-height: 1.7; }
|
|
538
|
+
.md-body table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: var(--fz-sm); }
|
|
539
|
+
.md-body th { text-align: left; padding: 9px 13px; border-bottom: 1px solid var(--line-2); color: var(--fg); font-weight: 600; }
|
|
540
|
+
.md-body td { padding: 9px 13px; border-bottom: 1px solid var(--line); color: var(--muted); }
|
|
390
541
|
.md-body tr:last-child td { border-bottom: none; }
|
|
391
|
-
.md-body blockquote { border-left: 2px solid var(--accent); padding: 8px
|
|
392
|
-
.md-body hr { border: none; height: 1px; background: var(--line); margin:
|
|
542
|
+
.md-body blockquote { border-left: 2px solid var(--accent); padding: 8px 18px; margin: 16px 0; color: var(--muted); background: var(--accent-soft); border-radius: 0 var(--r-sm) var(--r-sm) 0; }
|
|
543
|
+
.md-body hr { border: none; height: 1px; background: var(--line-2); margin: 28px 0; }
|
|
393
544
|
.wikilink { color: var(--accent); cursor: pointer; border-bottom: 1px dashed var(--accent-dim); font-weight: 500; }
|
|
394
545
|
.wikilink:hover { border-bottom-style: solid; }
|
|
395
546
|
|
|
396
547
|
/* Rail cards */
|
|
397
|
-
.rail-card {
|
|
548
|
+
.rail-card {
|
|
549
|
+
background: var(--glass); border: 1px solid var(--line);
|
|
550
|
+
border-radius: var(--r-lg); box-shadow: var(--shadow-card);
|
|
551
|
+
overflow: hidden;
|
|
552
|
+
}
|
|
398
553
|
.rail-card-header {
|
|
399
554
|
display: flex; align-items: center; justify-content: space-between;
|
|
400
|
-
padding:
|
|
555
|
+
padding: 13px 14px 11px;
|
|
401
556
|
}
|
|
402
557
|
.rail-card-title {
|
|
403
|
-
display: flex; align-items: center; gap:
|
|
404
|
-
font-size: var(--fz-xs); letter-spacing: 0.
|
|
405
|
-
text-transform: uppercase; color: var(--
|
|
406
|
-
}
|
|
407
|
-
.rail-card-body {
|
|
408
|
-
border: 1px solid var(--line); background: var(--panel);
|
|
409
|
-
border-radius: var(--r); padding: 10px 12px;
|
|
558
|
+
display: flex; align-items: center; gap: 7px;
|
|
559
|
+
font-size: var(--fz-xs); letter-spacing: 0.1em;
|
|
560
|
+
text-transform: uppercase; color: var(--muted); font-weight: 600;
|
|
410
561
|
}
|
|
562
|
+
.rail-card-title svg { width: 13px; height: 13px; }
|
|
563
|
+
.rail-card-body { padding: 0 14px 14px; }
|
|
411
564
|
.rail-icon-btn {
|
|
412
|
-
width:
|
|
565
|
+
width: 26px; height: 24px;
|
|
413
566
|
border-radius: var(--r-sm); border: 1px solid var(--line);
|
|
414
|
-
color: var(--muted);
|
|
415
|
-
|
|
567
|
+
color: var(--muted); display: inline-flex; align-items: center; justify-content: center;
|
|
568
|
+
transition: all 0.15s;
|
|
416
569
|
}
|
|
417
|
-
.rail-icon-btn
|
|
570
|
+
.rail-icon-btn svg { width: 13px; height: 13px; }
|
|
571
|
+
.rail-icon-btn:hover { color: var(--fg); border-color: var(--line-2); background: var(--panel); }
|
|
418
572
|
|
|
419
573
|
/* Outline */
|
|
420
574
|
.outline-item {
|
|
421
|
-
display: block; font-size: var(--fz-sm); padding:
|
|
575
|
+
display: block; font-size: var(--fz-sm); padding: 4px 6px;
|
|
422
576
|
color: var(--muted); font-weight: 400; text-decoration: none;
|
|
423
577
|
border-radius: var(--r-sm); cursor: pointer;
|
|
424
578
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
579
|
+
transition: background 0.12s;
|
|
425
580
|
}
|
|
426
|
-
.outline-item:hover { background: var(--panel
|
|
427
|
-
.outline-item.lv1 { color: var(--fg); font-weight:
|
|
581
|
+
.outline-item:hover { background: var(--panel); color: var(--fg); }
|
|
582
|
+
.outline-item.lv1 { color: var(--fg); font-weight: 600; }
|
|
428
583
|
|
|
429
584
|
/* Backlink items */
|
|
430
585
|
.bl-item {
|
|
431
586
|
display: flex; align-items: center; gap: 8px;
|
|
432
|
-
padding:
|
|
587
|
+
padding: 6px 4px; font-size: var(--fz-sm); color: var(--muted); cursor: pointer;
|
|
588
|
+
border-radius: var(--r-sm); transition: background 0.12s;
|
|
433
589
|
}
|
|
434
|
-
.bl-item:hover { color: var(--fg); }
|
|
590
|
+
.bl-item:hover { color: var(--fg); background: var(--panel); }
|
|
591
|
+
.bl-item svg { width: 12px; height: 12px; color: var(--dim); flex-shrink: 0; }
|
|
435
592
|
|
|
436
593
|
/* Session view */
|
|
437
|
-
.session-view { overflow-y: auto; height: 100%; min-height: 0; padding-right:
|
|
594
|
+
.session-view { overflow-y: auto; height: 100%; min-height: 0; padding-right: 8px; }
|
|
438
595
|
.session-status {
|
|
439
|
-
display: inline-flex; align-items: center; gap:
|
|
440
|
-
padding:
|
|
596
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
597
|
+
padding: 4px 11px; border-radius: 999px; font-size: var(--fz-sm); font-weight: 500;
|
|
441
598
|
}
|
|
442
|
-
.session-status.live { border: 1px solid var(--accent-dim); background:
|
|
443
|
-
.session-status.done { border: 1px solid var(--line); background: var(--panel); color: var(--muted); }
|
|
599
|
+
.session-status.live { border: 1px solid var(--accent-dim); background: var(--accent-soft); color: var(--accent); }
|
|
600
|
+
.session-status.done { border: 1px solid var(--line-2); background: var(--panel); color: var(--muted); }
|
|
444
601
|
|
|
445
602
|
/* Session sections */
|
|
446
603
|
.session-section {
|
|
447
|
-
margin-bottom:
|
|
448
|
-
|
|
604
|
+
margin-bottom: var(--gap);
|
|
605
|
+
background: var(--glass);
|
|
606
|
+
border: 1px solid var(--line);
|
|
607
|
+
border-radius: var(--r-lg); overflow: hidden;
|
|
608
|
+
box-shadow: var(--shadow-card);
|
|
449
609
|
}
|
|
450
610
|
.ss-header {
|
|
451
|
-
padding:
|
|
452
|
-
display: flex; align-items: center; gap:
|
|
453
|
-
border-bottom: 1px solid var(--line);
|
|
454
|
-
}
|
|
455
|
-
.ss-
|
|
456
|
-
.ss-
|
|
457
|
-
.ss-body
|
|
458
|
-
.ss-body.md-body
|
|
459
|
-
.
|
|
460
|
-
.
|
|
611
|
+
padding: 13px 18px; font-size: var(--fz-lg); font-weight: 600;
|
|
612
|
+
display: flex; align-items: center; gap: 10px;
|
|
613
|
+
border-bottom: 1px solid var(--line);
|
|
614
|
+
}
|
|
615
|
+
.ss-header .ss-hico { display: inline-flex; color: var(--accent); }
|
|
616
|
+
.ss-header .ss-hico svg { width: 15px; height: 15px; }
|
|
617
|
+
.ss-body { padding: 16px 18px; }
|
|
618
|
+
.ss-body.md-body { font-size: var(--fz-sm); line-height: 1.72; }
|
|
619
|
+
.ss-body.md-body ul { padding-left: 20px; }
|
|
620
|
+
.ss-body.md-body li { margin-bottom: 5px; }
|
|
621
|
+
.prompts-list.scrollable { max-height: 300px; overflow-y: auto; }
|
|
622
|
+
.prompt-entry { border-bottom: 1px solid var(--line); padding-bottom: 12px; margin-bottom: 12px; }
|
|
461
623
|
.prompt-entry:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
|
|
462
|
-
.prompt-entry h3 { font-size: var(--fz-sm); color: var(--accent); font-weight:
|
|
624
|
+
.prompt-entry h3 { font-size: var(--fz-sm); color: var(--accent); font-weight: 600; margin-bottom: 5px; }
|
|
463
625
|
.prompt-entry blockquote {
|
|
464
|
-
border-left: 2px solid var(--accent-dim); padding
|
|
465
|
-
color: var(--muted); font-size: var(--fz-sm); margin:
|
|
626
|
+
border-left: 2px solid var(--accent-dim); padding: 4px 14px;
|
|
627
|
+
color: var(--muted); font-size: var(--fz-sm); margin: 5px 0;
|
|
628
|
+
background: var(--accent-soft); border-radius: 0 var(--r-sm) var(--r-sm) 0;
|
|
466
629
|
}
|
|
467
630
|
|
|
468
631
|
/* Graph view */
|
|
469
632
|
.graph-view {
|
|
470
|
-
display: grid; grid-template-columns: minmax(0,1fr)
|
|
633
|
+
display: grid; grid-template-columns: minmax(0,1fr) 312px;
|
|
471
634
|
gap: var(--gap); height: 100%; min-height: 0;
|
|
472
635
|
}
|
|
473
636
|
.graph-canvas {
|
|
474
|
-
border: 1px solid var(--line); background: var(--
|
|
475
|
-
border-radius: var(--r); position: relative; overflow: hidden;
|
|
476
|
-
display: flex; flex-direction: column;
|
|
637
|
+
border: 1px solid var(--line); background: var(--glass);
|
|
638
|
+
border-radius: var(--r-lg); position: relative; overflow: hidden;
|
|
639
|
+
display: flex; flex-direction: column; box-shadow: var(--shadow-card);
|
|
477
640
|
}
|
|
478
641
|
.graph-header {
|
|
479
642
|
display: flex; align-items: center; justify-content: space-between;
|
|
480
|
-
padding:
|
|
481
|
-
|
|
643
|
+
padding: 13px 16px; border-bottom: 1px solid var(--line);
|
|
644
|
+
gap: 12px; position: relative; z-index: 10;
|
|
482
645
|
}
|
|
483
|
-
.graph-header .ghost-btn { cursor: pointer; }
|
|
484
646
|
.graph-header-left { display: flex; align-items: center; gap: 10px; font-size: var(--fz-sm); white-space: nowrap; }
|
|
647
|
+
.graph-header-left .gh-ico { color: var(--accent); display: inline-flex; }
|
|
648
|
+
.graph-header-left .gh-ico svg { width: 16px; height: 16px; }
|
|
485
649
|
.graph-header-right { display: flex; gap: 6px; flex-shrink: 0; }
|
|
486
650
|
.tool-chip {
|
|
487
|
-
padding:
|
|
651
|
+
padding: 5px 11px; border-radius: var(--r-sm);
|
|
488
652
|
border: 1px solid var(--line); color: var(--muted);
|
|
489
|
-
background: transparent; font-size: var(--fz-xs);
|
|
653
|
+
background: transparent; font-size: var(--fz-xs); font-weight: 500;
|
|
654
|
+
transition: all 0.15s;
|
|
490
655
|
}
|
|
491
|
-
.tool-chip
|
|
656
|
+
.tool-chip:hover { color: var(--fg); border-color: var(--line-2); }
|
|
657
|
+
.tool-chip.active { border-color: var(--accent-dim); color: var(--accent); background: var(--accent-soft); }
|
|
492
658
|
.graph-svg-wrap { position: relative; flex: 1; min-height: 0; }
|
|
493
659
|
.graph-svg-wrap svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
494
660
|
.graph-legend {
|
|
495
|
-
position: absolute; left:
|
|
496
|
-
background:
|
|
497
|
-
border: 1px solid var(--line); border-radius: var(--r);
|
|
498
|
-
padding:
|
|
499
|
-
display: flex; flex-direction: column; gap:
|
|
500
|
-
backdrop-filter: blur(
|
|
501
|
-
}
|
|
502
|
-
.graph-legend-title { color: var(--dim); letter-spacing: 0.
|
|
503
|
-
.graph-legend-item { display: flex; align-items: center; gap:
|
|
661
|
+
position: absolute; left: 16px; bottom: 16px;
|
|
662
|
+
background: rgba(10,10,12,0.7);
|
|
663
|
+
border: 1px solid var(--line-2); border-radius: var(--r);
|
|
664
|
+
padding: 10px 12px; font-size: var(--fz-xs);
|
|
665
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
666
|
+
backdrop-filter: blur(10px);
|
|
667
|
+
}
|
|
668
|
+
.graph-legend-title { color: var(--dim); letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600; }
|
|
669
|
+
.graph-legend-item { display: flex; align-items: center; gap: 9px; color: var(--muted); }
|
|
504
670
|
.graph-rail { display: flex; flex-direction: column; gap: var(--gap); overflow-y: auto; }
|
|
505
671
|
.conn-item {
|
|
506
|
-
display: flex; align-items: center; gap:
|
|
507
|
-
width: 100%; padding:
|
|
672
|
+
display: flex; align-items: center; gap: 9px;
|
|
673
|
+
width: 100%; padding: 6px 7px; border-radius: var(--r-sm);
|
|
508
674
|
font-size: var(--fz-sm); color: var(--muted); text-align: left;
|
|
509
|
-
transition: background 0.
|
|
675
|
+
transition: background 0.12s;
|
|
510
676
|
}
|
|
511
|
-
.conn-item:hover { background: var(--panel
|
|
677
|
+
.conn-item:hover { background: var(--panel); color: var(--fg); }
|
|
512
678
|
|
|
513
679
|
/* Modals */
|
|
514
680
|
.modal-overlay {
|
|
515
|
-
position: fixed; inset: 0; background: rgba(0,0,0,0.
|
|
681
|
+
position: fixed; inset: 0; background: rgba(0,0,0,0.55);
|
|
516
682
|
z-index: 200; display: none; align-items: flex-start;
|
|
517
|
-
justify-content: center; padding-top:
|
|
518
|
-
backdrop-filter: blur(
|
|
683
|
+
justify-content: center; padding-top: 14vh;
|
|
684
|
+
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
|
|
519
685
|
}
|
|
520
686
|
.modal-overlay.active { display: flex; }
|
|
521
687
|
.modal-overlay.center { align-items: center; padding-top: 0; }
|
|
522
688
|
.modal {
|
|
523
|
-
width:
|
|
524
|
-
border: 1px solid var(--line); border-radius: var(--r-
|
|
525
|
-
box-shadow: 0
|
|
689
|
+
width: 520px; background: var(--panel-solid);
|
|
690
|
+
border: 1px solid var(--line-2); border-radius: var(--r-xl);
|
|
691
|
+
box-shadow: 0 32px 80px rgba(0,0,0,0.7);
|
|
526
692
|
overflow: hidden;
|
|
527
693
|
}
|
|
528
|
-
.modal-sm { width:
|
|
694
|
+
.modal-sm { width: 440px; }
|
|
529
695
|
.modal-header {
|
|
530
|
-
padding:
|
|
531
|
-
font-size: var(--fz-
|
|
532
|
-
letter-spacing: 0.
|
|
696
|
+
padding: 18px 22px 14px; border-bottom: 1px solid var(--line);
|
|
697
|
+
font-size: var(--fz-xs); font-weight: 700; color: var(--muted);
|
|
698
|
+
letter-spacing: 0.1em; text-transform: uppercase;
|
|
533
699
|
}
|
|
534
|
-
.modal-body { padding:
|
|
535
|
-
.modal-footer { padding:
|
|
700
|
+
.modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
701
|
+
.modal-footer { padding: 14px 22px; border-top: 1px solid var(--line); display: flex; justify-content: flex-end; gap: 9px; }
|
|
536
702
|
.modal-field label {
|
|
537
703
|
display: block; font-size: var(--fz-xs); font-weight: 600;
|
|
538
|
-
text-transform: uppercase; letter-spacing:
|
|
704
|
+
text-transform: uppercase; letter-spacing: 0.08em; color: var(--dim); margin-bottom: 6px;
|
|
539
705
|
}
|
|
540
706
|
.modal-field input, .modal-field textarea {
|
|
541
|
-
width: 100%; background:
|
|
542
|
-
color: var(--fg); padding:
|
|
707
|
+
width: 100%; background: rgba(0,0,0,0.3); border: 1px solid var(--line);
|
|
708
|
+
color: var(--fg); padding: 10px 13px; border-radius: var(--r); outline: none;
|
|
709
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
543
710
|
}
|
|
544
|
-
.modal-field input:focus, .modal-field textarea:focus { border-color: var(--accent-dim); }
|
|
545
|
-
.modal-field textarea { min-height:
|
|
711
|
+
.modal-field input:focus, .modal-field textarea:focus { border-color: var(--accent-dim); box-shadow: 0 0 0 3px rgba(139,127,245,0.12); }
|
|
712
|
+
.modal-field textarea { min-height: 68px; resize: vertical; }
|
|
546
713
|
.modal-btn {
|
|
547
|
-
padding:
|
|
548
|
-
border: 1px solid var(--line); color: var(--muted); background: var(--panel);
|
|
714
|
+
padding: 8px 16px; border-radius: var(--r-sm); font-size: var(--fz-sm); font-weight: 500;
|
|
715
|
+
border: 1px solid var(--line-2); color: var(--muted); background: var(--panel);
|
|
716
|
+
transition: all 0.15s;
|
|
549
717
|
}
|
|
550
|
-
.modal-btn:hover { border-color: var(--line-
|
|
551
|
-
.modal-btn.primary { border-color: var(--accent-dim); color:
|
|
552
|
-
.modal-btn.primary:hover { background:
|
|
718
|
+
.modal-btn:hover { border-color: var(--line-3); color: var(--fg); background: var(--panel-2); }
|
|
719
|
+
.modal-btn.primary { border-color: var(--accent-dim); color: #fff; background: linear-gradient(180deg, rgba(139,127,245,0.5), rgba(139,127,245,0.32)); }
|
|
720
|
+
.modal-btn.primary:hover { background: linear-gradient(180deg, rgba(139,127,245,0.62), rgba(139,127,245,0.42)); }
|
|
553
721
|
|
|
554
722
|
/* Edit modal */
|
|
555
723
|
.edit-textarea {
|
|
556
|
-
width: 100%; height:
|
|
724
|
+
width: 100%; height: 52vh; background: rgba(0,0,0,0.35);
|
|
557
725
|
border: 1px solid var(--line); color: var(--fg);
|
|
558
|
-
padding:
|
|
559
|
-
font-
|
|
726
|
+
padding: 18px; border-radius: var(--r); outline: none;
|
|
727
|
+
font-family: "JetBrains Mono", monospace;
|
|
728
|
+
font-size: var(--fz); line-height: 1.7; resize: none;
|
|
560
729
|
}
|
|
561
|
-
.edit-textarea:focus { border-color: var(--accent-dim); }
|
|
730
|
+
.edit-textarea:focus { border-color: var(--accent-dim); box-shadow: 0 0 0 3px rgba(139,127,245,0.12); }
|
|
562
731
|
|
|
563
732
|
/* Quick switcher */
|
|
564
733
|
.qs-input {
|
|
565
734
|
width: 100%; background: transparent; border: none;
|
|
566
735
|
border-bottom: 1px solid var(--line); color: var(--fg);
|
|
567
|
-
font-size:
|
|
736
|
+
font-size: 15px; padding: 18px 22px; outline: none;
|
|
568
737
|
}
|
|
569
738
|
.qs-input::placeholder { color: var(--dim); }
|
|
570
|
-
.qs-results { max-height:
|
|
739
|
+
.qs-results { max-height: 340px; overflow-y: auto; }
|
|
571
740
|
.qs-item {
|
|
572
|
-
padding:
|
|
573
|
-
display: flex; align-items: center; gap:
|
|
741
|
+
padding: 11px 22px; cursor: pointer;
|
|
742
|
+
display: flex; align-items: center; gap: 12px; transition: background 0.1s;
|
|
574
743
|
}
|
|
575
744
|
.qs-item:hover, .qs-item.selected { background: var(--panel-2); }
|
|
576
|
-
.qs-item .qs-icon { color: var(--accent);
|
|
745
|
+
.qs-item .qs-icon { color: var(--accent); display: inline-flex; opacity: 0.85; }
|
|
746
|
+
.qs-item .qs-icon svg { width: 14px; height: 14px; }
|
|
577
747
|
.qs-item .qs-name { font-size: var(--fz); color: var(--fg); font-weight: 500; }
|
|
578
|
-
.qs-item .qs-path { font-size: var(--fz-xs); color: var(--dim); margin-left: auto; }
|
|
748
|
+
.qs-item .qs-path { font-size: var(--fz-xs); color: var(--dim); margin-left: auto; font-family: "JetBrains Mono", monospace; }
|
|
579
749
|
.qs-item.selected .qs-name { color: var(--accent); }
|
|
580
|
-
.qs-empty { padding:
|
|
750
|
+
.qs-empty { padding: 22px; text-align: center; color: var(--dim); font-size: var(--fz-sm); }
|
|
581
751
|
|
|
582
752
|
/* Session search results in sidebar */
|
|
583
|
-
.ss-results { max-height:
|
|
584
|
-
.ss-result { padding:
|
|
753
|
+
.ss-results { max-height: 180px; overflow-y: auto; margin-bottom: 8px; }
|
|
754
|
+
.ss-result { padding: 7px 9px; border-radius: var(--r-sm); cursor: pointer; margin: 1px 0; transition: background 0.12s; }
|
|
585
755
|
.ss-result:hover { background: var(--panel); }
|
|
586
|
-
.ss-result .ssr-title { font-size: var(--fz-sm); color: var(--accent); font-weight:
|
|
587
|
-
.ss-result .ssr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top:
|
|
756
|
+
.ss-result .ssr-title { font-size: var(--fz-sm); color: var(--accent); font-weight: 600; }
|
|
757
|
+
.ss-result .ssr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
758
|
+
|
|
759
|
+
/* empty state center */
|
|
760
|
+
.center-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 14px; }
|
|
588
761
|
</style>
|
|
589
762
|
</head>
|
|
590
763
|
<body>
|
|
@@ -593,24 +766,27 @@ body.resizing #resize-handle { pointer-events: auto !important; }
|
|
|
593
766
|
<header id="topbar">
|
|
594
767
|
<div class="topbar-left">
|
|
595
768
|
<button class="toggle-btn" id="sidebar-toggle" title="Toggle sidebar">
|
|
596
|
-
<svg width="
|
|
769
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="currentColor" stroke-width="1.3"/><line x1="6" y1="2.5" x2="6" y2="13.5" stroke="currentColor" stroke-width="1.3"/></svg>
|
|
597
770
|
</button>
|
|
598
|
-
<span class="logo"
|
|
599
|
-
|
|
771
|
+
<span class="logo">
|
|
772
|
+
<span class="logo-mark"><svg viewBox="0 0 16 16" fill="none"><path d="M3 3v10M3 8l6-5M3 8l6 5" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
|
|
773
|
+
KYP<span style="color:var(--dim)">·</span>MEM
|
|
774
|
+
<span class="lo-sub">know your project</span>
|
|
775
|
+
</span>
|
|
600
776
|
<span class="dim">·</span>
|
|
601
777
|
<span class="breadcrumb" id="breadcrumb"></span>
|
|
602
778
|
</div>
|
|
603
779
|
<div class="topbar-right">
|
|
604
780
|
<div class="view-switch" id="view-switch">
|
|
605
|
-
<button class="active" data-view="
|
|
606
|
-
<button data-view="
|
|
607
|
-
<button data-view="
|
|
781
|
+
<button class="active" data-view="dashboard"><svg viewBox="0 0 16 16" fill="none"><rect x="1.5" y="1.5" width="5.5" height="5.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="9" y="1.5" width="5.5" height="5.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="1.5" y="9" width="5.5" height="5.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="9" y="9" width="5.5" height="5.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg>overview</button>
|
|
782
|
+
<button data-view="note"><svg viewBox="0 0 16 16" fill="none"><path d="M3.5 2h6l3 3v9a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M9 2v4h4" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>note</button>
|
|
783
|
+
<button data-view="session"><svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.2" stroke="currentColor" stroke-width="1.3"/><path d="M8 4.5V8l2.5 1.6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>session</button>
|
|
784
|
+
<button data-view="graph"><svg viewBox="0 0 16 16" fill="none"><circle cx="4" cy="4" r="2" stroke="currentColor" stroke-width="1.3"/><circle cx="12" cy="5" r="2" stroke="currentColor" stroke-width="1.3"/><circle cx="7" cy="12" r="2" stroke="currentColor" stroke-width="1.3"/><path d="M5.6 5.3 9.5 11M5.7 5l4.4-.3" stroke="currentColor" stroke-width="1.2"/></svg>graph</button>
|
|
608
785
|
</div>
|
|
609
|
-
<button class="ghost-btn" id="new-project-btn"
|
|
786
|
+
<button class="ghost-btn primary" id="new-project-btn"><svg viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>project</button>
|
|
610
787
|
<div class="search-bar" id="search-bar">
|
|
611
|
-
<span class="s-
|
|
612
|
-
<input type="text" id="search-input" placeholder="
|
|
613
|
-
<span class="s-caret"></span>
|
|
788
|
+
<span class="s-ico"><svg viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.4"/><path d="M10.5 10.5 14 14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg></span>
|
|
789
|
+
<input type="text" id="search-input" placeholder="Search vault…">
|
|
614
790
|
<span class="s-hint">⌘K</span>
|
|
615
791
|
<div class="search-dropdown" id="search-dropdown"></div>
|
|
616
792
|
</div>
|
|
@@ -632,7 +808,7 @@ body.resizing #resize-handle { pointer-events: auto !important; }
|
|
|
632
808
|
<span><span class="mut tab-nums" id="stat-notes">0</span> notes</span>
|
|
633
809
|
<span><span class="mut tab-nums" id="stat-folders">0</span> folders</span>
|
|
634
810
|
<span>·</span>
|
|
635
|
-
<span>store <span class="mut">~/.kyp-mem/vault</span></span>
|
|
811
|
+
<span>store <span class="mut mono">~/.kyp-mem/vault</span></span>
|
|
636
812
|
<span><i class="sb-dot"></i>sync ok</span>
|
|
637
813
|
</div>
|
|
638
814
|
<div class="sb-center" id="token-economics" style="display:none;">
|
|
@@ -647,7 +823,7 @@ body.resizing #resize-handle { pointer-events: auto !important; }
|
|
|
647
823
|
<span>·</span>
|
|
648
824
|
<span class="tab-nums" id="clock"></span>
|
|
649
825
|
<span>·</span>
|
|
650
|
-
<span class="tab-nums mut">v0.
|
|
826
|
+
<span class="tab-nums mut">v0.9.0</span>
|
|
651
827
|
</div>
|
|
652
828
|
</footer>
|
|
653
829
|
</div>
|
|
@@ -655,21 +831,21 @@ body.resizing #resize-handle { pointer-events: auto !important; }
|
|
|
655
831
|
<!-- Quick Switcher -->
|
|
656
832
|
<div class="modal-overlay" id="qs-overlay">
|
|
657
833
|
<div class="modal">
|
|
658
|
-
<input type="text" class="qs-input" id="qs-input" placeholder="Jump to note
|
|
834
|
+
<input type="text" class="qs-input" id="qs-input" placeholder="Jump to note…">
|
|
659
835
|
<div class="qs-results" id="qs-results"></div>
|
|
660
836
|
</div>
|
|
661
837
|
</div>
|
|
662
838
|
|
|
663
839
|
<!-- Edit Modal -->
|
|
664
840
|
<div class="modal-overlay center" id="edit-overlay">
|
|
665
|
-
<div class="modal" style="width:
|
|
666
|
-
<div class="modal-header" id="edit-path"></div>
|
|
667
|
-
<div style="padding:
|
|
668
|
-
<textarea class="edit-textarea" id="edit-textarea" placeholder="Write markdown
|
|
841
|
+
<div class="modal" style="width:680px">
|
|
842
|
+
<div class="modal-header mono" id="edit-path"></div>
|
|
843
|
+
<div style="padding:18px 22px">
|
|
844
|
+
<textarea class="edit-textarea" id="edit-textarea" placeholder="Write markdown…"></textarea>
|
|
669
845
|
</div>
|
|
670
846
|
<div class="modal-footer">
|
|
671
|
-
<button class="modal-btn" id="edit-cancel">
|
|
672
|
-
<button class="modal-btn primary" id="edit-save">
|
|
847
|
+
<button class="modal-btn" id="edit-cancel">Cancel</button>
|
|
848
|
+
<button class="modal-btn primary" id="edit-save">Save ⌘S</button>
|
|
673
849
|
</div>
|
|
674
850
|
</div>
|
|
675
851
|
</div>
|
|
@@ -677,14 +853,14 @@ body.resizing #resize-handle { pointer-events: auto !important; }
|
|
|
677
853
|
<!-- Session Create Modal -->
|
|
678
854
|
<div class="modal-overlay center" id="session-create-overlay">
|
|
679
855
|
<div class="modal modal-sm">
|
|
680
|
-
<div class="modal-header">
|
|
856
|
+
<div class="modal-header">New Session</div>
|
|
681
857
|
<div class="modal-body">
|
|
682
|
-
<div class="modal-field"><label>Project</label><input type="text" id="sc-project" list="sc-project-list" placeholder="Project name
|
|
858
|
+
<div class="modal-field"><label>Project</label><input type="text" id="sc-project" list="sc-project-list" placeholder="Project name…"><datalist id="sc-project-list"></datalist></div>
|
|
683
859
|
<div class="modal-field"><label>Summary</label><textarea id="sc-summary" placeholder="What are you working on?"></textarea></div>
|
|
684
860
|
</div>
|
|
685
861
|
<div class="modal-footer">
|
|
686
|
-
<button class="modal-btn" id="sc-cancel">
|
|
687
|
-
<button class="modal-btn primary" id="sc-create">
|
|
862
|
+
<button class="modal-btn" id="sc-cancel">Cancel</button>
|
|
863
|
+
<button class="modal-btn primary" id="sc-create">Create</button>
|
|
688
864
|
</div>
|
|
689
865
|
</div>
|
|
690
866
|
</div>
|
|
@@ -692,40 +868,64 @@ body.resizing #resize-handle { pointer-events: auto !important; }
|
|
|
692
868
|
<!-- Project Create Modal -->
|
|
693
869
|
<div class="modal-overlay center" id="project-create-overlay">
|
|
694
870
|
<div class="modal modal-sm">
|
|
695
|
-
<div class="modal-header">
|
|
871
|
+
<div class="modal-header">New Project</div>
|
|
696
872
|
<div class="modal-body">
|
|
697
|
-
<div class="modal-field"><label>Project Name</label><input type="text" id="pc-name" placeholder="My Project
|
|
698
|
-
<div class="modal-field"><label>Overview (optional)</label><textarea id="pc-overview" placeholder="Brief description
|
|
873
|
+
<div class="modal-field"><label>Project Name</label><input type="text" id="pc-name" placeholder="My Project…"></div>
|
|
874
|
+
<div class="modal-field"><label>Overview (optional)</label><textarea id="pc-overview" placeholder="Brief description…"></textarea></div>
|
|
699
875
|
</div>
|
|
700
876
|
<div class="modal-footer">
|
|
701
|
-
<button class="modal-btn" id="pc-cancel">
|
|
702
|
-
<button class="modal-btn primary" id="pc-create">
|
|
877
|
+
<button class="modal-btn" id="pc-cancel">Cancel</button>
|
|
878
|
+
<button class="modal-btn primary" id="pc-create">Create</button>
|
|
703
879
|
</div>
|
|
704
880
|
</div>
|
|
705
881
|
</div>
|
|
706
882
|
|
|
707
883
|
<script>
|
|
884
|
+
// ─── Icons (inline SVG strings) ───────────────────────────────────────────────
|
|
885
|
+
const ICON = {
|
|
886
|
+
note: '<svg viewBox="0 0 16 16" fill="none"><path d="M3.5 2h6l3 3v9a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M9 2v4h4" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>',
|
|
887
|
+
session: '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.2" stroke="currentColor" stroke-width="1.3"/><path d="M8 4.5V8l2.5 1.6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
|
|
888
|
+
folder: '<svg viewBox="0 0 16 16" fill="none"><path d="M1.8 4a1 1 0 0 1 1-1h3.3l1.2 1.5h5a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H2.8a1 1 0 0 1-1-1V4z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>',
|
|
889
|
+
graph: '<svg viewBox="0 0 16 16" fill="none"><circle cx="4" cy="4" r="2" stroke="currentColor" stroke-width="1.3"/><circle cx="12" cy="5" r="2" stroke="currentColor" stroke-width="1.3"/><circle cx="7" cy="12" r="2" stroke="currentColor" stroke-width="1.3"/><path d="M5.6 5.3 9.5 11M5.7 5l4.4-.3" stroke="currentColor" stroke-width="1.2"/></svg>',
|
|
890
|
+
chevron: '<svg viewBox="0 0 16 16" fill="none"><path d="M5.5 3.5 10 8l-4.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
891
|
+
caretDown: '<svg viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
892
|
+
doc: '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="2" stroke="currentColor" stroke-width="1.3"/></svg>',
|
|
893
|
+
notes: '<svg viewBox="0 0 16 16" fill="none"><path d="M4 1.5h5l3 3V13a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 3 13V3A1.5 1.5 0 0 1 4 1.5z" stroke="currentColor" stroke-width="1.3"/><path d="M5.5 7.5h5M5.5 10h3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
|
|
894
|
+
clock: '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.2" stroke="currentColor" stroke-width="1.3"/><path d="M8 4.5V8l2.5 1.6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
|
|
895
|
+
spark: '<svg viewBox="0 0 16 16" fill="none"><path d="M8 1.5l1.6 4.4 4.4 1.6-4.4 1.6L8 14.5 6.4 9.6 2 8l4.4-1.6L8 1.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>',
|
|
896
|
+
layers: '<svg viewBox="0 0 16 16" fill="none"><path d="M8 1.5 14.5 5 8 8.5 1.5 5 8 1.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M2.5 8.5 8 11.5l5.5-3M2.5 11.5 8 14.5l5.5-3" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>',
|
|
897
|
+
activity: '<svg viewBox="0 0 16 16" fill="none"><path d="M1.5 9.5 4 9.5 6 3.5 9 12.5 11 7.5h3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
898
|
+
gauge: '<svg viewBox="0 0 16 16" fill="none"><path d="M2.5 12a6 6 0 1 1 11 0" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><path d="M8 10l3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
|
|
899
|
+
arrowLeft: '<svg viewBox="0 0 16 16" fill="none"><path d="M9.5 3.5 5 8l4.5 4.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
900
|
+
expand: '<svg viewBox="0 0 16 16" fill="none"><path d="M9.5 2.5h4v4M6.5 13.5h-4v-4M13.5 2.5 9 7M2.5 13.5 7 9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
901
|
+
edit: '<svg viewBox="0 0 16 16" fill="none"><path d="M11 2.5 13.5 5 5.5 13H3v-2.5L11 2.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>',
|
|
902
|
+
trash: '<svg viewBox="0 0 16 16" fill="none"><path d="M2.5 4h11M6 4V2.5h4V4M4 4l.6 9a1 1 0 0 0 1 1h4.8a1 1 0 0 0 1-1L12 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
903
|
+
link: '<svg viewBox="0 0 16 16" fill="none"><path d="M6.5 9.5 9.5 6.5M7 4l1-1a2.8 2.8 0 0 1 4 4l-1 1M9 12l-1 1a2.8 2.8 0 0 1-4-4l1-1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
|
|
904
|
+
list: '<svg viewBox="0 0 16 16" fill="none"><path d="M5 4h9M5 8h9M5 12h6M2 4h.01M2 8h.01M2 12h.01" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
|
|
905
|
+
tag: '<svg viewBox="0 0 16 16" fill="none"><path d="M2 2.5h5l6.5 6.5a1.2 1.2 0 0 1 0 1.7l-3.3 3.3a1.2 1.2 0 0 1-1.7 0L2 7.5v-5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><circle cx="5" cy="5" r="0.9" fill="currentColor"/></svg>',
|
|
906
|
+
};
|
|
907
|
+
|
|
708
908
|
// ─── State ───────────────────────────────────────────────────────────────────
|
|
709
909
|
let currentPath = null;
|
|
710
910
|
let currentNote = null;
|
|
711
911
|
let treeData = null;
|
|
712
912
|
let allNotes = {};
|
|
713
913
|
let activeTagFilters = new Set();
|
|
714
|
-
let view = '
|
|
914
|
+
let view = 'dashboard';
|
|
715
915
|
let activeSession = null;
|
|
716
916
|
let sidebarOpen = localStorage.getItem('kyp-sidebar-open') !== 'false';
|
|
717
917
|
let qsSelectedIndex = 0;
|
|
718
918
|
let editingPath = null;
|
|
719
919
|
let editingNote = null;
|
|
720
920
|
let lastKnownStats = null;
|
|
721
|
-
let graphHeight = parseInt(localStorage.getItem('kyp-graph-h')) || 240;
|
|
722
921
|
let _graphData = null;
|
|
922
|
+
let _sessionsData = {};
|
|
723
923
|
let searchTimeout, sessionSearchTimeout;
|
|
724
924
|
|
|
725
925
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
726
926
|
async function fetchJSON(url) { const r = await fetch(url, { cache: 'no-store' }); return r.json(); }
|
|
727
|
-
|
|
728
927
|
function $(id) { return document.getElementById(id); }
|
|
928
|
+
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
729
929
|
|
|
730
930
|
function formatSessionTime(filename) {
|
|
731
931
|
const stem = filename.replace('.md', '');
|
|
@@ -735,7 +935,7 @@ function formatSessionTime(filename) {
|
|
|
735
935
|
const hr = parseInt(h), ampm = hr >= 12 ? 'PM' : 'AM';
|
|
736
936
|
const hr12 = hr % 12 || 12;
|
|
737
937
|
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
738
|
-
return
|
|
938
|
+
return months[parseInt(mo)-1] + ' ' + parseInt(d) + ', ' + hr12 + ':' + mi + ' ' + ampm;
|
|
739
939
|
}
|
|
740
940
|
|
|
741
941
|
function findNotePath(name) {
|
|
@@ -759,13 +959,19 @@ function isSessionPath(path) {
|
|
|
759
959
|
return path.includes('/Sessions/') || path.startsWith('Sessions/');
|
|
760
960
|
}
|
|
761
961
|
|
|
962
|
+
function fmtTokens(n) {
|
|
963
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
964
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
965
|
+
return String(n);
|
|
966
|
+
}
|
|
967
|
+
|
|
762
968
|
// ─── Clock ───────────────────────────────────────────────────────────────────
|
|
763
969
|
function updateClock() {
|
|
764
970
|
const d = new Date();
|
|
765
971
|
const h = d.getHours() % 12 || 12;
|
|
766
972
|
const m = String(d.getMinutes()).padStart(2, '0');
|
|
767
973
|
const ampm = d.getHours() >= 12 ? 'PM' : 'AM';
|
|
768
|
-
$('clock').textContent =
|
|
974
|
+
$('clock').textContent = h + ':' + m + ' ' + ampm;
|
|
769
975
|
}
|
|
770
976
|
updateClock();
|
|
771
977
|
setInterval(updateClock, 30000);
|
|
@@ -777,8 +983,8 @@ function updateBreadcrumb(path) {
|
|
|
777
983
|
const parts = path.replace('.md', '').split('/').filter(Boolean);
|
|
778
984
|
bc.innerHTML = parts.map((p, i) =>
|
|
779
985
|
i < parts.length - 1
|
|
780
|
-
?
|
|
781
|
-
:
|
|
986
|
+
? '<span>' + esc(p) + '</span><span class="bc-sep">/</span>'
|
|
987
|
+
: '<span class="bc-last">' + esc(p) + '</span>'
|
|
782
988
|
).join('');
|
|
783
989
|
}
|
|
784
990
|
|
|
@@ -791,18 +997,27 @@ function setView(v) {
|
|
|
791
997
|
}
|
|
792
998
|
|
|
793
999
|
$('view-switch').addEventListener('click', e => {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1000
|
+
const btn = e.target.closest('button[data-view]');
|
|
1001
|
+
if (btn && btn.dataset.view) {
|
|
1002
|
+
setView(btn.dataset.view);
|
|
1003
|
+
if (view === 'dashboard') { updateBreadcrumb(null); renderDashboard(); }
|
|
1004
|
+
else if (view === 'graph') { _graphProject = null; _graphData = null; renderGraphView(); }
|
|
797
1005
|
else if (view === 'session' && activeSession) loadNote(activeSession);
|
|
798
1006
|
else if (view === 'note' && currentPath) loadNote(currentPath);
|
|
1007
|
+
else if (view === 'note') showNotePlaceholder();
|
|
1008
|
+
else if (view === 'session') showSessionPlaceholder();
|
|
799
1009
|
}
|
|
800
1010
|
});
|
|
801
1011
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1012
|
+
function showNotePlaceholder() {
|
|
1013
|
+
$('content-area').innerHTML = '<div class="center-empty"><span class="dim">' + ICON.note + '</span><span class="dim" style="font-size:var(--fz-sm)">Select a note from the sidebar, or press <span style="border:1px solid var(--line-2);border-radius:5px;padding:1px 6px;color:var(--muted)">⌘O</span></span></div>';
|
|
1014
|
+
}
|
|
1015
|
+
function showSessionPlaceholder() {
|
|
1016
|
+
$('content-area').innerHTML = '<div class="center-empty"><span class="dim">' + ICON.session + '</span><span class="dim" style="font-size:var(--fz-sm)">Open a session from the sidebar</span></div>';
|
|
805
1017
|
}
|
|
1018
|
+
|
|
1019
|
+
// ─── Sidebar Toggle ──────────────────────────────────────────────────────────
|
|
1020
|
+
function applySidebar() { $('main').classList.toggle('sidebar-hidden', !sidebarOpen); }
|
|
806
1021
|
$('sidebar-toggle').addEventListener('click', () => {
|
|
807
1022
|
sidebarOpen = !sidebarOpen;
|
|
808
1023
|
localStorage.setItem('kyp-sidebar-open', sidebarOpen);
|
|
@@ -810,24 +1025,21 @@ $('sidebar-toggle').addEventListener('click', () => {
|
|
|
810
1025
|
});
|
|
811
1026
|
applySidebar();
|
|
812
1027
|
|
|
813
|
-
// Rail is always open
|
|
814
1028
|
let railOpen = true;
|
|
815
1029
|
|
|
816
1030
|
// ─── Resize Handle ───────────────────────────────────────────────────────────
|
|
817
1031
|
(function initResize() {
|
|
818
1032
|
const handle = $('resize-handle');
|
|
819
1033
|
const main = $('main');
|
|
820
|
-
let sidebarW = parseInt(localStorage.getItem('kyp-sidebar-w')) ||
|
|
1034
|
+
let sidebarW = parseInt(localStorage.getItem('kyp-sidebar-w')) || 248;
|
|
821
1035
|
main.style.setProperty('--sidebar-w', sidebarW + 'px');
|
|
822
|
-
|
|
823
1036
|
handle.addEventListener('mousedown', e => {
|
|
824
1037
|
e.preventDefault();
|
|
825
1038
|
const startX = e.clientX, startW = sidebarW;
|
|
826
1039
|
handle.classList.add('dragging');
|
|
827
1040
|
document.body.classList.add('resizing');
|
|
828
|
-
|
|
829
1041
|
function onMove(ev) {
|
|
830
|
-
sidebarW = Math.max(
|
|
1042
|
+
sidebarW = Math.max(200, Math.min(420, startW + (ev.clientX - startX)));
|
|
831
1043
|
main.style.setProperty('--sidebar-w', sidebarW + 'px');
|
|
832
1044
|
}
|
|
833
1045
|
function onUp() {
|
|
@@ -849,50 +1061,52 @@ function renderSidebar(sessionsData) {
|
|
|
849
1061
|
|
|
850
1062
|
// Sessions section
|
|
851
1063
|
const sessSection = document.createElement('section');
|
|
852
|
-
sessSection.innerHTML =
|
|
853
|
-
<div class="side-section-header">
|
|
854
|
-
<span class="side-section-title"><span class="side-dot" style="background:var(--
|
|
855
|
-
<button class="side-add-btn" id="sidebar-new-session">+ new</button>
|
|
856
|
-
</div>
|
|
857
|
-
`;
|
|
1064
|
+
sessSection.innerHTML =
|
|
1065
|
+
'<div class="side-section-header">' +
|
|
1066
|
+
'<span class="side-section-title"><span class="side-dot" style="background:var(--c-teal);box-shadow:0 0 6px var(--c-teal)"></span>sessions</span>' +
|
|
1067
|
+
'<button class="side-add-btn" id="sidebar-new-session">+ new</button>' +
|
|
1068
|
+
'</div>';
|
|
858
1069
|
const sessSearch = document.createElement('input');
|
|
859
1070
|
sessSearch.className = 'side-search';
|
|
860
|
-
sessSearch.placeholder = '
|
|
1071
|
+
sessSearch.placeholder = 'Semantic search sessions…';
|
|
861
1072
|
sessSection.appendChild(sessSearch);
|
|
862
1073
|
|
|
863
1074
|
const ssResults = document.createElement('div');
|
|
864
1075
|
ssResults.className = 'ss-results';
|
|
865
1076
|
sessSection.appendChild(ssResults);
|
|
866
1077
|
|
|
867
|
-
// Session search
|
|
868
1078
|
sessSearch.addEventListener('input', () => {
|
|
869
1079
|
clearTimeout(sessionSearchTimeout);
|
|
870
1080
|
const q = sessSearch.value.trim();
|
|
871
1081
|
if (!q) { ssResults.innerHTML = ''; return; }
|
|
872
1082
|
sessionSearchTimeout = setTimeout(async () => {
|
|
873
|
-
const results = await fetchJSON(
|
|
1083
|
+
const results = await fetchJSON('/api/sessions/search?q=' + encodeURIComponent(q));
|
|
874
1084
|
ssResults.innerHTML = '';
|
|
875
1085
|
results.forEach(r => {
|
|
876
1086
|
const div = document.createElement('div');
|
|
877
1087
|
div.className = 'ss-result';
|
|
878
|
-
div.innerHTML =
|
|
1088
|
+
div.innerHTML = '<div class="ssr-title">' + esc(r.title) + '</div><div class="ssr-snippet">' + esc((r.snippet || '').substring(0, 100)) + '</div>';
|
|
879
1089
|
div.addEventListener('click', () => { openSession(r.path); sessSearch.value = ''; ssResults.innerHTML = ''; });
|
|
880
1090
|
ssResults.appendChild(div);
|
|
881
1091
|
});
|
|
882
1092
|
}, 250);
|
|
883
1093
|
});
|
|
884
1094
|
|
|
885
|
-
// Session groups
|
|
886
1095
|
const sortedProjects = Object.keys(sessionsData).sort();
|
|
887
1096
|
sortedProjects.forEach(project => {
|
|
888
1097
|
const sessions = sessionsData[project];
|
|
889
1098
|
const group = document.createElement('div');
|
|
890
1099
|
const folder = document.createElement('div');
|
|
891
1100
|
folder.className = 'tree-folder';
|
|
892
|
-
folder.innerHTML =
|
|
1101
|
+
folder.innerHTML =
|
|
1102
|
+
'<span class="tf-arrow closed">' + ICON.chevron + '</span>' +
|
|
1103
|
+
'<span class="tf-icon">' + ICON.folder + '</span>' +
|
|
1104
|
+
'<span class="tf-label">' + esc(project) + '</span>' +
|
|
1105
|
+
'<span class="tf-count">' + sessions.length + '</span>' +
|
|
1106
|
+
'<button class="tf-graph-btn" title="Open graph for ' + esc(project) + '" data-project="' + esc(project) + '">' + ICON.graph + '</button>';
|
|
893
1107
|
|
|
894
1108
|
const list = document.createElement('div');
|
|
895
|
-
list.style.cssText = 'display:none;flex-direction:column;gap:1px;padding-left:
|
|
1109
|
+
list.style.cssText = 'display:none;flex-direction:column;gap:1px;padding-left:18px;margin-top:2px;';
|
|
896
1110
|
|
|
897
1111
|
sessions.sort((a, b) => b.path.localeCompare(a.path));
|
|
898
1112
|
const MAX_VISIBLE = 5;
|
|
@@ -900,44 +1114,39 @@ function renderSidebar(sessionsData) {
|
|
|
900
1114
|
const row = document.createElement('button');
|
|
901
1115
|
row.className = 'sidebar-row';
|
|
902
1116
|
row.dataset.path = s.path;
|
|
903
|
-
if (idx >= MAX_VISIBLE) row.style.display = 'none';
|
|
904
|
-
if (idx >= MAX_VISIBLE) row.dataset.overflow = 'true';
|
|
1117
|
+
if (idx >= MAX_VISIBLE) { row.style.display = 'none'; row.dataset.overflow = 'true'; }
|
|
905
1118
|
const displayTime = formatSessionTime(s.path.split('/').pop());
|
|
906
|
-
row.innerHTML =
|
|
1119
|
+
row.innerHTML = '<span class="sr-ico">' + ICON.session + '</span><span class="sr-label">' + esc(displayTime) + '</span>';
|
|
907
1120
|
row.addEventListener('click', () => openSession(s.path));
|
|
908
1121
|
list.appendChild(row);
|
|
909
1122
|
});
|
|
910
1123
|
if (sessions.length > MAX_VISIBLE) {
|
|
911
1124
|
const expandBtn = document.createElement('button');
|
|
912
1125
|
expandBtn.className = 'sidebar-row expand-sessions-btn';
|
|
913
|
-
|
|
1126
|
+
const moreLabel = (n) => '<span class="sr-ico" style="opacity:0.6">' + ICON.caretDown + '</span><span class="sr-label" style="color:var(--dim);font-style:italic">' + n + '</span>';
|
|
1127
|
+
expandBtn.innerHTML = moreLabel('show ' + (sessions.length - MAX_VISIBLE) + ' more');
|
|
914
1128
|
expandBtn.addEventListener('click', (e) => {
|
|
915
1129
|
e.stopPropagation();
|
|
916
1130
|
const hidden = list.querySelectorAll('[data-overflow]');
|
|
917
1131
|
const isExpanded = expandBtn.dataset.expanded === 'true';
|
|
918
1132
|
hidden.forEach(r => r.style.display = isExpanded ? 'none' : 'flex');
|
|
919
1133
|
expandBtn.dataset.expanded = isExpanded ? '' : 'true';
|
|
920
|
-
expandBtn.innerHTML = isExpanded
|
|
921
|
-
? `<span class="sr-dot" style="color:var(--dim)">⋯</span><span class="sr-label" style="color:var(--dim);font-style:italic">show ${sessions.length - MAX_VISIBLE} more</span>`
|
|
922
|
-
: `<span class="sr-dot" style="color:var(--dim)">⋯</span><span class="sr-label" style="color:var(--dim);font-style:italic">show less</span>`;
|
|
1134
|
+
expandBtn.innerHTML = isExpanded ? moreLabel('show ' + (sessions.length - MAX_VISIBLE) + ' more') : moreLabel('show less');
|
|
923
1135
|
});
|
|
924
1136
|
list.appendChild(expandBtn);
|
|
925
1137
|
}
|
|
926
1138
|
|
|
927
1139
|
folder.querySelector('.tf-graph-btn').addEventListener('click', (e) => {
|
|
928
1140
|
e.stopPropagation();
|
|
929
|
-
_graphProject = project;
|
|
930
|
-
|
|
931
|
-
setView('graph');
|
|
932
|
-
renderGraphView();
|
|
1141
|
+
_graphProject = project; _graphData = null;
|
|
1142
|
+
setView('graph'); renderGraphView();
|
|
933
1143
|
});
|
|
934
1144
|
|
|
935
1145
|
folder.addEventListener('click', () => {
|
|
936
1146
|
const arrow = folder.querySelector('.tf-arrow');
|
|
937
|
-
const
|
|
938
|
-
arrow.
|
|
939
|
-
|
|
940
|
-
list.style.display = isOpen ? 'none' : 'flex';
|
|
1147
|
+
const open = !arrow.classList.contains('closed');
|
|
1148
|
+
arrow.classList.toggle('closed', open);
|
|
1149
|
+
list.style.display = open ? 'none' : 'flex';
|
|
941
1150
|
});
|
|
942
1151
|
|
|
943
1152
|
group.appendChild(folder);
|
|
@@ -948,39 +1157,38 @@ function renderSidebar(sessionsData) {
|
|
|
948
1157
|
|
|
949
1158
|
// Projects section
|
|
950
1159
|
const projSection = document.createElement('section');
|
|
951
|
-
projSection.innerHTML =
|
|
952
|
-
<div class="side-section-header">
|
|
953
|
-
<span class="side-section-title"><span class="side-dot" style="background:var(--
|
|
954
|
-
<button class="side-add-btn" id="sidebar-new-project">+ new</button>
|
|
955
|
-
</div>
|
|
956
|
-
`;
|
|
1160
|
+
projSection.innerHTML =
|
|
1161
|
+
'<div class="side-section-header">' +
|
|
1162
|
+
'<span class="side-section-title"><span class="side-dot" style="background:var(--c-violet);box-shadow:0 0 6px var(--c-violet)"></span>projects</span>' +
|
|
1163
|
+
'<button class="side-add-btn" id="sidebar-new-project">+ new</button>' +
|
|
1164
|
+
'</div>';
|
|
957
1165
|
renderProjectTree(projSection);
|
|
958
1166
|
container.appendChild(projSection);
|
|
959
1167
|
|
|
960
1168
|
// Tags section
|
|
961
1169
|
const tagSection = document.createElement('section');
|
|
962
1170
|
let tagsOpen = false;
|
|
963
|
-
tagSection.innerHTML =
|
|
964
|
-
<div class="side-section-header">
|
|
965
|
-
<button class="side-section-title" id="tags-toggle"><span class="side-dot" style="background:var(--
|
|
966
|
-
</div>
|
|
967
|
-
`;
|
|
1171
|
+
tagSection.innerHTML =
|
|
1172
|
+
'<div class="side-section-header">' +
|
|
1173
|
+
'<button class="side-section-title" id="tags-toggle"><span class="side-dot" style="background:var(--c-amber);box-shadow:0 0 6px var(--c-amber)"></span>tags<span class="side-collapse-arrow collapsed">' + ICON.caretDown + '</span></button>' +
|
|
1174
|
+
'</div>';
|
|
968
1175
|
const tagBody = document.createElement('div');
|
|
969
|
-
tagBody.
|
|
1176
|
+
tagBody.id = 'tag-cloud-body';
|
|
1177
|
+
tagBody.style.cssText = 'display:none;flex-wrap:wrap;gap:6px;';
|
|
970
1178
|
renderTagCloud(tagBody);
|
|
971
1179
|
tagSection.appendChild(tagBody);
|
|
972
1180
|
container.appendChild(tagSection);
|
|
973
1181
|
|
|
974
|
-
// Wire tag toggle
|
|
975
1182
|
tagSection.querySelector('#tags-toggle').addEventListener('click', () => {
|
|
976
1183
|
tagsOpen = !tagsOpen;
|
|
977
1184
|
tagBody.style.display = tagsOpen ? 'flex' : 'none';
|
|
978
1185
|
tagSection.querySelector('.side-collapse-arrow').classList.toggle('collapsed', !tagsOpen);
|
|
979
1186
|
});
|
|
980
1187
|
|
|
981
|
-
|
|
982
|
-
container.querySelector('#sidebar-new-
|
|
983
|
-
|
|
1188
|
+
container.querySelector('#sidebar-new-session') && container.querySelector('#sidebar-new-session').addEventListener('click', () => openSessionCreate(''));
|
|
1189
|
+
container.querySelector('#sidebar-new-project') && container.querySelector('#sidebar-new-project').addEventListener('click', () => openProjectCreate());
|
|
1190
|
+
|
|
1191
|
+
markActiveItems();
|
|
984
1192
|
}
|
|
985
1193
|
|
|
986
1194
|
function renderProjectTree(container) {
|
|
@@ -992,24 +1200,26 @@ function renderProjectTree(container) {
|
|
|
992
1200
|
const folder = document.createElement('div');
|
|
993
1201
|
folder.className = 'tree-folder';
|
|
994
1202
|
const isTopLevel = depth === 0;
|
|
995
|
-
folder.innerHTML =
|
|
1203
|
+
folder.innerHTML =
|
|
1204
|
+
'<span class="tf-arrow closed">' + ICON.chevron + '</span>' +
|
|
1205
|
+
'<span class="tf-icon">' + ICON.folder + '</span>' +
|
|
1206
|
+
'<span class="tf-label">' + esc(node.name) + '</span>' +
|
|
1207
|
+
(isTopLevel ? '<button class="tf-graph-btn" title="Open graph for ' + esc(node.name) + '" data-project="' + esc(node.name) + '">' + ICON.graph + '</button>' : '');
|
|
996
1208
|
const children = document.createElement('div');
|
|
997
|
-
children.style.cssText = 'display:none;flex-direction:column;gap:1px;padding-left:
|
|
1209
|
+
children.style.cssText = 'display:none;flex-direction:column;gap:1px;padding-left:18px;margin-top:2px;';
|
|
998
1210
|
|
|
999
1211
|
folder.addEventListener('click', () => {
|
|
1000
1212
|
const arrow = folder.querySelector('.tf-arrow');
|
|
1001
|
-
const
|
|
1002
|
-
arrow.
|
|
1003
|
-
children.style.display =
|
|
1213
|
+
const open = !arrow.classList.contains('closed');
|
|
1214
|
+
arrow.classList.toggle('closed', open);
|
|
1215
|
+
children.style.display = open ? 'none' : 'flex';
|
|
1004
1216
|
});
|
|
1005
1217
|
|
|
1006
1218
|
if (isTopLevel) {
|
|
1007
1219
|
folder.querySelector('.tf-graph-btn').addEventListener('click', (e) => {
|
|
1008
1220
|
e.stopPropagation();
|
|
1009
|
-
_graphProject = node.name;
|
|
1010
|
-
|
|
1011
|
-
setView('graph');
|
|
1012
|
-
renderGraphView();
|
|
1221
|
+
_graphProject = node.name; _graphData = null;
|
|
1222
|
+
setView('graph'); renderGraphView();
|
|
1013
1223
|
});
|
|
1014
1224
|
}
|
|
1015
1225
|
|
|
@@ -1022,7 +1232,7 @@ function renderProjectTree(container) {
|
|
|
1022
1232
|
row.className = 'sidebar-row';
|
|
1023
1233
|
row.dataset.path = node.path;
|
|
1024
1234
|
const name = node.name.replace('.md', '');
|
|
1025
|
-
row.innerHTML =
|
|
1235
|
+
row.innerHTML = '<span class="sr-ico">' + ICON.note + '</span><span class="sr-label">' + esc(name) + '</span>';
|
|
1026
1236
|
row.addEventListener('click', () => { setView('note'); loadNote(node.path); });
|
|
1027
1237
|
parent.appendChild(row);
|
|
1028
1238
|
} else if (node.children) {
|
|
@@ -1039,7 +1249,7 @@ function renderTagCloud(container) {
|
|
|
1039
1249
|
sorted.forEach(([tag, count]) => {
|
|
1040
1250
|
const chip = document.createElement('button');
|
|
1041
1251
|
chip.className = 'tag-chip' + (activeTagFilters.has(tag) ? ' active' : '');
|
|
1042
|
-
chip.innerHTML =
|
|
1252
|
+
chip.innerHTML = '<span class="tc-hash">#</span>' + esc(tag) + '<span class="tc-count">' + count + '</span>';
|
|
1043
1253
|
chip.addEventListener('click', () => {
|
|
1044
1254
|
if (activeTagFilters.has(tag)) activeTagFilters.delete(tag);
|
|
1045
1255
|
else activeTagFilters.add(tag);
|
|
@@ -1064,8 +1274,7 @@ function applyTagFilter() {
|
|
|
1064
1274
|
el.style.display = matches ? '' : 'none';
|
|
1065
1275
|
});
|
|
1066
1276
|
}
|
|
1067
|
-
|
|
1068
|
-
const tagBody = document.querySelector('.sidebar-scroll section:last-child > div[style*="flex-wrap"]');
|
|
1277
|
+
const tagBody = document.getElementById('tag-cloud-body');
|
|
1069
1278
|
if (tagBody) renderTagCloud(tagBody);
|
|
1070
1279
|
}
|
|
1071
1280
|
|
|
@@ -1085,31 +1294,355 @@ function openSession(path) {
|
|
|
1085
1294
|
|
|
1086
1295
|
async function deleteSession(path) {
|
|
1087
1296
|
if (!confirm('Delete this session?')) return;
|
|
1088
|
-
const res = await fetch(
|
|
1297
|
+
const res = await fetch('/api/note/' + path, { method: 'DELETE' });
|
|
1089
1298
|
const data = await res.json();
|
|
1090
1299
|
if (data.ok) {
|
|
1091
1300
|
activeSession = null;
|
|
1092
1301
|
currentPath = null;
|
|
1093
1302
|
$('content-area').innerHTML = '<div class="dim" style="padding:40px;text-align:center">Session deleted</div>';
|
|
1094
|
-
|
|
1303
|
+
refreshTree();
|
|
1095
1304
|
}
|
|
1096
1305
|
}
|
|
1097
1306
|
|
|
1098
1307
|
// ─── Load Note ───────────────────────────────────────────────────────────────
|
|
1099
1308
|
async function loadNote(path) {
|
|
1100
1309
|
currentPath = path;
|
|
1101
|
-
const note = await fetchJSON(
|
|
1310
|
+
const note = await fetchJSON('/api/note/' + path);
|
|
1102
1311
|
if (note.error) return;
|
|
1103
1312
|
currentNote = note;
|
|
1104
1313
|
markActiveItems();
|
|
1105
1314
|
updateBreadcrumb(path);
|
|
1315
|
+
if (view === 'session') { activeSession = path; renderSessionView(note); }
|
|
1316
|
+
else { renderNoteView(note); }
|
|
1317
|
+
}
|
|
1106
1318
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1319
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1320
|
+
// DASHBOARD
|
|
1321
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1322
|
+
async function renderDashboard() {
|
|
1323
|
+
const area = $('content-area');
|
|
1324
|
+
updateBreadcrumb(null);
|
|
1325
|
+
area.innerHTML =
|
|
1326
|
+
'<div class="dash" id="dash-scroll">' +
|
|
1327
|
+
'<div class="dash-head">' +
|
|
1328
|
+
'<div><div class="dash-title">Overview</div><div class="dash-sub">Your project memory at a glance</div></div>' +
|
|
1329
|
+
'<div class="dash-head-actions">' +
|
|
1330
|
+
'<button class="ghost-btn" id="dash-new-session">' + ICON.session + ' New session</button>' +
|
|
1331
|
+
'<button class="ghost-btn primary" id="dash-new-project">' + ICON.folder + ' New project</button>' +
|
|
1332
|
+
'</div>' +
|
|
1333
|
+
'</div>' +
|
|
1334
|
+
'<div class="hero-grid" id="hero-grid"></div>' +
|
|
1335
|
+
'<div class="dash-grid">' +
|
|
1336
|
+
'<div class="panel">' +
|
|
1337
|
+
'<div class="panel-head"><div class="panel-title"><span class="pt-ico">' + ICON.activity + '</span>Session activity</div><div class="dim" style="font-size:var(--fz-xs)" id="dash-activity-range">Last 14 days</div></div>' +
|
|
1338
|
+
'<div class="chart-legend" id="dash-chart-legend"></div>' +
|
|
1339
|
+
'<div class="panel-body"><div class="chart-wrap" id="dash-activity-chart"></div></div>' +
|
|
1340
|
+
'</div>' +
|
|
1341
|
+
'<div class="panel">' +
|
|
1342
|
+
'<div class="panel-head"><div class="panel-title"><span class="pt-ico">' + ICON.gauge + '</span>Token economics</div></div>' +
|
|
1343
|
+
'<div class="panel-body" id="dash-econ"></div>' +
|
|
1344
|
+
'</div>' +
|
|
1345
|
+
'</div>' +
|
|
1346
|
+
'<div class="dash-grid">' +
|
|
1347
|
+
'<div class="panel">' +
|
|
1348
|
+
'<div class="panel-head"><div class="panel-title"><span class="pt-ico">' + ICON.clock + '</span>Recent sessions</div></div>' +
|
|
1349
|
+
'<div class="panel-body"><div class="recent-list" id="dash-recent-sessions"></div></div>' +
|
|
1350
|
+
'</div>' +
|
|
1351
|
+
'<div class="panel">' +
|
|
1352
|
+
'<div class="panel-head"><div class="panel-title"><span class="pt-ico">' + ICON.folder + '</span>Projects</div></div>' +
|
|
1353
|
+
'<div class="panel-body"><div class="recent-list" id="dash-projects"></div></div>' +
|
|
1354
|
+
'</div>' +
|
|
1355
|
+
'</div>' +
|
|
1356
|
+
'<div style="height:24px"></div>' +
|
|
1357
|
+
'</div>';
|
|
1358
|
+
|
|
1359
|
+
area.querySelector('#dash-new-session').addEventListener('click', () => openSessionCreate(''));
|
|
1360
|
+
area.querySelector('#dash-new-project').addEventListener('click', () => openProjectCreate());
|
|
1361
|
+
|
|
1362
|
+
// Gather data
|
|
1363
|
+
let stats = {}, sessionsData = _sessionsData || {}, projects = [], te = {};
|
|
1364
|
+
try {
|
|
1365
|
+
[stats, sessionsData, projects, te] = await Promise.all([
|
|
1366
|
+
fetchJSON('/api/stats'),
|
|
1367
|
+
fetchJSON('/api/sessions'),
|
|
1368
|
+
fetchJSON('/api/projects'),
|
|
1369
|
+
fetchJSON('/api/token-economics'),
|
|
1370
|
+
]);
|
|
1371
|
+
} catch (e) { console.error('dashboard data error', e); }
|
|
1372
|
+
|
|
1373
|
+
_sessionsData = sessionsData || {};
|
|
1374
|
+
|
|
1375
|
+
// Flatten sessions
|
|
1376
|
+
const allSessions = [];
|
|
1377
|
+
Object.entries(sessionsData || {}).forEach(([proj, arr]) => {
|
|
1378
|
+
(arr || []).forEach(s => allSessions.push(Object.assign({ project: proj }, s)));
|
|
1379
|
+
});
|
|
1380
|
+
const totalSessions = allSessions.length;
|
|
1381
|
+
|
|
1382
|
+
// Build per-day activity from session filenames / created dates
|
|
1383
|
+
const dayCounts = buildDailySeries(allSessions, te && te.sessions);
|
|
1384
|
+
|
|
1385
|
+
// ── Hero cards ──
|
|
1386
|
+
const notesCount = (stats && stats.notes != null) ? stats.notes : 0;
|
|
1387
|
+
const projCount = (projects || []).length;
|
|
1388
|
+
let savedDisplay = '—', savedMeta = 'no data yet';
|
|
1389
|
+
if (te && te.compression_ratio > 0) {
|
|
1390
|
+
savedDisplay = te.compression_ratio + 'x';
|
|
1391
|
+
savedMeta = 'avg ' + fmtTokens(te.avg_exploration_per_session) + 't / session saved';
|
|
1392
|
+
} else if (te && te.total_exploration_tokens > 0) {
|
|
1393
|
+
savedDisplay = fmtTokens(te.total_exploration_tokens);
|
|
1394
|
+
savedMeta = 'total exploration tokens';
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const heroSparkNotes = dayCounts.map(d => d.count);
|
|
1398
|
+
const teSeries = (te && te.sessions || []).map(s => s.exploration_tokens || 0);
|
|
1399
|
+
|
|
1400
|
+
const hero = $('hero-grid');
|
|
1401
|
+
hero.innerHTML = '';
|
|
1402
|
+
hero.appendChild(makeStatCard('violet', ICON.notes, 'Notes', notesCount, (stats.tags != null ? stats.tags + ' tags · ' : '') + (stats.links != null ? stats.links + ' links' : 'knowledge base'), heroSparkNotes));
|
|
1403
|
+
hero.appendChild(makeStatCard('teal', ICON.session, 'Sessions', totalSessions, projCount + ' project' + (projCount === 1 ? '' : 's'), heroSparkNotes));
|
|
1404
|
+
hero.appendChild(makeStatCard('amber', ICON.spark, 'Tokens saved', savedDisplay, savedMeta, teSeries.length ? teSeries : heroSparkNotes));
|
|
1405
|
+
hero.appendChild(makeStatCard('violet', ICON.folder, 'Projects', projCount, ((stats.folders != null ? stats.folders : 0)) + ' folders', heroSparkNotes));
|
|
1406
|
+
|
|
1407
|
+
// ── Activity chart ──
|
|
1408
|
+
drawActivityChart($('dash-activity-chart'), $('dash-chart-legend'), dayCounts);
|
|
1409
|
+
|
|
1410
|
+
// ── Token economics bars ──
|
|
1411
|
+
renderEconBars($('dash-econ'), te);
|
|
1412
|
+
|
|
1413
|
+
// ── Recent sessions ──
|
|
1414
|
+
renderRecentSessions($('dash-recent-sessions'), allSessions);
|
|
1415
|
+
|
|
1416
|
+
// ── Projects ──
|
|
1417
|
+
renderProjectsList($('dash-projects'), projects, sessionsData);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function makeStatCard(accent, icon, label, value, meta, sparkData) {
|
|
1421
|
+
const card = document.createElement('div');
|
|
1422
|
+
card.className = 'stat-card';
|
|
1423
|
+
card.dataset.accent = accent;
|
|
1424
|
+
const valStr = (typeof value === 'number') ? value.toLocaleString() : value;
|
|
1425
|
+
card.innerHTML =
|
|
1426
|
+
'<div class="glow"></div>' +
|
|
1427
|
+
'<div class="sc-head"><span class="sc-label">' + esc(label) + '</span><span class="sc-ico">' + icon + '</span></div>' +
|
|
1428
|
+
'<div class="sc-value">' + esc(valStr) + '</div>' +
|
|
1429
|
+
'<div class="sc-foot"><span class="sc-meta">' + esc(meta) + '</span><span class="sc-spark"></span></div>';
|
|
1430
|
+
setTimeout(() => drawSparkline(card.querySelector('.sc-spark'), sparkData || [], accent), 0);
|
|
1431
|
+
return card;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function buildDailySeries(allSessions, teSessions) {
|
|
1435
|
+
// Prefer session note paths (deterministic dates); fall back to te sessions ts
|
|
1436
|
+
const DAYS = 14;
|
|
1437
|
+
const today = new Date(); today.setHours(0,0,0,0);
|
|
1438
|
+
const buckets = [];
|
|
1439
|
+
const idxByKey = {};
|
|
1440
|
+
for (let i = DAYS - 1; i >= 0; i--) {
|
|
1441
|
+
const d = new Date(today); d.setDate(today.getDate() - i);
|
|
1442
|
+
const key = d.toISOString().slice(0, 10);
|
|
1443
|
+
idxByKey[key] = buckets.length;
|
|
1444
|
+
buckets.push({ date: d, key: key, count: 0, tokens: 0 });
|
|
1112
1445
|
}
|
|
1446
|
+
(allSessions || []).forEach(s => {
|
|
1447
|
+
const fname = (s.path || '').split('/').pop().replace('.md', '');
|
|
1448
|
+
const m = fname.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
1449
|
+
let key = null;
|
|
1450
|
+
if (m) key = m[1] + '-' + m[2] + '-' + m[3];
|
|
1451
|
+
else if (s.created) key = String(s.created).slice(0, 10);
|
|
1452
|
+
if (key != null && idxByKey[key] != null) buckets[idxByKey[key]].count += 1;
|
|
1453
|
+
});
|
|
1454
|
+
(teSessions || []).forEach(s => {
|
|
1455
|
+
if (!s.ts) return;
|
|
1456
|
+
const key = String(s.ts).slice(0, 10);
|
|
1457
|
+
if (idxByKey[key] != null) buckets[idxByKey[key]].tokens += (s.exploration_tokens || 0);
|
|
1458
|
+
});
|
|
1459
|
+
return buckets;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// ─── D3 sparkline ────────────────────────────────────────────────────────────
|
|
1463
|
+
function drawSparkline(container, data, accent) {
|
|
1464
|
+
if (!container) return;
|
|
1465
|
+
container.innerHTML = '';
|
|
1466
|
+
const colors = { violet: '#8b7ff5', teal: '#5eead4', amber: '#e6b35c' };
|
|
1467
|
+
const col = colors[accent] || '#8b7ff5';
|
|
1468
|
+
const W = 96, H = 34;
|
|
1469
|
+
const arr = (data && data.length) ? data.slice(-16) : [0, 0];
|
|
1470
|
+
if (arr.length < 2) arr.push(arr[0] || 0);
|
|
1471
|
+
const max = Math.max(1, d3.max(arr));
|
|
1472
|
+
const x = d3.scaleLinear().domain([0, arr.length - 1]).range([1, W - 1]);
|
|
1473
|
+
const y = d3.scaleLinear().domain([0, max]).range([H - 3, 3]);
|
|
1474
|
+
const svg = d3.select(container).append('svg').attr('viewBox', '0 0 ' + W + ' ' + H).attr('preserveAspectRatio', 'none');
|
|
1475
|
+
const gid = 'sg-' + accent + '-' + Math.random().toString(36).slice(2, 7);
|
|
1476
|
+
const grad = svg.append('defs').append('linearGradient').attr('id', gid).attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1);
|
|
1477
|
+
grad.append('stop').attr('offset', '0%').attr('stop-color', col).attr('stop-opacity', 0.35);
|
|
1478
|
+
grad.append('stop').attr('offset', '100%').attr('stop-color', col).attr('stop-opacity', 0);
|
|
1479
|
+
const area = d3.area().x((d, i) => x(i)).y0(H).y1(d => y(d)).curve(d3.curveMonotoneX);
|
|
1480
|
+
const line = d3.line().x((d, i) => x(i)).y(d => y(d)).curve(d3.curveMonotoneX);
|
|
1481
|
+
svg.append('path').attr('d', area(arr)).attr('fill', 'url(#' + gid + ')');
|
|
1482
|
+
svg.append('path').attr('d', line(arr)).attr('fill', 'none').attr('stroke', col).attr('stroke-width', 1.6).attr('stroke-linecap', 'round').attr('stroke-linejoin', 'round');
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// ─── D3 activity area chart ──────────────────────────────────────────────────
|
|
1486
|
+
function drawActivityChart(container, legendEl, dayCounts) {
|
|
1487
|
+
if (!container) return;
|
|
1488
|
+
container.innerHTML = '';
|
|
1489
|
+
const hasTokens = dayCounts.some(d => d.tokens > 0);
|
|
1490
|
+
legendEl.innerHTML =
|
|
1491
|
+
'<div class="cl-item"><span class="cl-swatch" style="background:var(--c-teal)"></span>Sessions</div>' +
|
|
1492
|
+
(hasTokens ? '<div class="cl-item"><span class="cl-swatch" style="background:var(--c-amber)"></span>Exploration tokens</div>' : '');
|
|
1493
|
+
|
|
1494
|
+
const rect = container.getBoundingClientRect();
|
|
1495
|
+
const W = rect.width || 600, H = rect.height || 230;
|
|
1496
|
+
const m = { top: 10, right: 14, bottom: 24, left: 30 };
|
|
1497
|
+
const iw = W - m.left - m.right, ih = H - m.top - m.bottom;
|
|
1498
|
+
|
|
1499
|
+
const svg = d3.select(container).append('svg').attr('viewBox', '0 0 ' + W + ' ' + H);
|
|
1500
|
+
const g = svg.append('g').attr('transform', 'translate(' + m.left + ',' + m.top + ')');
|
|
1501
|
+
|
|
1502
|
+
const x = d3.scalePoint().domain(dayCounts.map((d, i) => i)).range([0, iw]);
|
|
1503
|
+
const maxC = Math.max(1, d3.max(dayCounts, d => d.count));
|
|
1504
|
+
const yC = d3.scaleLinear().domain([0, maxC]).nice().range([ih, 0]);
|
|
1505
|
+
const maxT = Math.max(1, d3.max(dayCounts, d => d.tokens));
|
|
1506
|
+
const yT = d3.scaleLinear().domain([0, maxT]).range([ih, 0]);
|
|
1507
|
+
|
|
1508
|
+
// gridlines
|
|
1509
|
+
yC.ticks(4).forEach(t => {
|
|
1510
|
+
g.append('line').attr('x1', 0).attr('x2', iw).attr('y1', yC(t)).attr('y2', yC(t))
|
|
1511
|
+
.attr('stroke', 'rgba(255,255,255,0.05)').attr('stroke-width', 1);
|
|
1512
|
+
g.append('text').attr('x', -8).attr('y', yC(t) + 3).attr('text-anchor', 'end')
|
|
1513
|
+
.attr('font-size', 9.5).attr('fill', '#6c6c78').style('font-family', 'JetBrains Mono, monospace').text(t);
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// x labels (every ~3rd)
|
|
1517
|
+
dayCounts.forEach((d, i) => {
|
|
1518
|
+
if (i % 3 !== 0 && i !== dayCounts.length - 1) return;
|
|
1519
|
+
g.append('text').attr('x', x(i)).attr('y', ih + 16).attr('text-anchor', 'middle')
|
|
1520
|
+
.attr('font-size', 9.5).attr('fill', '#6c6c78').style('font-family', 'JetBrains Mono, monospace')
|
|
1521
|
+
.text((d.date.getMonth() + 1) + '/' + d.date.getDate());
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
const defs = svg.append('defs');
|
|
1525
|
+
const gA = defs.append('linearGradient').attr('id', 'areaTeal').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1);
|
|
1526
|
+
gA.append('stop').attr('offset', '0%').attr('stop-color', '#5eead4').attr('stop-opacity', 0.32);
|
|
1527
|
+
gA.append('stop').attr('offset', '100%').attr('stop-color', '#5eead4').attr('stop-opacity', 0);
|
|
1528
|
+
|
|
1529
|
+
const area = d3.area().x((d, i) => x(i)).y0(ih).y1(d => yC(d.count)).curve(d3.curveMonotoneX);
|
|
1530
|
+
const lineC = d3.line().x((d, i) => x(i)).y(d => yC(d.count)).curve(d3.curveMonotoneX);
|
|
1531
|
+
g.append('path').datum(dayCounts).attr('d', area).attr('fill', 'url(#areaTeal)');
|
|
1532
|
+
g.append('path').datum(dayCounts).attr('d', lineC).attr('fill', 'none').attr('stroke', '#5eead4').attr('stroke-width', 2).attr('stroke-linecap', 'round').attr('stroke-linejoin', 'round');
|
|
1533
|
+
|
|
1534
|
+
if (hasTokens) {
|
|
1535
|
+
const lineT = d3.line().x((d, i) => x(i)).y(d => yT(d.tokens)).curve(d3.curveMonotoneX);
|
|
1536
|
+
g.append('path').datum(dayCounts).attr('d', lineT).attr('fill', 'none').attr('stroke', '#e6b35c').attr('stroke-width', 1.6).attr('stroke-dasharray', '4 3').attr('stroke-opacity', 0.9);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// tooltip + hover dots
|
|
1540
|
+
const tip = document.createElement('div');
|
|
1541
|
+
tip.className = 'chart-tooltip';
|
|
1542
|
+
container.appendChild(tip);
|
|
1543
|
+
const focus = g.append('circle').attr('r', 4).attr('fill', '#5eead4').attr('stroke', '#0a0a0c').attr('stroke-width', 1.5).style('opacity', 0);
|
|
1544
|
+
|
|
1545
|
+
svg.append('rect').attr('x', m.left).attr('y', m.top).attr('width', iw).attr('height', ih).attr('fill', 'transparent')
|
|
1546
|
+
.on('mousemove', function (ev) {
|
|
1547
|
+
const [mx] = d3.pointer(ev, g.node());
|
|
1548
|
+
let nearest = 0, best = Infinity;
|
|
1549
|
+
dayCounts.forEach((d, i) => { const dist = Math.abs(x(i) - mx); if (dist < best) { best = dist; nearest = i; } });
|
|
1550
|
+
const d = dayCounts[nearest];
|
|
1551
|
+
focus.attr('cx', x(nearest)).attr('cy', yC(d.count)).style('opacity', 1);
|
|
1552
|
+
tip.style.opacity = 1;
|
|
1553
|
+
tip.innerHTML = '<div class="tt-title">' + (d.date.getMonth() + 1) + '/' + d.date.getDate() + '</div>' +
|
|
1554
|
+
'<div class="tt-row"><span class="tt-dot" style="background:#5eead4"></span>' + d.count + ' session' + (d.count === 1 ? '' : 's') + '</div>' +
|
|
1555
|
+
(hasTokens ? '<div class="tt-row"><span class="tt-dot" style="background:#e6b35c"></span>' + fmtTokens(d.tokens) + ' tokens</div>' : '');
|
|
1556
|
+
const tx = Math.min(m.left + x(nearest) + 12, W - 120);
|
|
1557
|
+
tip.style.left = tx + 'px';
|
|
1558
|
+
tip.style.top = (m.top + yC(d.count) - 10) + 'px';
|
|
1559
|
+
})
|
|
1560
|
+
.on('mouseleave', () => { focus.style('opacity', 0); tip.style.opacity = 0; });
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function renderEconBars(container, te) {
|
|
1564
|
+
if (!container) return;
|
|
1565
|
+
if (!te || !te.session_count) {
|
|
1566
|
+
container.innerHTML = '<div class="dash-empty">No token economics recorded yet.<br>Run a few agent sessions to populate this.</div>';
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const explore = te.total_exploration_tokens || 0;
|
|
1570
|
+
const inject = te.latest_injection_tokens || 0;
|
|
1571
|
+
const savingsPct = te.per_session_savings_pct || 0;
|
|
1572
|
+
const compRatio = te.compression_ratio || 0;
|
|
1573
|
+
const avgExp = te.avg_exploration_per_session || 0;
|
|
1574
|
+
|
|
1575
|
+
// Exploration vs injection — show injection as a fraction of avg exploration
|
|
1576
|
+
const injectFrac = avgExp > 0 ? Math.min(100, Math.round((inject / avgExp) * 100)) : 0;
|
|
1577
|
+
// Compression: map ratio onto a bar (cap at 20x = 100%)
|
|
1578
|
+
const compFrac = compRatio > 0 ? Math.min(100, Math.round((compRatio / 20) * 100)) : 0;
|
|
1579
|
+
|
|
1580
|
+
const rows = [
|
|
1581
|
+
{ label: 'Per-session savings', val: savingsPct + '%', pct: Math.min(100, savingsPct), cls: 'teal' },
|
|
1582
|
+
{ label: 'Compression ratio', val: (compRatio > 0 ? compRatio + 'x' : '—'), pct: compFrac, cls: 'violet' },
|
|
1583
|
+
{ label: 'Injection vs cold-start', val: fmtTokens(inject) + 't / ' + fmtTokens(avgExp) + 't', pct: injectFrac, cls: 'amber' },
|
|
1584
|
+
];
|
|
1585
|
+
container.innerHTML = rows.map(r =>
|
|
1586
|
+
'<div class="econ-row">' +
|
|
1587
|
+
'<div class="econ-top"><span class="econ-label">' + r.label + '</span><span class="econ-val">' + r.val + '</span></div>' +
|
|
1588
|
+
'<div class="bar-track"><div class="bar-fill ' + r.cls + '" style="width:0%" data-w="' + r.pct + '"></div></div>' +
|
|
1589
|
+
'</div>'
|
|
1590
|
+
).join('') +
|
|
1591
|
+
'<div style="display:flex;justify-content:space-between;margin-top:18px;padding-top:14px;border-top:1px solid var(--line);font-size:var(--fz-xs);color:var(--dim)">' +
|
|
1592
|
+
'<span>total explore <span class="mut tab-nums">' + fmtTokens(explore) + 't</span></span>' +
|
|
1593
|
+
'<span>' + (te.session_count || 0) + ' sessions tracked</span>' +
|
|
1594
|
+
'</div>';
|
|
1595
|
+
// animate
|
|
1596
|
+
setTimeout(() => { container.querySelectorAll('.bar-fill').forEach(b => { b.style.width = b.dataset.w + '%'; }); }, 60);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function renderRecentSessions(container, allSessions) {
|
|
1600
|
+
if (!container) return;
|
|
1601
|
+
if (!allSessions.length) {
|
|
1602
|
+
container.innerHTML = '<div class="dash-empty">No sessions yet.</div>';
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const sorted = [...allSessions].sort((a, b) => (b.path || '').localeCompare(a.path || '')).slice(0, 6);
|
|
1606
|
+
container.innerHTML = '';
|
|
1607
|
+
sorted.forEach(s => {
|
|
1608
|
+
const item = document.createElement('button');
|
|
1609
|
+
item.className = 'recent-item';
|
|
1610
|
+
const time = formatSessionTime((s.path || '').split('/').pop());
|
|
1611
|
+
const sub = s.summary ? s.summary : s.project;
|
|
1612
|
+
item.innerHTML =
|
|
1613
|
+
'<span class="ri-ico">' + ICON.session + '</span>' +
|
|
1614
|
+
'<span class="ri-body"><span class="ri-title">' + esc(s.project) + '</span><span class="ri-sub">' + esc(sub) + '</span></span>' +
|
|
1615
|
+
'<span class="ri-meta">' + esc(time) + '</span>';
|
|
1616
|
+
item.addEventListener('click', () => openSession(s.path));
|
|
1617
|
+
container.appendChild(item);
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function renderProjectsList(container, projects, sessionsData) {
|
|
1622
|
+
if (!container) return;
|
|
1623
|
+
if (!projects || !projects.length) {
|
|
1624
|
+
container.innerHTML = '<div class="dash-empty">No projects yet. Create one to get started.</div>';
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
container.innerHTML = '';
|
|
1628
|
+
projects.slice(0, 8).forEach(p => {
|
|
1629
|
+
const item = document.createElement('button');
|
|
1630
|
+
item.className = 'recent-item';
|
|
1631
|
+
const knowledgePath = p.name + '/Knowledge.md';
|
|
1632
|
+
item.innerHTML =
|
|
1633
|
+
'<span class="ri-ico proj">' + ICON.folder + '</span>' +
|
|
1634
|
+
'<span class="ri-body"><span class="ri-title">' + esc(p.name) + '</span><span class="ri-sub">' + p.session_count + ' session' + (p.session_count === 1 ? '' : 's') + '</span></span>' +
|
|
1635
|
+
'<span class="ri-meta">' + ICON.graph + '</span>';
|
|
1636
|
+
item.addEventListener('click', () => {
|
|
1637
|
+
if (allNotes[knowledgePath]) { setView('note'); loadNote(knowledgePath); }
|
|
1638
|
+
else { _graphProject = p.name; _graphData = null; setView('graph'); renderGraphView(); }
|
|
1639
|
+
});
|
|
1640
|
+
item.querySelector('.ri-meta').addEventListener('click', (e) => {
|
|
1641
|
+
e.stopPropagation();
|
|
1642
|
+
_graphProject = p.name; _graphData = null; setView('graph'); renderGraphView();
|
|
1643
|
+
});
|
|
1644
|
+
container.appendChild(item);
|
|
1645
|
+
});
|
|
1113
1646
|
}
|
|
1114
1647
|
|
|
1115
1648
|
// ─── Note View ───────────────────────────────────────────────────────────────
|
|
@@ -1121,65 +1654,47 @@ function renderNoteView(note) {
|
|
|
1121
1654
|
let md = note.content || '';
|
|
1122
1655
|
md = md.replace(/\[\[([^\]]+)\]\]/g, (_, link) => {
|
|
1123
1656
|
const display = link.includes('#') ? link.split('#').pop() : link;
|
|
1124
|
-
return
|
|
1657
|
+
return '<span class="wikilink" data-link="' + esc(link) + '">' + esc(display) + '</span>';
|
|
1125
1658
|
});
|
|
1126
1659
|
|
|
1127
1660
|
const tagsHtml = (note.tags || []).map(t =>
|
|
1128
|
-
|
|
1661
|
+
'<button class="tag-chip" data-tag="' + esc(t) + '"><span class="tc-hash">#</span>' + esc(t) + '</button>'
|
|
1129
1662
|
).join('');
|
|
1130
1663
|
|
|
1131
1664
|
const railClass = railOpen ? '' : ' no-rail';
|
|
1132
|
-
area.innerHTML =
|
|
1133
|
-
<div class="note-view
|
|
1134
|
-
<article class="note-article">
|
|
1135
|
-
<div class="
|
|
1136
|
-
<
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
<
|
|
1140
|
-
</div>
|
|
1141
|
-
<
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
</
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
<span><span class="mut tab-nums">${wordCount.toLocaleString()}</span> words</span>
|
|
1153
|
-
<span>·</span>
|
|
1154
|
-
<span><span class="mut">${readTime}</span> read</span>
|
|
1155
|
-
</div>
|
|
1156
|
-
<div class="md-body">${marked.parse(md)}</div>
|
|
1157
|
-
<div style="height:80px"></div>
|
|
1158
|
-
</article>
|
|
1159
|
-
${railOpen ? `<aside class="note-rail" id="note-rail"></aside>` : ''}
|
|
1160
|
-
</div>
|
|
1161
|
-
`;
|
|
1665
|
+
area.innerHTML =
|
|
1666
|
+
'<div class="note-view' + railClass + '">' +
|
|
1667
|
+
'<article class="note-article">' +
|
|
1668
|
+
'<div class="note-tags-strip">' +
|
|
1669
|
+
'<div class="note-tags-left">' + tagsHtml +
|
|
1670
|
+
'<span class="note-dates">created <span class="mut tab-nums">' + esc(note.created || '') + '</span> · edited <span class="mut tab-nums">' + esc(note.updated || '') + '</span></span>' +
|
|
1671
|
+
'</div>' +
|
|
1672
|
+
'<div class="note-tags-right"><button class="ghost-btn" id="note-edit-btn">' + ICON.edit + ' Edit</button></div>' +
|
|
1673
|
+
'</div>' +
|
|
1674
|
+
'<h1 class="note-title">' + esc(note.title) + '</h1>' +
|
|
1675
|
+
'<div class="note-meta">' +
|
|
1676
|
+
'<span><span class="mut tab-nums">' + wordCount.toLocaleString() + '</span> words</span>' +
|
|
1677
|
+
'<span>·</span><span><span class="mut">' + readTime + '</span> read</span>' +
|
|
1678
|
+
'<span>·</span><span class="mono dim">' + esc(note.path.replace('.md', '')) + '</span>' +
|
|
1679
|
+
'</div>' +
|
|
1680
|
+
'<div class="md-body">' + marked.parse(md) + '</div>' +
|
|
1681
|
+
'<div style="height:80px"></div>' +
|
|
1682
|
+
'</article>' +
|
|
1683
|
+
(railOpen ? '<aside class="note-rail" id="note-rail"></aside>' : '') +
|
|
1684
|
+
'</div>';
|
|
1162
1685
|
|
|
1163
|
-
// Wire wikilinks
|
|
1164
1686
|
area.querySelectorAll('.wikilink').forEach(el => {
|
|
1165
1687
|
el.addEventListener('click', () => {
|
|
1166
1688
|
const target = findNotePath(el.dataset.link.split('#')[0]);
|
|
1167
1689
|
if (target) loadNote(target);
|
|
1168
1690
|
});
|
|
1169
1691
|
});
|
|
1170
|
-
|
|
1171
|
-
// Wire tag clicks
|
|
1172
1692
|
area.querySelectorAll('.tag-chip[data-tag]').forEach(el => {
|
|
1173
|
-
el.addEventListener('click', () => {
|
|
1174
|
-
activeTagFilters.add(el.dataset.tag);
|
|
1175
|
-
applyTagFilter();
|
|
1176
|
-
});
|
|
1693
|
+
el.addEventListener('click', () => { activeTagFilters.add(el.dataset.tag); applyTagFilter(); });
|
|
1177
1694
|
});
|
|
1695
|
+
const editBtn = area.querySelector('#note-edit-btn');
|
|
1696
|
+
if (editBtn) editBtn.addEventListener('click', () => openEditor(note.path));
|
|
1178
1697
|
|
|
1179
|
-
// Wire edit button
|
|
1180
|
-
area.querySelector('#note-edit-btn')?.addEventListener('click', () => openEditor(note.path));
|
|
1181
|
-
|
|
1182
|
-
// Render rail
|
|
1183
1698
|
if (railOpen) renderRail(note);
|
|
1184
1699
|
}
|
|
1185
1700
|
|
|
@@ -1189,41 +1704,38 @@ function renderRail(note) {
|
|
|
1189
1704
|
if (!rail) return;
|
|
1190
1705
|
rail.innerHTML = '';
|
|
1191
1706
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
right: `<div style="display:flex;gap:2px"><button class="rail-icon-btn" id="rail-graph-expand" title="Open full graph">⤢</button></div>`
|
|
1707
|
+
const graphCard = createRailCard('local graph', ICON.graph, null, {
|
|
1708
|
+
right: '<button class="rail-icon-btn" id="rail-graph-expand" title="Open full graph">' + ICON.expand + '</button>'
|
|
1195
1709
|
});
|
|
1196
1710
|
const graphBody = graphCard.querySelector('.rail-card-body');
|
|
1197
|
-
graphBody.style.cssText = 'position:relative;height:240px;padding:0;overflow:hidden;';
|
|
1711
|
+
graphBody.style.cssText = 'position:relative;height:240px;padding:0;overflow:hidden;margin:0 12px 12px;border:1px solid var(--line);border-radius:var(--r);background:rgba(0,0,0,0.2);';
|
|
1198
1712
|
renderLocalGraph(graphBody, note);
|
|
1199
1713
|
rail.appendChild(graphCard);
|
|
1200
1714
|
|
|
1201
|
-
graphCard.querySelector('#rail-graph-expand')
|
|
1715
|
+
const expBtn = graphCard.querySelector('#rail-graph-expand');
|
|
1716
|
+
if (expBtn) expBtn.addEventListener('click', () => {
|
|
1202
1717
|
const parts = (note.path || '').split('/');
|
|
1203
|
-
_graphProject = parts.length > 1 ? parts[0] : null;
|
|
1204
|
-
|
|
1205
|
-
setView('graph');
|
|
1206
|
-
renderGraphView();
|
|
1718
|
+
_graphProject = parts.length > 1 ? parts[0] : null; _graphData = null;
|
|
1719
|
+
setView('graph'); renderGraphView();
|
|
1207
1720
|
});
|
|
1208
1721
|
|
|
1209
|
-
// Outline
|
|
1210
1722
|
const headings = [];
|
|
1211
1723
|
(note.content || '').split('\n').forEach(line => {
|
|
1212
1724
|
const m = line.match(/^(#{1,3})\s+(.+)/);
|
|
1213
1725
|
if (m) headings.push({ level: m[1].length, text: m[2].trim() });
|
|
1214
1726
|
});
|
|
1215
1727
|
if (headings.length > 1) {
|
|
1216
|
-
const outCard = createRailCard(
|
|
1728
|
+
const outCard = createRailCard('outline <span class="dim tab-nums" style="margin-left:6px">' + headings.length + '</span>', ICON.list, null, { collapsible: true });
|
|
1217
1729
|
const outBody = outCard.querySelector('.rail-card-body');
|
|
1218
1730
|
outBody.style.cssText = 'display:flex;flex-direction:column;gap:2px;';
|
|
1219
1731
|
headings.forEach(h => {
|
|
1220
1732
|
const el = document.createElement('div');
|
|
1221
1733
|
el.className = 'outline-item' + (h.level === 1 ? ' lv1' : '');
|
|
1222
|
-
el.style.paddingLeft = (
|
|
1734
|
+
el.style.paddingLeft = (6 + (h.level - 1) * 12) + 'px';
|
|
1223
1735
|
el.textContent = h.text;
|
|
1224
1736
|
el.addEventListener('click', () => {
|
|
1225
1737
|
const target = Array.from(document.querySelectorAll('.md-body h1,.md-body h2,.md-body h3')).find(
|
|
1226
|
-
|
|
1738
|
+
x => x.textContent.trim() === h.text
|
|
1227
1739
|
);
|
|
1228
1740
|
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1229
1741
|
});
|
|
@@ -1232,15 +1744,14 @@ function renderRail(note) {
|
|
|
1232
1744
|
rail.appendChild(outCard);
|
|
1233
1745
|
}
|
|
1234
1746
|
|
|
1235
|
-
// Backlinks
|
|
1236
1747
|
const backlinks = note.backlinks || [];
|
|
1237
1748
|
if (backlinks.length > 0) {
|
|
1238
|
-
const blCard = createRailCard(
|
|
1749
|
+
const blCard = createRailCard('backlinks <span class="dim tab-nums" style="margin-left:6px">' + backlinks.length + '</span>', ICON.link, null, { collapsible: true });
|
|
1239
1750
|
const blBody = blCard.querySelector('.rail-card-body');
|
|
1240
1751
|
backlinks.forEach(bl => {
|
|
1241
1752
|
const item = document.createElement('div');
|
|
1242
1753
|
item.className = 'bl-item';
|
|
1243
|
-
item.innerHTML =
|
|
1754
|
+
item.innerHTML = ICON.arrowLeft + '<span>' + esc(bl.title || bl.path) + '</span>';
|
|
1244
1755
|
item.addEventListener('click', () => loadNote(bl.path));
|
|
1245
1756
|
blBody.appendChild(item);
|
|
1246
1757
|
});
|
|
@@ -1248,18 +1759,18 @@ function renderRail(note) {
|
|
|
1248
1759
|
}
|
|
1249
1760
|
}
|
|
1250
1761
|
|
|
1251
|
-
function createRailCard(title, content, opts
|
|
1762
|
+
function createRailCard(title, icon, content, opts) {
|
|
1763
|
+
opts = opts || {};
|
|
1252
1764
|
const card = document.createElement('div');
|
|
1253
1765
|
card.className = 'rail-card';
|
|
1254
1766
|
let collapseHtml = '';
|
|
1255
|
-
if (opts.collapsible) collapseHtml = '<span class="side-collapse-arrow" style="margin-right:4px"
|
|
1256
|
-
card.innerHTML =
|
|
1257
|
-
<div class="rail-card-header">
|
|
1258
|
-
<button class="rail-card-title"
|
|
1259
|
-
|
|
1260
|
-
</div>
|
|
1261
|
-
<div class="rail-card-body"
|
|
1262
|
-
`;
|
|
1767
|
+
if (opts.collapsible) collapseHtml = '<span class="side-collapse-arrow" style="margin-right:4px">' + ICON.caretDown + '</span>';
|
|
1768
|
+
card.innerHTML =
|
|
1769
|
+
'<div class="rail-card-header">' +
|
|
1770
|
+
'<button class="rail-card-title">' + collapseHtml + (icon ? '<span style="display:inline-flex">' + icon + '</span>' : '') + '<span>' + title + '</span></button>' +
|
|
1771
|
+
(opts.right || '') +
|
|
1772
|
+
'</div>' +
|
|
1773
|
+
'<div class="rail-card-body">' + (content || '') + '</div>';
|
|
1263
1774
|
if (opts.collapsible) {
|
|
1264
1775
|
const titleBtn = card.querySelector('.rail-card-title');
|
|
1265
1776
|
const body = card.querySelector('.rail-card-body');
|
|
@@ -1277,31 +1788,22 @@ function createRailCard(title, content, opts = {}) {
|
|
|
1277
1788
|
function renderLocalGraph(container, note) {
|
|
1278
1789
|
const nodes = new Map();
|
|
1279
1790
|
const links = [];
|
|
1280
|
-
|
|
1281
1791
|
nodes.set(note.path, { id: note.path, title: note.title, kind: 'note', center: true });
|
|
1282
1792
|
|
|
1283
1793
|
(note.links || []).forEach(link => {
|
|
1284
1794
|
const path = findNotePath(link);
|
|
1285
1795
|
if (path && !nodes.has(path)) {
|
|
1286
|
-
nodes.set(path, { id: path, title: allNotes[path]
|
|
1796
|
+
nodes.set(path, { id: path, title: allNotes[path] ? allNotes[path].title : link, kind: 'note', center: false });
|
|
1287
1797
|
links.push({ source: note.path, target: path });
|
|
1288
|
-
} else if (path) {
|
|
1289
|
-
links.push({ source: note.path, target: path });
|
|
1290
|
-
}
|
|
1798
|
+
} else if (path) { links.push({ source: note.path, target: path }); }
|
|
1291
1799
|
});
|
|
1292
|
-
|
|
1293
1800
|
(note.backlinks || []).forEach(bl => {
|
|
1294
1801
|
const p = bl.path;
|
|
1295
|
-
if (p && !nodes.has(p)) {
|
|
1296
|
-
nodes.set(p, { id: p, title: bl.title || p, kind: isSessionPath(p) ? 'session' : 'note', center: false });
|
|
1297
|
-
}
|
|
1802
|
+
if (p && !nodes.has(p)) nodes.set(p, { id: p, title: bl.title || p, kind: isSessionPath(p) ? 'session' : 'note', center: false });
|
|
1298
1803
|
if (p) links.push({ source: p, target: note.path });
|
|
1299
1804
|
});
|
|
1300
|
-
|
|
1301
1805
|
(note.related || []).slice(0, 5).forEach(r => {
|
|
1302
|
-
if (r.path && !nodes.has(r.path)) {
|
|
1303
|
-
nodes.set(r.path, { id: r.path, title: r.title, kind: isSessionPath(r.path) ? 'session' : 'note', center: false });
|
|
1304
|
-
}
|
|
1806
|
+
if (r.path && !nodes.has(r.path)) nodes.set(r.path, { id: r.path, title: r.title, kind: isSessionPath(r.path) ? 'session' : 'note', center: false });
|
|
1305
1807
|
if (r.path) links.push({ source: note.path, target: r.path, session: isSessionPath(r.path) });
|
|
1306
1808
|
});
|
|
1307
1809
|
|
|
@@ -1311,10 +1813,8 @@ function renderLocalGraph(container, note) {
|
|
|
1311
1813
|
return;
|
|
1312
1814
|
}
|
|
1313
1815
|
|
|
1314
|
-
const width = 100, height = 100;
|
|
1315
1816
|
const svg = d3.select(container).append('svg')
|
|
1316
|
-
.attr('viewBox', '0 0 100 100')
|
|
1317
|
-
.attr('preserveAspectRatio', 'xMidYMid meet')
|
|
1817
|
+
.attr('viewBox', '0 0 100 100').attr('preserveAspectRatio', 'xMidYMid meet')
|
|
1318
1818
|
.style('width', '100%').style('height', '100%');
|
|
1319
1819
|
|
|
1320
1820
|
const simulation = d3.forceSimulation(nodeArray)
|
|
@@ -1322,84 +1822,57 @@ function renderLocalGraph(container, note) {
|
|
|
1322
1822
|
.force('charge', d3.forceManyBody().strength(-80))
|
|
1323
1823
|
.force('center', d3.forceCenter(50, 50))
|
|
1324
1824
|
.force('collision', d3.forceCollide(8));
|
|
1325
|
-
|
|
1326
|
-
simulation.on('tick', () => {
|
|
1327
|
-
nodeArray.forEach(d => {
|
|
1328
|
-
d.x = Math.max(10, Math.min(90, d.x));
|
|
1329
|
-
d.y = Math.max(10, Math.min(90, d.y));
|
|
1330
|
-
});
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
// Run simulation
|
|
1825
|
+
simulation.on('tick', () => { nodeArray.forEach(d => { d.x = Math.max(10, Math.min(90, d.x)); d.y = Math.max(10, Math.min(90, d.y)); }); });
|
|
1334
1826
|
for (let i = 0; i < 120; i++) simulation.tick();
|
|
1335
1827
|
simulation.stop();
|
|
1336
1828
|
|
|
1337
|
-
// Draw edges
|
|
1338
1829
|
links.forEach(l => {
|
|
1339
1830
|
const s = typeof l.source === 'object' ? l.source : nodeArray.find(n => n.id === l.source);
|
|
1340
1831
|
const t = typeof l.target === 'object' ? l.target : nodeArray.find(n => n.id === l.target);
|
|
1341
1832
|
if (!s || !t) return;
|
|
1342
1833
|
const isSession = l.session || s.kind === 'session' || t.kind === 'session';
|
|
1343
|
-
svg.append('line')
|
|
1344
|
-
.attr('
|
|
1345
|
-
.attr('stroke', 'var(--line-2)')
|
|
1346
|
-
.attr('stroke-width', 0.22)
|
|
1834
|
+
svg.append('line').attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
|
|
1835
|
+
.attr('stroke', 'rgba(255,255,255,0.18)').attr('stroke-width', 0.3)
|
|
1347
1836
|
.attr('stroke-opacity', isSession ? 0.5 : 0.85)
|
|
1348
|
-
.attr('stroke-dasharray', isSession ? '0.9 0.9' : '')
|
|
1349
|
-
.attr('stroke-linecap', 'round');
|
|
1837
|
+
.attr('stroke-dasharray', isSession ? '0.9 0.9' : '').attr('stroke-linecap', 'round');
|
|
1350
1838
|
});
|
|
1351
1839
|
|
|
1352
|
-
// Draw nodes
|
|
1353
1840
|
nodeArray.forEach(n => {
|
|
1354
|
-
const r = n.center ? 5 : (n.kind === 'session' ? 2.
|
|
1841
|
+
const r = n.center ? 5 : (n.kind === 'session' ? 2.4 : 3.2);
|
|
1355
1842
|
const g = svg.append('g').style('cursor', 'pointer');
|
|
1356
|
-
|
|
1357
1843
|
if (n.center) {
|
|
1358
|
-
g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r * 1.9)
|
|
1359
|
-
|
|
1360
|
-
g.append('
|
|
1361
|
-
.attr('fill', '
|
|
1362
|
-
g.append('text').attr('x', n.x).attr('y', n.y + r + 6)
|
|
1363
|
-
.attr('text-anchor', 'middle').attr('font-size', 2.5).attr('font-weight', 500)
|
|
1364
|
-
.attr('fill', 'var(--accent)')
|
|
1365
|
-
.style('font-family', 'JetBrains Mono, monospace')
|
|
1844
|
+
g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r * 1.9).attr('fill', '#8b7ff5').attr('fill-opacity', 0.22);
|
|
1845
|
+
g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', '#8b7ff5');
|
|
1846
|
+
g.append('text').attr('x', n.x).attr('y', n.y + r + 6).attr('text-anchor', 'middle').attr('font-size', 2.5).attr('font-weight', 600)
|
|
1847
|
+
.attr('fill', '#b7adfb').style('font-family', 'JetBrains Mono, monospace')
|
|
1366
1848
|
.text(n.title.length > 24 ? n.title.substring(0, 22) + '…' : n.title);
|
|
1367
1849
|
} else if (n.kind === 'session') {
|
|
1368
|
-
g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
|
|
1369
|
-
.attr('fill', 'var(--bg)').attr('stroke', 'var(--muted)').attr('stroke-width', 0.35);
|
|
1850
|
+
g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', '#0b0b0e').attr('stroke', '#5eead4').attr('stroke-width', 0.4);
|
|
1370
1851
|
} else {
|
|
1371
|
-
g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
|
|
1372
|
-
.attr('fill', 'var(--panel-2)').attr('stroke', 'var(--accent)').attr('stroke-width', 0.5);
|
|
1852
|
+
g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', 'rgba(255,255,255,0.06)').attr('stroke', '#8b7ff5').attr('stroke-width', 0.5);
|
|
1373
1853
|
}
|
|
1374
|
-
|
|
1375
1854
|
g.on('click', () => { if (!n.center) loadNote(n.id); });
|
|
1376
1855
|
});
|
|
1377
1856
|
}
|
|
1378
1857
|
|
|
1379
1858
|
// ─── Session View ────────────────────────────────────────────────────────────
|
|
1380
1859
|
function extractSection(content, sectionName) {
|
|
1381
|
-
const idx = content.indexOf(
|
|
1860
|
+
const idx = content.indexOf('## ' + sectionName);
|
|
1382
1861
|
if (idx === -1) return '';
|
|
1383
1862
|
const nextSec = content.indexOf('\n## ', idx + 5);
|
|
1384
1863
|
const block = nextSec !== -1 ? content.substring(idx, nextSec) : content.substring(idx);
|
|
1385
|
-
return block.replace(
|
|
1864
|
+
return block.replace('## ' + sectionName, '').trim();
|
|
1386
1865
|
}
|
|
1387
1866
|
|
|
1388
1867
|
function renderSessionView(note) {
|
|
1389
1868
|
const area = $('content-area');
|
|
1390
1869
|
const content = note.content || '';
|
|
1391
|
-
|
|
1392
|
-
// Extract summary
|
|
1393
1870
|
let summary = extractSection(content, 'Summary');
|
|
1394
|
-
|
|
1395
|
-
// Extract session ID
|
|
1396
1871
|
const pathParts = note.path.split('/');
|
|
1397
1872
|
const sessionFile = pathParts[pathParts.length - 1].replace('.md', '');
|
|
1398
1873
|
const projectName = pathParts[0] || '';
|
|
1399
|
-
|
|
1400
1874
|
const wordCount = content.split(/\s+/).filter(Boolean).length;
|
|
1401
1875
|
|
|
1402
|
-
// Extract each section
|
|
1403
1876
|
const promptsRaw = extractSection(content, 'PROMPTS');
|
|
1404
1877
|
const investigated = extractSection(content, 'INVESTIGATED');
|
|
1405
1878
|
const learned = extractSection(content, 'LEARNED');
|
|
@@ -1407,74 +1880,51 @@ function renderSessionView(note) {
|
|
|
1407
1880
|
const nextSteps = extractSection(content, 'NEXT STEPS');
|
|
1408
1881
|
|
|
1409
1882
|
const sectionNames = ['Summary', 'PROMPTS', 'INVESTIGATED', 'LEARNED', 'COMPLETED', 'NEXT STEPS'];
|
|
1410
|
-
const presentSections = sectionNames.filter(s => content.includes(
|
|
1411
|
-
|
|
1412
|
-
// Count prompt entries
|
|
1883
|
+
const presentSections = sectionNames.filter(s => content.includes('## ' + s));
|
|
1413
1884
|
const promptEntries = promptsRaw.split(/^###\s+/m).filter(p => p.trim());
|
|
1414
1885
|
const hasMultiplePrompts = promptEntries.length > 1;
|
|
1415
1886
|
|
|
1416
1887
|
const tagsHtml = (note.tags || ['session', 'auto-captured']).map(t =>
|
|
1417
|
-
|
|
1888
|
+
'<button class="tag-chip"><span class="tc-hash">#</span>' + esc(t) + '</button>'
|
|
1418
1889
|
).join('');
|
|
1419
1890
|
|
|
1420
|
-
|
|
1421
|
-
<
|
|
1422
|
-
<div
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
<
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
<span>
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
</div
|
|
1448
|
-
|
|
1449
|
-
<div class="
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
</div>` : ''}
|
|
1458
|
-
${learned ? `
|
|
1459
|
-
<div class="session-section">
|
|
1460
|
-
<div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Learned</div>
|
|
1461
|
-
<div class="ss-body md-body">${marked.parse(learned)}</div>
|
|
1462
|
-
</div>` : ''}
|
|
1463
|
-
${completed ? `
|
|
1464
|
-
<div class="session-section">
|
|
1465
|
-
<div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Completed</div>
|
|
1466
|
-
<div class="ss-body md-body">${marked.parse(completed)}</div>
|
|
1467
|
-
</div>` : ''}
|
|
1468
|
-
${nextSteps ? `
|
|
1469
|
-
<div class="session-section">
|
|
1470
|
-
<div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Next Steps</div>
|
|
1471
|
-
<div class="ss-body md-body">${marked.parse(nextSteps)}</div>
|
|
1472
|
-
</div>` : ''}
|
|
1473
|
-
<div style="height:60px"></div>
|
|
1474
|
-
</article>
|
|
1475
|
-
`;
|
|
1476
|
-
|
|
1477
|
-
// Render prompts with scrollable container
|
|
1891
|
+
const sec = (title, bodyHtml, extra) =>
|
|
1892
|
+
'<div class="session-section">' +
|
|
1893
|
+
'<div class="ss-header"><span class="ss-hico">' + ICON.doc + '</span>' + title + (extra || '') + '</div>' +
|
|
1894
|
+
bodyHtml +
|
|
1895
|
+
'</div>';
|
|
1896
|
+
|
|
1897
|
+
area.innerHTML =
|
|
1898
|
+
'<article class="session-view">' +
|
|
1899
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;padding:2px 0 16px;gap:10px;flex-wrap:wrap">' +
|
|
1900
|
+
'<div style="display:flex;gap:7px;align-items:center;flex-wrap:wrap">' +
|
|
1901
|
+
'<span class="session-status done"><i style="width:6px;height:6px;border-radius:999px;background:var(--muted);display:inline-block"></i> done</span>' +
|
|
1902
|
+
tagsHtml +
|
|
1903
|
+
'<span class="dim" style="font-size:var(--fz-xs);margin-left:8px">' + esc(note.created || sessionFile) + '</span>' +
|
|
1904
|
+
'</div>' +
|
|
1905
|
+
'<div style="display:flex;gap:7px">' +
|
|
1906
|
+
'<button class="ghost-btn" id="session-edit-btn">' + ICON.edit + ' Edit</button>' +
|
|
1907
|
+
'<button class="ghost-btn" id="session-delete-btn">' + ICON.trash + ' Delete</button>' +
|
|
1908
|
+
'</div>' +
|
|
1909
|
+
'</div>' +
|
|
1910
|
+
'<h1 style="margin:10px 0 6px;font-size:calc(var(--fz-xl) + 8px);font-weight:700;letter-spacing:-0.02em">Session ' + esc(sessionFile) + '</h1>' +
|
|
1911
|
+
'<div class="dim" style="font-size:var(--fz-xs);margin-bottom:24px;display:flex;gap:16px;align-items:center">' +
|
|
1912
|
+
'<span>words <span class="mut tab-nums">' + wordCount.toLocaleString() + '</span></span><span>·</span>' +
|
|
1913
|
+
'<span>project <span class="acc">' + esc(projectName) + '</span></span><span>·</span>' +
|
|
1914
|
+
'<span>sections <span class="mut tab-nums">' + presentSections.length + '</span></span>' +
|
|
1915
|
+
'</div>' +
|
|
1916
|
+
(summary ? sec('Summary', '<div class="ss-body md-body">' + marked.parse(summary) + '</div>') : '') +
|
|
1917
|
+
(promptsRaw ? sec('Prompts', '<div class="ss-body prompts-list' + (hasMultiplePrompts ? ' scrollable' : '') + '" id="session-prompts"></div>', '<span class="dim tab-nums" style="font-size:var(--fz-xs);margin-left:6px">' + promptEntries.length + '</span>') : '') +
|
|
1918
|
+
(investigated ? sec('Investigated', '<div class="ss-body md-body">' + marked.parse(investigated) + '</div>') : '') +
|
|
1919
|
+
(learned ? sec('Learned', '<div class="ss-body md-body">' + marked.parse(learned) + '</div>') : '') +
|
|
1920
|
+
(completed ? sec('Completed', '<div class="ss-body md-body">' + marked.parse(completed) + '</div>') : '') +
|
|
1921
|
+
(nextSteps ? sec('Next Steps', '<div class="ss-body md-body">' + marked.parse(nextSteps) + '</div>') : '') +
|
|
1922
|
+
'<div style="height:60px"></div>' +
|
|
1923
|
+
'</article>';
|
|
1924
|
+
|
|
1925
|
+
$('session-edit-btn')?.addEventListener('click', () => openEditor(note.path));
|
|
1926
|
+
$('session-delete-btn')?.addEventListener('click', () => deleteSession(note.path));
|
|
1927
|
+
|
|
1478
1928
|
const promptsContainer = $('session-prompts');
|
|
1479
1929
|
if (promptsContainer && promptEntries.length > 0) {
|
|
1480
1930
|
promptEntries.forEach(entry => {
|
|
@@ -1492,82 +1942,71 @@ let _graphProject = null;
|
|
|
1492
1942
|
|
|
1493
1943
|
function renderGraphView() {
|
|
1494
1944
|
const area = $('content-area');
|
|
1495
|
-
area.innerHTML =
|
|
1496
|
-
<div class="graph-view">
|
|
1497
|
-
<section class="graph-canvas">
|
|
1498
|
-
<div class="graph-header">
|
|
1499
|
-
<div class="graph-header-left">
|
|
1500
|
-
<span class="
|
|
1501
|
-
<button class="tool-chip graph-mode active" data-mode="projects">projects</button>
|
|
1502
|
-
<button class="tool-chip graph-mode" data-mode="sessions">sessions</button>
|
|
1503
|
-
<span class="dim tab-nums" id="graph-stats"></span>
|
|
1504
|
-
<span id="graph-project-filter" style="display:none;font-size:var(--fz-sm);color:var(--accent);margin-left:4px">
|
|
1505
|
-
<span id="graph-project-name"></span>
|
|
1506
|
-
<button class="ghost-btn" id="graph-clear-project" style="color:var(--muted);margin-left:4px;font-size:var(--fz-xs)" title="Show all projects">✕</button>
|
|
1507
|
-
</span>
|
|
1508
|
-
</div>
|
|
1509
|
-
<div class="graph-header-right">
|
|
1510
|
-
<button class="tool-chip graph-layout active" data-layout="force">force</button>
|
|
1511
|
-
<button class="tool-chip graph-layout" data-layout="radial">radial</button>
|
|
1512
|
-
<button class="tool-chip graph-layout" data-layout="time">time</button>
|
|
1513
|
-
<button class="ghost-btn" id="graph-close">✕</button>
|
|
1514
|
-
</div>
|
|
1515
|
-
</div>
|
|
1516
|
-
<div class="graph-svg-wrap" id="graph-svg-wrap"></div>
|
|
1517
|
-
<div class="graph-legend" id="graph-legend"></div>
|
|
1518
|
-
</section>
|
|
1519
|
-
<aside class="graph-rail" id="graph-rail">
|
|
1520
|
-
<div class="rail-card">
|
|
1521
|
-
<div class="rail-card-header"><span class="rail-card-title"><span>focused</span></span></div>
|
|
1522
|
-
<div class="rail-card-body" id="graph-focused">
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
</aside>
|
|
1531
|
-
</div>
|
|
1532
|
-
`;
|
|
1945
|
+
area.innerHTML =
|
|
1946
|
+
'<div class="graph-view">' +
|
|
1947
|
+
'<section class="graph-canvas">' +
|
|
1948
|
+
'<div class="graph-header">' +
|
|
1949
|
+
'<div class="graph-header-left">' +
|
|
1950
|
+
'<span class="gh-ico">' + ICON.graph + '</span>' +
|
|
1951
|
+
'<button class="tool-chip graph-mode active" data-mode="projects">projects</button>' +
|
|
1952
|
+
'<button class="tool-chip graph-mode" data-mode="sessions">sessions</button>' +
|
|
1953
|
+
'<span class="dim tab-nums" id="graph-stats"></span>' +
|
|
1954
|
+
'<span id="graph-project-filter" style="display:none;font-size:var(--fz-sm);color:var(--accent);margin-left:4px">' +
|
|
1955
|
+
'<span id="graph-project-name"></span>' +
|
|
1956
|
+
'<button class="ghost-btn" id="graph-clear-project" style="color:var(--muted);margin-left:4px;font-size:var(--fz-xs)" title="Show all projects">✕</button>' +
|
|
1957
|
+
'</span>' +
|
|
1958
|
+
'</div>' +
|
|
1959
|
+
'<div class="graph-header-right">' +
|
|
1960
|
+
'<button class="tool-chip graph-layout active" data-layout="force">force</button>' +
|
|
1961
|
+
'<button class="tool-chip graph-layout" data-layout="radial">radial</button>' +
|
|
1962
|
+
'<button class="tool-chip graph-layout" data-layout="time">time</button>' +
|
|
1963
|
+
'<button class="ghost-btn" id="graph-close">✕</button>' +
|
|
1964
|
+
'</div>' +
|
|
1965
|
+
'</div>' +
|
|
1966
|
+
'<div class="graph-svg-wrap" id="graph-svg-wrap"></div>' +
|
|
1967
|
+
'<div class="graph-legend" id="graph-legend"></div>' +
|
|
1968
|
+
'</section>' +
|
|
1969
|
+
'<aside class="graph-rail" id="graph-rail">' +
|
|
1970
|
+
'<div class="rail-card">' +
|
|
1971
|
+
'<div class="rail-card-header"><span class="rail-card-title"><span style="display:inline-flex">' + ICON.doc + '</span><span>focused</span></span></div>' +
|
|
1972
|
+
'<div class="rail-card-body" id="graph-focused"><div class="dim" style="font-size:var(--fz-xs)">Hover or click a node</div></div>' +
|
|
1973
|
+
'</div>' +
|
|
1974
|
+
'<div class="rail-card">' +
|
|
1975
|
+
'<div class="rail-card-header"><span class="rail-card-title"><span style="display:inline-flex">' + ICON.link + '</span><span>connections</span></span></div>' +
|
|
1976
|
+
'<div class="rail-card-body" id="graph-connections"></div>' +
|
|
1977
|
+
'</div>' +
|
|
1978
|
+
'</aside>' +
|
|
1979
|
+
'</div>';
|
|
1533
1980
|
|
|
1534
1981
|
updateGraphLegend();
|
|
1535
1982
|
updateGraphProjectFilter();
|
|
1536
1983
|
|
|
1537
1984
|
const closeBtn = $('graph-close');
|
|
1538
|
-
if (closeBtn) {
|
|
1539
|
-
|
|
1540
|
-
e.stopPropagation();
|
|
1541
|
-
_graphProject = null;
|
|
1542
|
-
setView('note');
|
|
1543
|
-
if (currentPath) loadNote(currentPath);
|
|
1544
|
-
else $('content-area').innerHTML = '<div style="padding:40px;color:var(--muted)">Select a note from the sidebar</div>';
|
|
1545
|
-
});
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
// Clear project filter
|
|
1549
|
-
$('graph-clear-project')?.addEventListener('click', () => {
|
|
1985
|
+
if (closeBtn) closeBtn.addEventListener('click', (e) => {
|
|
1986
|
+
e.stopPropagation();
|
|
1550
1987
|
_graphProject = null;
|
|
1551
|
-
|
|
1552
|
-
|
|
1988
|
+
if (currentPath) { setView('note'); loadNote(currentPath); }
|
|
1989
|
+
else { setView('dashboard'); renderDashboard(); }
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
const clearBtn = $('graph-clear-project');
|
|
1993
|
+
if (clearBtn) clearBtn.addEventListener('click', () => {
|
|
1994
|
+
_graphProject = null; _graphData = null; updateGraphProjectFilter();
|
|
1553
1995
|
const activeLayout = document.querySelector('.graph-layout.active');
|
|
1554
1996
|
buildFullGraph(activeLayout ? activeLayout.dataset.layout : 'force');
|
|
1555
1997
|
});
|
|
1556
1998
|
|
|
1557
|
-
// Mode toggle (projects / sessions)
|
|
1558
1999
|
document.querySelectorAll('.graph-mode').forEach(btn => {
|
|
1559
2000
|
btn.addEventListener('click', () => {
|
|
1560
2001
|
document.querySelectorAll('.graph-mode').forEach(b => b.classList.remove('active'));
|
|
1561
2002
|
btn.classList.add('active');
|
|
1562
|
-
_graphMode = btn.dataset.mode;
|
|
1563
|
-
_graphData = null;
|
|
2003
|
+
_graphMode = btn.dataset.mode; _graphData = null;
|
|
1564
2004
|
updateGraphLegend();
|
|
1565
2005
|
const activeLayout = document.querySelector('.graph-layout.active');
|
|
1566
2006
|
buildFullGraph(activeLayout ? activeLayout.dataset.layout : 'force');
|
|
1567
2007
|
});
|
|
1568
2008
|
});
|
|
1569
2009
|
|
|
1570
|
-
// Layout toggle (force / radial / time)
|
|
1571
2010
|
document.querySelectorAll('.graph-layout').forEach(chip => {
|
|
1572
2011
|
chip.addEventListener('click', () => {
|
|
1573
2012
|
document.querySelectorAll('.graph-layout').forEach(c => c.classList.remove('active'));
|
|
@@ -1582,29 +2021,23 @@ function renderGraphView() {
|
|
|
1582
2021
|
function updateGraphProjectFilter() {
|
|
1583
2022
|
const el = $('graph-project-filter');
|
|
1584
2023
|
if (!el) return;
|
|
1585
|
-
if (_graphProject) {
|
|
1586
|
-
|
|
1587
|
-
el.style.display = 'inline';
|
|
1588
|
-
} else {
|
|
1589
|
-
el.style.display = 'none';
|
|
1590
|
-
}
|
|
2024
|
+
if (_graphProject) { $('graph-project-name').textContent = '› ' + _graphProject; el.style.display = 'inline'; }
|
|
2025
|
+
else el.style.display = 'none';
|
|
1591
2026
|
}
|
|
1592
2027
|
|
|
1593
2028
|
function updateGraphLegend() {
|
|
1594
2029
|
const legend = $('graph-legend');
|
|
1595
2030
|
if (!legend) return;
|
|
1596
2031
|
if (_graphMode === 'sessions') {
|
|
1597
|
-
legend.innerHTML =
|
|
1598
|
-
<div class="graph-legend-title">sessions</div>
|
|
1599
|
-
<div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background
|
|
1600
|
-
<div class="graph-legend-item"><i style="width:12px;height:0;border-top:1px dashed
|
|
1601
|
-
`;
|
|
2032
|
+
legend.innerHTML =
|
|
2033
|
+
'<div class="graph-legend-title">sessions</div>' +
|
|
2034
|
+
'<div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:#0b0b0e;border:1px solid #5eead4"></i>session</div>' +
|
|
2035
|
+
'<div class="graph-legend-item"><i style="width:12px;height:0;border-top:1px dashed rgba(255,255,255,0.3)"></i>temporal link</div>';
|
|
1602
2036
|
} else {
|
|
1603
|
-
legend.innerHTML =
|
|
1604
|
-
<div class="graph-legend-title">projects</div>
|
|
1605
|
-
<div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background
|
|
1606
|
-
<div class="graph-legend-item"><i style="width:12px;height:1px;background:
|
|
1607
|
-
`;
|
|
2037
|
+
legend.innerHTML =
|
|
2038
|
+
'<div class="graph-legend-title">projects</div>' +
|
|
2039
|
+
'<div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:#8b7ff5"></i>notes</div>' +
|
|
2040
|
+
'<div class="graph-legend-item"><i style="width:12px;height:1px;background:rgba(255,255,255,0.3)"></i>wikilink</div>';
|
|
1608
2041
|
}
|
|
1609
2042
|
}
|
|
1610
2043
|
|
|
@@ -1614,74 +2047,52 @@ async function buildFullGraph(layout) {
|
|
|
1614
2047
|
wrap.innerHTML = '';
|
|
1615
2048
|
|
|
1616
2049
|
if (!_graphData) {
|
|
1617
|
-
let url =
|
|
1618
|
-
if (_graphProject) url +=
|
|
2050
|
+
let url = '/api/graph?kind=' + _graphMode;
|
|
2051
|
+
if (_graphProject) url += '&project=' + encodeURIComponent(_graphProject);
|
|
1619
2052
|
_graphData = await fetchJSON(url);
|
|
1620
2053
|
}
|
|
1621
2054
|
|
|
1622
|
-
let nodes = _graphData.nodes.map(n => ({
|
|
1623
|
-
let links = _graphData.edges.map(e => ({
|
|
2055
|
+
let nodes = _graphData.nodes.map(n => Object.assign({}, n));
|
|
2056
|
+
let links = _graphData.edges.map(e => Object.assign({}, e));
|
|
1624
2057
|
|
|
1625
|
-
// For sessions mode, create temporal links between consecutive sessions
|
|
1626
2058
|
if (_graphMode === 'sessions' && links.length === 0 && nodes.length > 1) {
|
|
1627
2059
|
const sorted = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
|
|
1628
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
1629
|
-
links.push({ source: sorted[i - 1].id, target: sorted[i].id });
|
|
1630
|
-
}
|
|
2060
|
+
for (let i = 1; i < sorted.length; i++) links.push({ source: sorted[i - 1].id, target: sorted[i].id });
|
|
1631
2061
|
}
|
|
1632
2062
|
|
|
1633
2063
|
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
1634
|
-
|
|
1635
|
-
$('graph-stats').textContent = `${nodes.length}n · ${links.length}e`;
|
|
2064
|
+
$('graph-stats').textContent = nodes.length + 'n · ' + links.length + 'e';
|
|
1636
2065
|
if (nodes.length === 0) {
|
|
1637
2066
|
wrap.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:var(--fz-sm)">No nodes to display</div>';
|
|
1638
2067
|
return;
|
|
1639
2068
|
}
|
|
1640
2069
|
|
|
1641
2070
|
const rect = wrap.getBoundingClientRect();
|
|
1642
|
-
const W = rect.width || 800;
|
|
1643
|
-
const H = rect.height || 500;
|
|
2071
|
+
const W = rect.width || 800, H = rect.height || 500;
|
|
1644
2072
|
|
|
1645
|
-
const svg = d3.select(wrap).append('svg')
|
|
1646
|
-
.attr('viewBox', `0 0 ${W} ${H}`)
|
|
1647
|
-
.attr('preserveAspectRatio', 'xMidYMid meet');
|
|
2073
|
+
const svg = d3.select(wrap).append('svg').attr('viewBox', '0 0 ' + W + ' ' + H).attr('preserveAspectRatio', 'xMidYMid meet');
|
|
1648
2074
|
|
|
1649
2075
|
const defs = svg.append('defs');
|
|
1650
|
-
const pattern = defs.append('pattern').attr('id', 'dotgrid').attr('width',
|
|
1651
|
-
pattern.append('circle').attr('cx',
|
|
2076
|
+
const pattern = defs.append('pattern').attr('id', 'dotgrid').attr('width', 22).attr('height', 22).attr('patternUnits', 'userSpaceOnUse');
|
|
2077
|
+
pattern.append('circle').attr('cx', 11).attr('cy', 11).attr('r', 0.8).attr('fill', 'rgba(255,255,255,0.06)');
|
|
1652
2078
|
svg.append('rect').attr('width', W).attr('height', H).attr('fill', 'url(#dotgrid)');
|
|
1653
2079
|
|
|
1654
2080
|
const g = svg.append('g');
|
|
1655
2081
|
const zoom = d3.zoom().scaleExtent([0.3, 5]).on('zoom', e => g.attr('transform', e.transform));
|
|
1656
2082
|
svg.call(zoom);
|
|
1657
2083
|
|
|
1658
|
-
// Compute positions based on layout
|
|
1659
2084
|
const noteNodes = nodes.filter(n => n.kind === 'note');
|
|
1660
2085
|
const sessionNodes = nodes.filter(n => n.kind === 'session');
|
|
1661
2086
|
const cx = W / 2, cy = H / 2;
|
|
1662
2087
|
|
|
1663
2088
|
if (layout === 'radial') {
|
|
1664
|
-
const noteR = Math.min(W, H) * 0.25;
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
const a = (2 * Math.PI * i) / Math.max(noteNodes.length, 1) - Math.PI / 2;
|
|
1668
|
-
n.x = cx + noteR * Math.cos(a);
|
|
1669
|
-
n.y = cy + noteR * Math.sin(a);
|
|
1670
|
-
});
|
|
1671
|
-
sessionNodes.forEach((n, i) => {
|
|
1672
|
-
const a = (2 * Math.PI * i) / Math.max(sessionNodes.length, 1) - Math.PI / 2;
|
|
1673
|
-
n.x = cx + sessionR * Math.cos(a);
|
|
1674
|
-
n.y = cy + sessionR * Math.sin(a);
|
|
1675
|
-
});
|
|
2089
|
+
const noteR = Math.min(W, H) * 0.25, sessionR = Math.min(W, H) * 0.42;
|
|
2090
|
+
noteNodes.forEach((n, i) => { const a = (2 * Math.PI * i) / Math.max(noteNodes.length, 1) - Math.PI / 2; n.x = cx + noteR * Math.cos(a); n.y = cy + noteR * Math.sin(a); });
|
|
2091
|
+
sessionNodes.forEach((n, i) => { const a = (2 * Math.PI * i) / Math.max(sessionNodes.length, 1) - Math.PI / 2; n.x = cx + sessionR * Math.cos(a); n.y = cy + sessionR * Math.sin(a); });
|
|
1676
2092
|
} else if (layout === 'time') {
|
|
1677
2093
|
const sorted = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
|
|
1678
|
-
const pad = 80;
|
|
1679
|
-
const
|
|
1680
|
-
sorted.forEach((n, i) => {
|
|
1681
|
-
const src = nodes.find(x => x.id === n.id);
|
|
1682
|
-
src.x = pad + (usableW * i) / Math.max(sorted.length - 1, 1);
|
|
1683
|
-
src.y = n.kind === 'note' ? cy - 60 : cy + 60;
|
|
1684
|
-
});
|
|
2094
|
+
const pad = 80, usableW = W - pad * 2;
|
|
2095
|
+
sorted.forEach((n, i) => { const src = nodes.find(x => x.id === n.id); src.x = pad + (usableW * i) / Math.max(sorted.length - 1, 1); src.y = n.kind === 'note' ? cy - 60 : cy + 60; });
|
|
1685
2096
|
} else {
|
|
1686
2097
|
const sim = d3.forceSimulation(nodes)
|
|
1687
2098
|
.force('link', d3.forceLink(links).id(d => d.id).distance(80).strength(0.6))
|
|
@@ -1694,62 +2105,42 @@ async function buildFullGraph(layout) {
|
|
|
1694
2105
|
sim.stop();
|
|
1695
2106
|
}
|
|
1696
2107
|
|
|
1697
|
-
nodes.forEach(d => {
|
|
1698
|
-
d.x = Math.max(40, Math.min(W - 40, d.x || cx));
|
|
1699
|
-
d.y = Math.max(40, Math.min(H - 40, d.y || cy));
|
|
1700
|
-
});
|
|
2108
|
+
nodes.forEach(d => { d.x = Math.max(40, Math.min(W - 40, d.x || cx)); d.y = Math.max(40, Math.min(H - 40, d.y || cy)); });
|
|
1701
2109
|
|
|
1702
|
-
// Draw edges
|
|
1703
2110
|
const edgeGroup = g.append('g');
|
|
1704
2111
|
links.forEach(l => {
|
|
1705
2112
|
const s = typeof l.source === 'object' ? l.source : nodeMap.get(l.source);
|
|
1706
2113
|
const t = typeof l.target === 'object' ? l.target : nodeMap.get(l.target);
|
|
1707
2114
|
if (!s || !t) return;
|
|
1708
2115
|
const isTemporal = _graphMode === 'sessions';
|
|
1709
|
-
edgeGroup.append('line')
|
|
1710
|
-
.attr('
|
|
1711
|
-
.attr('stroke
|
|
1712
|
-
.attr('stroke-
|
|
1713
|
-
.attr('stroke-dasharray', isTemporal ? '4 3' : '')
|
|
1714
|
-
.attr('stroke-linecap', 'round');
|
|
2116
|
+
edgeGroup.append('line').attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
|
|
2117
|
+
.attr('stroke', isTemporal ? '#5eead4' : '#8b7ff5').attr('stroke-width', isTemporal ? 1 : 1.2)
|
|
2118
|
+
.attr('stroke-opacity', isTemporal ? 0.25 : 0.4)
|
|
2119
|
+
.attr('stroke-dasharray', isTemporal ? '4 3' : '').attr('stroke-linecap', 'round');
|
|
1715
2120
|
});
|
|
1716
2121
|
|
|
1717
|
-
// Draw nodes
|
|
1718
2122
|
const nodeGroup = g.append('g');
|
|
1719
2123
|
nodes.forEach(n => {
|
|
1720
2124
|
const isPrimary = _graphMode === 'sessions' ? n.kind === 'session' : n.kind === 'note';
|
|
2125
|
+
const accentCol = _graphMode === 'sessions' ? '#5eead4' : '#8b7ff5';
|
|
1721
2126
|
const r = isPrimary ? 10 : 6;
|
|
1722
2127
|
const ng = nodeGroup.append('g').style('cursor', 'pointer');
|
|
1723
|
-
|
|
1724
2128
|
if (isPrimary) {
|
|
1725
|
-
ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r +
|
|
1726
|
-
|
|
1727
|
-
ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
|
|
1728
|
-
.attr('fill', 'var(--panel-2)').attr('stroke', 'var(--accent)').attr('stroke-width', 1.5);
|
|
2129
|
+
ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r + 5).attr('fill', accentCol).attr('fill-opacity', 0.15);
|
|
2130
|
+
ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', 'rgba(255,255,255,0.06)').attr('stroke', accentCol).attr('stroke-width', 1.6);
|
|
1729
2131
|
} else {
|
|
1730
|
-
ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
|
|
1731
|
-
.attr('fill', 'var(--bg-2)').attr('stroke', 'var(--muted)').attr('stroke-width', 1.2);
|
|
2132
|
+
ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', 'rgba(255,255,255,0.04)').attr('stroke', 'var(--muted)').attr('stroke-width', 1.2);
|
|
1732
2133
|
}
|
|
1733
|
-
|
|
1734
2134
|
let label = n.title;
|
|
1735
2135
|
if (_graphMode === 'sessions' && n.kind === 'session') {
|
|
1736
2136
|
const m = n.id.match(/(\d{4}-\d{2}-\d{2})_(\d{2})(\d{2})/);
|
|
1737
|
-
label = m ?
|
|
2137
|
+
label = m ? m[1] + ' ' + m[2] + ':' + m[3] : n.title;
|
|
1738
2138
|
}
|
|
1739
2139
|
if (label.length > 22) label = label.substring(0, 20) + '…';
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
.
|
|
1743
|
-
|
|
1744
|
-
.style('font-family', 'JetBrains Mono, monospace')
|
|
1745
|
-
.text(label);
|
|
1746
|
-
|
|
1747
|
-
ng.on('click', () => {
|
|
1748
|
-
updateGraphRail(n, links, nodes);
|
|
1749
|
-
if (isSessionPath(n.id)) { openSession(n.id); }
|
|
1750
|
-
else { setView('note'); loadNote(n.id); }
|
|
1751
|
-
});
|
|
1752
|
-
|
|
2140
|
+
ng.append('text').attr('x', n.x).attr('y', n.y + r + 15).attr('text-anchor', 'middle').attr('font-size', 11)
|
|
2141
|
+
.attr('fill', isPrimary ? '#a4a4b0' : '#6c6c78').attr('opacity', 0.95)
|
|
2142
|
+
.style('font-family', 'JetBrains Mono, monospace').text(label);
|
|
2143
|
+
ng.on('click', () => { updateGraphRail(n, links, nodes); if (isSessionPath(n.id)) openSession(n.id); else { setView('note'); loadNote(n.id); } });
|
|
1753
2144
|
ng.on('mouseenter', () => updateGraphRail(n, links, nodes));
|
|
1754
2145
|
});
|
|
1755
2146
|
}
|
|
@@ -1758,7 +2149,6 @@ function updateGraphRail(node, links, nodes) {
|
|
|
1758
2149
|
const focused = $('graph-focused');
|
|
1759
2150
|
const conns = $('graph-connections');
|
|
1760
2151
|
if (!focused || !conns) return;
|
|
1761
|
-
|
|
1762
2152
|
const neighbors = new Set();
|
|
1763
2153
|
links.forEach(l => {
|
|
1764
2154
|
const sId = typeof l.source === 'object' ? l.source.id : l.source;
|
|
@@ -1766,51 +2156,39 @@ function updateGraphRail(node, links, nodes) {
|
|
|
1766
2156
|
if (sId === node.id) neighbors.add(tId);
|
|
1767
2157
|
if (tId === node.id) neighbors.add(sId);
|
|
1768
2158
|
});
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
<div style="
|
|
1773
|
-
<i style="width:9px;height:9px;border-radius:999px;background:${dotColor}"></i>
|
|
1774
|
-
<span style="font-weight:500">${node.title}</span>
|
|
1775
|
-
</div>
|
|
1776
|
-
<div class="dim" style="font-size:var(--fz-xs);margin-top:6px">degree ${neighbors.size} · ${node.kind}</div>
|
|
1777
|
-
`;
|
|
1778
|
-
|
|
2159
|
+
const dotColor = node.kind === 'session' ? '#5eead4' : '#8b7ff5';
|
|
2160
|
+
focused.innerHTML =
|
|
2161
|
+
'<div style="display:flex;align-items:center;gap:8px"><i style="width:9px;height:9px;border-radius:999px;background:' + dotColor + '"></i><span style="font-weight:600">' + esc(node.title) + '</span></div>' +
|
|
2162
|
+
'<div class="dim" style="font-size:var(--fz-xs);margin-top:6px">degree ' + neighbors.size + ' · ' + node.kind + '</div>';
|
|
1779
2163
|
conns.innerHTML = '';
|
|
1780
2164
|
[...neighbors].forEach(id => {
|
|
1781
2165
|
const n = nodes.find(x => x.id === id);
|
|
1782
2166
|
if (!n) return;
|
|
1783
2167
|
const btn = document.createElement('button');
|
|
1784
2168
|
btn.className = 'conn-item';
|
|
1785
|
-
const kindColor = n.kind === 'session' ? '
|
|
1786
|
-
btn.innerHTML =
|
|
1787
|
-
btn.addEventListener('click', () => {
|
|
1788
|
-
if (isSessionPath(n.id)) openSession(n.id);
|
|
1789
|
-
else { setView('note'); loadNote(n.id); }
|
|
1790
|
-
});
|
|
2169
|
+
const kindColor = n.kind === 'session' ? '#5eead4' : '#8b7ff5';
|
|
2170
|
+
btn.innerHTML = '<i style="width:6px;height:6px;border-radius:999px;background:' + kindColor + ';flex-shrink:0"></i><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(n.title) + '</span><span class="dim" style="font-size:var(--fz-xs);flex-shrink:0">' + n.kind + '</span>';
|
|
2171
|
+
btn.addEventListener('click', () => { if (isSessionPath(n.id)) openSession(n.id); else { setView('note'); loadNote(n.id); } });
|
|
1791
2172
|
conns.appendChild(btn);
|
|
1792
2173
|
});
|
|
1793
2174
|
}
|
|
1794
2175
|
|
|
1795
2176
|
// ─── Editor ──────────────────────────────────────────────────────────────────
|
|
1796
2177
|
async function openEditor(path) {
|
|
1797
|
-
const note = await fetchJSON(
|
|
2178
|
+
const note = await fetchJSON('/api/note/' + path);
|
|
1798
2179
|
if (note.error) return;
|
|
1799
|
-
editingPath = path;
|
|
1800
|
-
editingNote = note;
|
|
2180
|
+
editingPath = path; editingNote = note;
|
|
1801
2181
|
$('edit-path').textContent = path;
|
|
1802
2182
|
$('edit-textarea').value = note.content || '';
|
|
1803
2183
|
$('edit-overlay').classList.add('active');
|
|
1804
2184
|
$('edit-textarea').focus();
|
|
1805
2185
|
}
|
|
1806
|
-
|
|
1807
2186
|
function closeEditor() { $('edit-overlay').classList.remove('active'); editingPath = null; }
|
|
1808
|
-
|
|
1809
2187
|
$('edit-cancel').addEventListener('click', closeEditor);
|
|
1810
2188
|
$('edit-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeEditor(); });
|
|
1811
2189
|
$('edit-save').addEventListener('click', async () => {
|
|
1812
2190
|
if (!editingPath) return;
|
|
1813
|
-
await fetch(
|
|
2191
|
+
await fetch('/api/note/' + editingPath, {
|
|
1814
2192
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1815
2193
|
body: JSON.stringify({ content: $('edit-textarea').value, tags: editingNote.tags || [], properties: editingNote.properties || {} }),
|
|
1816
2194
|
});
|
|
@@ -1823,19 +2201,13 @@ function openSessionCreate(defaultProject) {
|
|
|
1823
2201
|
const datalist = $('sc-project-list');
|
|
1824
2202
|
datalist.innerHTML = '';
|
|
1825
2203
|
const projects = new Set();
|
|
1826
|
-
for (const path of Object.keys(allNotes)) {
|
|
1827
|
-
|
|
1828
|
-
if (parts.length > 1) projects.add(parts[0]);
|
|
1829
|
-
}
|
|
1830
|
-
for (const p of [...projects].sort()) {
|
|
1831
|
-
const opt = document.createElement('option'); opt.value = p; datalist.appendChild(opt);
|
|
1832
|
-
}
|
|
2204
|
+
for (const path of Object.keys(allNotes)) { const parts = path.split('/'); if (parts.length > 1) projects.add(parts[0]); }
|
|
2205
|
+
for (const p of [...projects].sort()) { const opt = document.createElement('option'); opt.value = p; datalist.appendChild(opt); }
|
|
1833
2206
|
$('sc-project').value = defaultProject || '';
|
|
1834
2207
|
$('sc-summary').value = '';
|
|
1835
2208
|
$('session-create-overlay').classList.add('active');
|
|
1836
2209
|
(defaultProject ? $('sc-summary') : $('sc-project')).focus();
|
|
1837
2210
|
}
|
|
1838
|
-
|
|
1839
2211
|
function closeSessionCreate() { $('session-create-overlay').classList.remove('active'); }
|
|
1840
2212
|
$('sc-cancel').addEventListener('click', closeSessionCreate);
|
|
1841
2213
|
$('session-create-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeSessionCreate(); });
|
|
@@ -1845,7 +2217,7 @@ $('sc-create').addEventListener('click', async () => {
|
|
|
1845
2217
|
if (!project) { $('sc-project').focus(); return; }
|
|
1846
2218
|
const resp = await fetch('/api/sessions/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project, summary }) });
|
|
1847
2219
|
const result = await resp.json();
|
|
1848
|
-
if (result.ok) { closeSessionCreate(); await refreshTree();
|
|
2220
|
+
if (result.ok) { closeSessionCreate(); await refreshTree(); openSession(result.path); }
|
|
1849
2221
|
});
|
|
1850
2222
|
|
|
1851
2223
|
// ─── Project Create ──────────────────────────────────────────────────────────
|
|
@@ -1864,28 +2236,24 @@ $('pc-create').addEventListener('click', async () => {
|
|
|
1864
2236
|
if (!name) { $('pc-name').focus(); return; }
|
|
1865
2237
|
const resp = await fetch('/api/projects/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, overview }) });
|
|
1866
2238
|
const result = await resp.json();
|
|
1867
|
-
if (result.ok) { closeProjectCreate(); await refreshTree(); loadNote(result.path); }
|
|
2239
|
+
if (result.ok) { closeProjectCreate(); await refreshTree(); setView('note'); loadNote(result.path); }
|
|
1868
2240
|
});
|
|
1869
2241
|
|
|
1870
2242
|
// ─── Quick Switcher ──────────────────────────────────────────────────────────
|
|
1871
2243
|
function openQuickSwitcher() {
|
|
1872
2244
|
$('qs-overlay').classList.add('active');
|
|
1873
|
-
$('qs-input').value = '';
|
|
1874
|
-
|
|
1875
|
-
qsSelectedIndex = 0;
|
|
1876
|
-
renderQsResults('');
|
|
2245
|
+
$('qs-input').value = ''; $('qs-input').focus();
|
|
2246
|
+
qsSelectedIndex = 0; renderQsResults('');
|
|
1877
2247
|
}
|
|
1878
2248
|
function closeQuickSwitcher() { $('qs-overlay').classList.remove('active'); }
|
|
1879
2249
|
$('qs-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeQuickSwitcher(); });
|
|
1880
|
-
|
|
1881
2250
|
$('qs-input').addEventListener('input', e => { qsSelectedIndex = 0; renderQsResults(e.target.value.trim().toLowerCase()); });
|
|
1882
2251
|
$('qs-input').addEventListener('keydown', e => {
|
|
1883
2252
|
const items = document.querySelectorAll('.qs-item');
|
|
1884
2253
|
if (e.key === 'ArrowDown') { e.preventDefault(); qsSelectedIndex = Math.min(qsSelectedIndex + 1, items.length - 1); updateQsSelection(); }
|
|
1885
2254
|
else if (e.key === 'ArrowUp') { e.preventDefault(); qsSelectedIndex = Math.max(qsSelectedIndex - 1, 0); updateQsSelection(); }
|
|
1886
|
-
else if (e.key === 'Enter') { e.preventDefault(); const sel = items[qsSelectedIndex]; if (sel) { loadNote(sel.dataset.path); closeQuickSwitcher(); } }
|
|
2255
|
+
else if (e.key === 'Enter') { e.preventDefault(); const sel = items[qsSelectedIndex]; if (sel) { setView('note'); loadNote(sel.dataset.path); closeQuickSwitcher(); } }
|
|
1887
2256
|
});
|
|
1888
|
-
|
|
1889
2257
|
function renderQsResults(query) {
|
|
1890
2258
|
const container = $('qs-results');
|
|
1891
2259
|
let entries = Object.entries(allNotes);
|
|
@@ -1895,27 +2263,24 @@ function renderQsResults(query) {
|
|
|
1895
2263
|
if (entries.length === 0) { container.innerHTML = '<div class="qs-empty">No notes found</div>'; return; }
|
|
1896
2264
|
container.innerHTML = entries.map(([path, note], i) => {
|
|
1897
2265
|
const folder = path.includes('/') ? path.split('/').slice(0, -1).join('/') : '';
|
|
1898
|
-
return
|
|
2266
|
+
return '<div class="qs-item' + (i === qsSelectedIndex ? ' selected' : '') + '" data-path="' + esc(path) + '"><span class="qs-icon">' + ICON.note + '</span><span class="qs-name">' + esc(note.title) + '</span>' + (folder ? '<span class="qs-path">' + esc(folder) + '</span>' : '') + '</div>';
|
|
1899
2267
|
}).join('');
|
|
1900
2268
|
container.querySelectorAll('.qs-item').forEach((el, i) => {
|
|
1901
|
-
el.addEventListener('click', () => { loadNote(el.dataset.path); closeQuickSwitcher(); });
|
|
2269
|
+
el.addEventListener('click', () => { setView('note'); loadNote(el.dataset.path); closeQuickSwitcher(); });
|
|
1902
2270
|
el.addEventListener('mouseenter', () => { qsSelectedIndex = i; updateQsSelection(); });
|
|
1903
2271
|
});
|
|
1904
2272
|
}
|
|
1905
|
-
function updateQsSelection() {
|
|
1906
|
-
document.querySelectorAll('.qs-item').forEach((el, i) => el.classList.toggle('selected', i === qsSelectedIndex));
|
|
1907
|
-
}
|
|
2273
|
+
function updateQsSelection() { document.querySelectorAll('.qs-item').forEach((el, i) => el.classList.toggle('selected', i === qsSelectedIndex)); }
|
|
1908
2274
|
|
|
1909
2275
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
1910
2276
|
const searchInput = $('search-input');
|
|
1911
2277
|
const searchDropdown = $('search-dropdown');
|
|
1912
|
-
|
|
1913
2278
|
searchInput.addEventListener('input', () => {
|
|
1914
2279
|
clearTimeout(searchTimeout);
|
|
1915
2280
|
const q = searchInput.value.trim();
|
|
1916
2281
|
if (!q) { searchDropdown.classList.remove('active'); return; }
|
|
1917
2282
|
searchTimeout = setTimeout(async () => {
|
|
1918
|
-
const results = await fetchJSON(
|
|
2283
|
+
const results = await fetchJSON('/api/search?q=' + encodeURIComponent(q));
|
|
1919
2284
|
searchDropdown.innerHTML = '';
|
|
1920
2285
|
if (results.length === 0) {
|
|
1921
2286
|
searchDropdown.innerHTML = '<div class="search-result"><div class="sr-title" style="color:var(--dim)">No results</div></div>';
|
|
@@ -1923,17 +2288,14 @@ searchInput.addEventListener('input', () => {
|
|
|
1923
2288
|
results.forEach(r => {
|
|
1924
2289
|
const div = document.createElement('div');
|
|
1925
2290
|
div.className = 'search-result';
|
|
1926
|
-
div.innerHTML =
|
|
1927
|
-
div.addEventListener('click', () => { loadNote(r.path); searchDropdown.classList.remove('active'); searchInput.value = ''; });
|
|
2291
|
+
div.innerHTML = '<div class="sr-title">' + esc(r.title) + '</div><div class="sr-path">' + esc(r.path) + '</div>' + (r.snippet ? '<div class="sr-snippet">' + esc(r.snippet.substring(0, 120)) + '</div>' : '');
|
|
2292
|
+
div.addEventListener('click', () => { setView('note'); loadNote(r.path); searchDropdown.classList.remove('active'); searchInput.value = ''; });
|
|
1928
2293
|
searchDropdown.appendChild(div);
|
|
1929
2294
|
});
|
|
1930
2295
|
}
|
|
1931
2296
|
searchDropdown.classList.add('active');
|
|
1932
2297
|
}, 200);
|
|
1933
2298
|
});
|
|
1934
|
-
|
|
1935
|
-
searchInput.addEventListener('focus', () => { $('search-bar').querySelector('.s-caret').style.display = 'none'; $('search-bar').querySelector('.s-hint').style.display = 'none'; });
|
|
1936
|
-
searchInput.addEventListener('blur', () => { if (!searchInput.value) { $('search-bar').querySelector('.s-caret').style.display = ''; $('search-bar').querySelector('.s-hint').style.display = ''; } });
|
|
1937
2299
|
document.addEventListener('click', e => { if (!e.target.closest('.search-bar')) searchDropdown.classList.remove('active'); });
|
|
1938
2300
|
|
|
1939
2301
|
// ─── Keyboard Shortcuts ──────────────────────────────────────────────────────
|
|
@@ -1945,12 +2307,6 @@ document.addEventListener('keydown', e => {
|
|
|
1945
2307
|
});
|
|
1946
2308
|
|
|
1947
2309
|
// ─── Refresh & Polling ───────────────────────────────────────────────────────
|
|
1948
|
-
function fmtTokens(n) {
|
|
1949
|
-
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
1950
|
-
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
1951
|
-
return String(n);
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
2310
|
async function loadTokenEconomics() {
|
|
1955
2311
|
try {
|
|
1956
2312
|
const te = await fetchJSON('/api/token-economics');
|
|
@@ -1959,13 +2315,11 @@ async function loadTokenEconomics() {
|
|
|
1959
2315
|
$('te-inject').textContent = te.latest_injection_tokens > 0 ? fmtTokens(te.latest_injection_tokens) + 't' : '—';
|
|
1960
2316
|
if (te.compression_ratio > 0) {
|
|
1961
2317
|
$('te-savings').textContent = te.compression_ratio + 'x';
|
|
1962
|
-
$('te-savings').parentElement.title =
|
|
1963
|
-
} else {
|
|
1964
|
-
$('te-savings').textContent = '—';
|
|
1965
|
-
}
|
|
2318
|
+
$('te-savings').parentElement.title = te.per_session_savings_pct + '% per session — injection is ' + te.compression_ratio + 'x smaller than avg exploration (' + fmtTokens(te.avg_exploration_per_session) + 't)';
|
|
2319
|
+
} else { $('te-savings').textContent = '—'; }
|
|
1966
2320
|
$('token-economics').style.display = '';
|
|
1967
2321
|
}
|
|
1968
|
-
} catch {}
|
|
2322
|
+
} catch (e) {}
|
|
1969
2323
|
}
|
|
1970
2324
|
|
|
1971
2325
|
async function refreshTree() {
|
|
@@ -1973,6 +2327,7 @@ async function refreshTree() {
|
|
|
1973
2327
|
fetchJSON('/api/tree'), fetchJSON('/api/sessions'), fetchJSON('/api/stats'),
|
|
1974
2328
|
]);
|
|
1975
2329
|
treeData = rawTree;
|
|
2330
|
+
_sessionsData = sessionsData;
|
|
1976
2331
|
allNotes = {};
|
|
1977
2332
|
function walk(node) {
|
|
1978
2333
|
if (node.type === 'note') allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
|
|
@@ -1989,13 +2344,14 @@ async function refreshTree() {
|
|
|
1989
2344
|
async function pollForChanges() {
|
|
1990
2345
|
try {
|
|
1991
2346
|
const stats = await fetchJSON('/api/stats');
|
|
1992
|
-
const key =
|
|
2347
|
+
const key = stats.notes + ':' + stats.links + ':' + stats.tags;
|
|
1993
2348
|
if (lastKnownStats && lastKnownStats !== key) {
|
|
1994
2349
|
await refreshTree();
|
|
1995
|
-
if (
|
|
2350
|
+
if (view === 'dashboard') renderDashboard();
|
|
2351
|
+
else if (currentPath) loadNote(currentPath);
|
|
1996
2352
|
}
|
|
1997
2353
|
lastKnownStats = key;
|
|
1998
|
-
} catch {}
|
|
2354
|
+
} catch (e) {}
|
|
1999
2355
|
}
|
|
2000
2356
|
|
|
2001
2357
|
// ─── Init ────────────────────────────────────────────────────────────────────
|
|
@@ -2008,30 +2364,25 @@ async function init() {
|
|
|
2008
2364
|
fetchJSON('/api/tree'), fetchJSON('/api/sessions'), fetchJSON('/api/stats'),
|
|
2009
2365
|
]);
|
|
2010
2366
|
treeData = rawTree;
|
|
2011
|
-
|
|
2367
|
+
_sessionsData = sessionsData;
|
|
2012
2368
|
function walk(node) {
|
|
2013
2369
|
if (node.type === 'note') allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
|
|
2014
2370
|
(node.children || []).forEach(walk);
|
|
2015
2371
|
}
|
|
2016
2372
|
walk(treeData);
|
|
2017
|
-
|
|
2018
2373
|
renderSidebar(sessionsData);
|
|
2019
2374
|
$('stat-notes').textContent = stats.notes;
|
|
2020
2375
|
$('stat-folders').textContent = stats.folders;
|
|
2021
2376
|
loadTokenEconomics();
|
|
2377
|
+
lastKnownStats = stats.notes + ':' + stats.links + ':' + stats.tags;
|
|
2022
2378
|
} catch (e) {
|
|
2023
2379
|
console.error('KYP-MEM init failed:', e);
|
|
2024
|
-
$('sidebar-scroll').innerHTML =
|
|
2380
|
+
$('sidebar-scroll').innerHTML = '<div style="padding:12px;color:#f87171;font-size:12px">Init error: ' + esc(e.message) + '<br>Check console for details</div>';
|
|
2025
2381
|
}
|
|
2026
2382
|
|
|
2027
|
-
//
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
<span class="logo" style="font-size:28px;opacity:0.6"><span class="logo-mark" style="width:12px;height:12px"></span>KYP·MEM</span>
|
|
2031
|
-
<span class="dim" style="font-size:var(--fz-sm);letter-spacing:3px;text-transform:uppercase">know your project memory</span>
|
|
2032
|
-
<span class="dim" style="font-size:var(--fz-sm);margin-top:24px">Select a note or press <span style="border:1px solid var(--line);border-radius:3px;padding:0 5px;color:var(--muted)">⌘O</span> to quick-switch</span>
|
|
2033
|
-
</div>
|
|
2034
|
-
`;
|
|
2383
|
+
// Default landing: dashboard
|
|
2384
|
+
setView('dashboard');
|
|
2385
|
+
renderDashboard();
|
|
2035
2386
|
}
|
|
2036
2387
|
|
|
2037
2388
|
init();
|