@hncode/hncode 0.3.6 → 0.4.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/package.json +4 -2
- package/src/agent-task.js +154 -84
- package/src/agent.js +533 -145
- package/src/clipboard.js +5 -2
- package/src/colors.js +242 -238
- package/src/config.js +217 -17
- package/src/file-history.js +177 -0
- package/src/hooks.js +11 -1
- package/src/i18n.js +389 -0
- package/src/index.js +46 -3
- package/src/llm.js +139 -16
- package/src/permissions.js +120 -0
- package/src/session-hub.js +407 -0
- package/src/session.js +31 -0
- package/src/term.js +19 -3
- package/src/tool-result.js +236 -0
- package/src/tools/agent.js +11 -0
- package/src/tools/bash.js +84 -17
- package/src/tools/edit.js +192 -32
- package/src/tools/file-lines.js +5 -0
- package/src/tools/index.js +4 -0
- package/src/tools/memory.js +88 -0
- package/src/tools/read-media-file.js +85 -68
- package/src/tools/read.js +15 -1
- package/src/tools/utils.js +14 -0
- package/src/tools/write.js +35 -5
- package/src/tui.js +11665 -9179
- package/src/web-auth.js +98 -0
- package/src/web-daemon-cli.js +63 -0
- package/src/web-daemon-client.js +203 -0
- package/src/web-daemon.js +871 -0
- package/src/web-public/app.css +1509 -0
- package/src/web-public/app.js +2913 -0
- package/src/web-public/complete.js +154 -0
- package/src/web-public/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/src/web-public/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-100-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-101-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-102-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-103-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-104-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-105-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-106-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-107-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-108-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-109-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-110-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-111-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-112-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-113-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-114-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-115-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-116-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-117-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-118-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-119-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-21-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-22-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-23-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-24-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-25-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-26-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-27-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-28-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-29-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-30-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-31-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-32-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-33-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-34-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-35-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-36-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-37-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-38-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-39-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-4-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-40-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-41-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-42-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-43-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-44-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-45-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-46-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-47-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-48-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-49-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-5-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-50-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-51-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-52-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-53-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-54-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-55-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-56-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-57-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-58-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-59-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-6-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-60-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-61-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-62-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-63-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-64-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-65-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-66-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-67-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-68-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-69-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-70-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-71-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-72-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-73-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-74-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-75-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-76-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-77-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-78-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-79-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-80-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-81-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-82-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-83-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-84-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-85-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-86-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-87-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-88-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-89-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-90-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-91-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-97-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-98-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-99-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc.css +775 -0
- package/src/web-public/fonts.css +31 -0
- package/src/web-public/home.css +222 -0
- package/src/web-public/home.html +33 -0
- package/src/web-public/home.js +160 -0
- package/src/web-public/i18n.js +983 -0
- package/src/web-public/index.html +196 -0
- package/src/web-public/login.css +123 -0
- package/src/web-public/login.html +25 -0
- package/src/web-public/login.js +46 -0
- package/src/web-public/settings.js +290 -0
- package/src/web-ws.js +226 -0
- package/src/web.js +380 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Self-hosted faces: JetBrains Mono for everything, Noto Sans SC for CJK.
|
|
2
|
+
|
|
3
|
+
This product is Chinese-first. The UI body, the side rail, the settings, the
|
|
4
|
+
status line and the transcript are all Chinese — and JetBrains Mono has NO CJK
|
|
5
|
+
glyphs, so without Noto every Chinese character falls through to the OS font
|
|
6
|
+
and the page stops looking like one designed thing. Order matters: the mono
|
|
7
|
+
face comes first so Latin text (paths, code, numbers) gets the designed face,
|
|
8
|
+
then Noto's unicode-range shards take over for CJK.
|
|
9
|
+
|
|
10
|
+
There is deliberately no separate sans stack. kimi-code's UI is English, so it
|
|
11
|
+
can pair a Latin UI face (Geist) with a mono code face; a Chinese UI would be
|
|
12
|
+
all-Noto for the CJK anyway, so a second Latin face only ever covers the few
|
|
13
|
+
latin glyphs, at the cost of a second font and a mismatched metric. One mono
|
|
14
|
+
face that falls back to Noto reads as one thing. */
|
|
15
|
+
@import './fonts/noto-sans-sc.css';
|
|
16
|
+
|
|
17
|
+
@font-face {
|
|
18
|
+
font-family: 'JetBrains Mono';
|
|
19
|
+
font-style: normal;
|
|
20
|
+
font-weight: 400;
|
|
21
|
+
font-display: swap;
|
|
22
|
+
src: url('./fonts/JetBrainsMono-Regular.woff2') format('woff2');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@font-face {
|
|
26
|
+
font-family: 'JetBrains Mono';
|
|
27
|
+
font-style: normal;
|
|
28
|
+
font-weight: 700;
|
|
29
|
+
font-display: swap;
|
|
30
|
+
src: url('./fonts/JetBrainsMono-Bold.woff2') format('woff2');
|
|
31
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/* hncode web UI — the session directory.
|
|
2
|
+
Same tokens as app.css and the same palette as the terminal (src/colors.js). */
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
color-scheme: dark;
|
|
6
|
+
|
|
7
|
+
--s0: #08090b;
|
|
8
|
+
--s1: #0d0f13;
|
|
9
|
+
--s2: #12151b;
|
|
10
|
+
--s3: #1a1e26;
|
|
11
|
+
|
|
12
|
+
--fg: #e6eaf2;
|
|
13
|
+
--fg1: #b6bfcf;
|
|
14
|
+
--fg2: #8f97a6;
|
|
15
|
+
--fg3: #6b7280;
|
|
16
|
+
|
|
17
|
+
--line: #1b202a;
|
|
18
|
+
--line2: #272d3a;
|
|
19
|
+
|
|
20
|
+
--teal: #00b8db;
|
|
21
|
+
--cyan: #00d7ff;
|
|
22
|
+
--red: #ff6b6b;
|
|
23
|
+
--green: #5cff8f;
|
|
24
|
+
|
|
25
|
+
--mono: "JetBrains Mono", "Noto Sans SC", ui-monospace, SFMono-Regular,
|
|
26
|
+
"SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
27
|
+
--sans: var(--mono);
|
|
28
|
+
|
|
29
|
+
--radius: 3px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
* { box-sizing: border-box; }
|
|
33
|
+
[hidden] { display: none !important; }
|
|
34
|
+
html, body { margin: 0; }
|
|
35
|
+
|
|
36
|
+
body {
|
|
37
|
+
background: var(--s0);
|
|
38
|
+
color: var(--fg);
|
|
39
|
+
font-family: var(--sans);
|
|
40
|
+
font-size: 13px;
|
|
41
|
+
line-height: 1.5;
|
|
42
|
+
-webkit-font-smoothing: antialiased;
|
|
43
|
+
min-height: 100%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
* { scrollbar-width: thin; scrollbar-color: var(--line2) transparent; }
|
|
47
|
+
*::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
48
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
49
|
+
*::-webkit-scrollbar-thumb {
|
|
50
|
+
background: var(--line2);
|
|
51
|
+
border: 3px solid transparent;
|
|
52
|
+
background-clip: content-box;
|
|
53
|
+
border-radius: 5px;
|
|
54
|
+
}
|
|
55
|
+
::selection { background: rgba(0, 184, 219, .28); }
|
|
56
|
+
:focus-visible { outline: 1px solid var(--teal); outline-offset: 2px; }
|
|
57
|
+
|
|
58
|
+
.page {
|
|
59
|
+
max-width: 980px;
|
|
60
|
+
margin: 0 auto;
|
|
61
|
+
padding: 40px 32px 80px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.head {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: flex-end;
|
|
67
|
+
justify-content: space-between;
|
|
68
|
+
gap: 24px;
|
|
69
|
+
padding-bottom: 16px;
|
|
70
|
+
border-bottom: 1px solid var(--line);
|
|
71
|
+
margin-bottom: 28px;
|
|
72
|
+
}
|
|
73
|
+
h1 {
|
|
74
|
+
font-family: var(--mono);
|
|
75
|
+
font-size: 20px;
|
|
76
|
+
font-weight: 500;
|
|
77
|
+
letter-spacing: -.01em;
|
|
78
|
+
margin: 0 0 8px;
|
|
79
|
+
color: var(--fg);
|
|
80
|
+
}
|
|
81
|
+
h1 .dim { color: var(--fg3); }
|
|
82
|
+
.lede { color: var(--fg2); margin: 0; max-width: 56ch; font-size: 12.5px; }
|
|
83
|
+
.head-right { display: flex; gap: 8px; flex-shrink: 0; }
|
|
84
|
+
|
|
85
|
+
.btn {
|
|
86
|
+
appearance: none;
|
|
87
|
+
font-family: var(--mono);
|
|
88
|
+
font-size: 11px;
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
padding: 4px 11px;
|
|
91
|
+
border: 1px solid var(--line2);
|
|
92
|
+
border-radius: var(--radius);
|
|
93
|
+
background: transparent;
|
|
94
|
+
color: var(--fg1);
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
white-space: nowrap;
|
|
97
|
+
transition: border-color .12s ease, color .12s ease;
|
|
98
|
+
}
|
|
99
|
+
.btn:hover { border-color: var(--teal); color: var(--fg); }
|
|
100
|
+
|
|
101
|
+
.err {
|
|
102
|
+
color: var(--red);
|
|
103
|
+
border: 1px solid rgba(255, 107, 107, .4);
|
|
104
|
+
border-radius: var(--radius);
|
|
105
|
+
padding: 9px 12px;
|
|
106
|
+
margin: 0 0 24px;
|
|
107
|
+
font-family: var(--mono);
|
|
108
|
+
font-size: 11.5px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.groups { display: flex; flex-direction: column; gap: 32px; }
|
|
112
|
+
|
|
113
|
+
.group-head {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: baseline;
|
|
116
|
+
gap: 10px;
|
|
117
|
+
padding-bottom: 7px;
|
|
118
|
+
border-bottom: 1px solid var(--line);
|
|
119
|
+
margin-bottom: 4px;
|
|
120
|
+
}
|
|
121
|
+
.ws {
|
|
122
|
+
font-family: var(--mono);
|
|
123
|
+
font-size: 11.5px;
|
|
124
|
+
color: var(--fg1);
|
|
125
|
+
word-break: break-all;
|
|
126
|
+
}
|
|
127
|
+
.count {
|
|
128
|
+
margin-left: auto;
|
|
129
|
+
font-family: var(--mono);
|
|
130
|
+
font-size: 10.5px;
|
|
131
|
+
color: var(--fg3);
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.rows { display: flex; flex-direction: column; }
|
|
136
|
+
|
|
137
|
+
.row {
|
|
138
|
+
display: grid;
|
|
139
|
+
grid-template-columns: minmax(0, 1fr) 110px 54px;
|
|
140
|
+
gap: 16px;
|
|
141
|
+
align-items: baseline;
|
|
142
|
+
padding: 9px 10px 9px 3px;
|
|
143
|
+
border-bottom: 1px solid var(--line);
|
|
144
|
+
border-left: 2px solid transparent;
|
|
145
|
+
text-decoration: none;
|
|
146
|
+
color: inherit;
|
|
147
|
+
transition: background-color .1s ease, border-color .1s ease;
|
|
148
|
+
}
|
|
149
|
+
.row:hover { background: var(--s1); border-left-color: var(--teal); }
|
|
150
|
+
.row:focus-visible { outline: 1px solid var(--teal); outline-offset: -2px; }
|
|
151
|
+
|
|
152
|
+
.row-title {
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
color: var(--fg);
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
text-overflow: ellipsis;
|
|
157
|
+
white-space: nowrap;
|
|
158
|
+
}
|
|
159
|
+
.row-id {
|
|
160
|
+
font-family: var(--mono);
|
|
161
|
+
font-size: 11px;
|
|
162
|
+
color: var(--fg3);
|
|
163
|
+
margin-left: 8px;
|
|
164
|
+
}
|
|
165
|
+
.row-when {
|
|
166
|
+
font-family: var(--mono);
|
|
167
|
+
font-size: 11px;
|
|
168
|
+
color: var(--fg3);
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
font-variant-numeric: tabular-nums;
|
|
171
|
+
}
|
|
172
|
+
.row-rounds {
|
|
173
|
+
font-family: var(--mono);
|
|
174
|
+
font-size: 11px;
|
|
175
|
+
color: var(--fg3);
|
|
176
|
+
text-align: right;
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
font-variant-numeric: tabular-nums;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.tag {
|
|
182
|
+
display: inline-block;
|
|
183
|
+
font-family: var(--mono);
|
|
184
|
+
font-size: 10px;
|
|
185
|
+
line-height: 15px;
|
|
186
|
+
padding: 0 5px;
|
|
187
|
+
border: 1px solid currentColor;
|
|
188
|
+
border-radius: 2px;
|
|
189
|
+
margin-left: 8px;
|
|
190
|
+
vertical-align: 1px;
|
|
191
|
+
}
|
|
192
|
+
.tag-live { color: var(--green); }
|
|
193
|
+
.tag-ro { color: var(--fg3); }
|
|
194
|
+
|
|
195
|
+
.empty {
|
|
196
|
+
color: var(--fg3);
|
|
197
|
+
padding: 32px 0;
|
|
198
|
+
font-family: var(--mono);
|
|
199
|
+
font-size: 12px;
|
|
200
|
+
}
|
|
201
|
+
@media (max-width: 720px) {
|
|
202
|
+
.page { padding: 28px 16px 60px; }
|
|
203
|
+
.head { flex-direction: column; align-items: flex-start; gap: 16px; }
|
|
204
|
+
.row { grid-template-columns: minmax(0, 1fr) auto; }
|
|
205
|
+
.row-rounds { display: none; }
|
|
206
|
+
}
|
|
207
|
+
/* Language switcher in the home header, mirroring the app's nav-lang. */
|
|
208
|
+
.nav-lang { display: flex; gap: 2px; flex: none; align-items: center; }
|
|
209
|
+
.nav-lang button {
|
|
210
|
+
appearance: none;
|
|
211
|
+
border: 0;
|
|
212
|
+
background: none;
|
|
213
|
+
color: var(--fg3);
|
|
214
|
+
font-family: var(--mono);
|
|
215
|
+
font-size: 11px;
|
|
216
|
+
padding: 1px 4px;
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
border-radius: 2px;
|
|
219
|
+
transition: color .12s ease, background-color .12s ease;
|
|
220
|
+
}
|
|
221
|
+
.nav-lang button:hover { color: var(--fg1); }
|
|
222
|
+
.nav-lang button.is-on { color: var(--teal); }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="robots" content="noindex, nofollow">
|
|
7
|
+
<title>hncode</title>
|
|
8
|
+
<link rel="stylesheet" href="fonts.css">
|
|
9
|
+
<link rel="stylesheet" href="/home.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div class="page">
|
|
13
|
+
|
|
14
|
+
<header class="head">
|
|
15
|
+
<div class="head-left">
|
|
16
|
+
<h1>hncode <span class="dim" data-i18n="home.brandSuffix">sessions</span></h1>
|
|
17
|
+
<p class="lede" data-i18n="home.lede"></p>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="head-right">
|
|
20
|
+
<div class="nav-lang" id="lang-switch"></div>
|
|
21
|
+
<button id="refresh" class="btn btn-quiet" data-i18n="home.refresh"></button>
|
|
22
|
+
<button id="logout" class="btn btn-quiet" data-i18n="home.logout"></button>
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<p id="err" class="err" role="alert" hidden></p>
|
|
27
|
+
|
|
28
|
+
<main id="groups" class="groups"></main>
|
|
29
|
+
|
|
30
|
+
</div>
|
|
31
|
+
<script type="module" src="/home.js"></script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { t, catalog, LANGUAGES, DEFAULT_LANG } from './i18n.js';
|
|
4
|
+
|
|
5
|
+
let LANG = DEFAULT_LANG;
|
|
6
|
+
try {
|
|
7
|
+
const saved = localStorage.getItem('hncode.lang');
|
|
8
|
+
if (saved && LANGUAGES.some((l) => l.id === saved)) LANG = saved;
|
|
9
|
+
} catch {}
|
|
10
|
+
let T = catalog(LANG);
|
|
11
|
+
const tr = (key, vars) => {
|
|
12
|
+
let s = T[key] || key;
|
|
13
|
+
if (vars) for (const k of Object.keys(vars)) s = s.split('{' + k + '}').join(String(vars[k]));
|
|
14
|
+
return s;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function applyStatic() {
|
|
18
|
+
document.documentElement.lang = LANG === 'zh' ? 'zh-CN' : 'en';
|
|
19
|
+
for (const el of document.querySelectorAll('[data-i18n]')) {
|
|
20
|
+
el.textContent = tr(el.getAttribute('data-i18n'));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Language switcher in the header, mirroring the app's buildLangSwitch: one
|
|
25
|
+
// button per installed language; clicks update the module LANG, the localStorage
|
|
26
|
+
// preference (shared with the app so a choice here carries over) and re-render
|
|
27
|
+
// the static strings.
|
|
28
|
+
function buildLangSwitch() {
|
|
29
|
+
const box = document.getElementById('lang-switch');
|
|
30
|
+
if (!box) return;
|
|
31
|
+
box.replaceChildren();
|
|
32
|
+
box.title = tr('nav.language');
|
|
33
|
+
for (const l of LANGUAGES) {
|
|
34
|
+
const b = document.createElement('button');
|
|
35
|
+
b.type = 'button';
|
|
36
|
+
b.textContent = l.label;
|
|
37
|
+
b.className = l.id === LANG ? 'is-on' : '';
|
|
38
|
+
b.addEventListener('click', () => {
|
|
39
|
+
if (l.id === LANG) return;
|
|
40
|
+
LANG = l.id;
|
|
41
|
+
T = catalog(LANG);
|
|
42
|
+
try { localStorage.setItem('hncode.lang', LANG); } catch {}
|
|
43
|
+
buildLangSwitch();
|
|
44
|
+
applyStatic();
|
|
45
|
+
});
|
|
46
|
+
box.appendChild(b);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const groupsEl = document.getElementById('groups');
|
|
51
|
+
const errEl = document.getElementById('err');
|
|
52
|
+
|
|
53
|
+
function el(tag, cls, text) {
|
|
54
|
+
const n = document.createElement(tag);
|
|
55
|
+
if (cls) n.className = cls;
|
|
56
|
+
if (text != null) n.textContent = text;
|
|
57
|
+
return n;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function relTime(ms) {
|
|
61
|
+
if (!ms) return '';
|
|
62
|
+
const diff = Date.now() - ms;
|
|
63
|
+
if (diff < 0) return new Date(ms).toLocaleString();
|
|
64
|
+
const min = Math.floor(diff / 60000);
|
|
65
|
+
if (min < 1) return tr('home.justNow');
|
|
66
|
+
if (min < 60) return tr('home.minutesAgo', { n: min });
|
|
67
|
+
const hr = Math.floor(min / 60);
|
|
68
|
+
if (hr < 24) return tr('home.hoursAgo', { n: hr });
|
|
69
|
+
const day = Math.floor(hr / 24);
|
|
70
|
+
if (day < 7) return tr('home.daysAgo', { n: day });
|
|
71
|
+
return new Date(ms).toLocaleDateString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sessionRow(s, live) {
|
|
75
|
+
const a = el('a', 'row');
|
|
76
|
+
a.href = `/s/${encodeURIComponent(s.id)}/`;
|
|
77
|
+
|
|
78
|
+
const titleCell = el('div');
|
|
79
|
+
titleCell.appendChild(el('span', 'row-title', s.title || tr('home.untitled')));
|
|
80
|
+
titleCell.appendChild(el('span', 'row-id', s.id));
|
|
81
|
+
titleCell.appendChild(el('span', 'tag ' + (live ? 'tag-live' : 'tag-ro'),
|
|
82
|
+
live ? tr('home.live') : tr('home.readonly')));
|
|
83
|
+
a.appendChild(titleCell);
|
|
84
|
+
|
|
85
|
+
a.appendChild(el('div', 'row-when', relTime(s.at || s.updatedAt)));
|
|
86
|
+
a.appendChild(el('div', 'row-rounds', s.rounds ? String(s.rounds) : ''));
|
|
87
|
+
return a;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function render(live, saved) {
|
|
91
|
+
groupsEl.textContent = '';
|
|
92
|
+
const liveIds = new Set(live.map((s) => s.id));
|
|
93
|
+
|
|
94
|
+
const byWs = new Map();
|
|
95
|
+
const push = (ws, item) => {
|
|
96
|
+
const key = ws || tr('home.noWorkspace');
|
|
97
|
+
if (!byWs.has(key)) byWs.set(key, []);
|
|
98
|
+
byWs.get(key).push(item);
|
|
99
|
+
};
|
|
100
|
+
for (const s of live) push(s.workspace, { s, live: true });
|
|
101
|
+
for (const s of saved) {
|
|
102
|
+
if (liveIds.has(s.id)) continue;
|
|
103
|
+
push(s.workspace, { s, live: false });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!byWs.size) {
|
|
107
|
+
groupsEl.appendChild(el('p', 'empty', tr('home.empty')));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const entries = [...byWs.entries()].map(([ws, items]) => ({
|
|
112
|
+
ws,
|
|
113
|
+
items,
|
|
114
|
+
anyLive: items.some((i) => i.live),
|
|
115
|
+
newest: Math.max(0, ...items.map((i) => i.s.at || i.s.updatedAt || 0)),
|
|
116
|
+
}));
|
|
117
|
+
entries.sort((a, b) => (b.anyLive - a.anyLive) || (b.newest - a.newest));
|
|
118
|
+
|
|
119
|
+
for (const g of entries) {
|
|
120
|
+
const sec = el('section', 'group');
|
|
121
|
+
const head = el('div', 'group-head');
|
|
122
|
+
head.appendChild(el('span', 'ws', g.ws));
|
|
123
|
+
head.appendChild(el('span', 'count', tr('home.sessionCount', { n: g.items.length })));
|
|
124
|
+
sec.appendChild(head);
|
|
125
|
+
const rows = el('div', 'rows');
|
|
126
|
+
for (const it of g.items) rows.appendChild(sessionRow(it.s, it.live));
|
|
127
|
+
sec.appendChild(rows);
|
|
128
|
+
groupsEl.appendChild(sec);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let timer = null;
|
|
133
|
+
|
|
134
|
+
async function load() {
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch('api/sessions');
|
|
137
|
+
if (res.status === 401) { location.replace('/login'); return; }
|
|
138
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
errEl.hidden = true;
|
|
141
|
+
render(data.live || [], data.saved || []);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
errEl.textContent = tr('home.loadFailed') + e.message;
|
|
144
|
+
errEl.hidden = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
document.getElementById('refresh').addEventListener('click', () => { void load(); });
|
|
149
|
+
|
|
150
|
+
document.getElementById('logout').addEventListener('click', () => {
|
|
151
|
+
document.cookie = 'hncode_web=; Max-Age=0; Path=/';
|
|
152
|
+
location.replace('/login');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
buildLangSwitch();
|
|
156
|
+
applyStatic();
|
|
157
|
+
void load();
|
|
158
|
+
|
|
159
|
+
timer = setInterval(() => { void load(); }, 5000);
|
|
160
|
+
window.addEventListener('beforeunload', () => { if (timer) clearInterval(timer); });
|