@yaebal/panel 0.0.1 → 0.0.4
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/README.md +203 -12
- package/lib/index.d.ts +131 -17
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +619 -36
- package/lib/index.js.map +1 -1
- package/lib/index.test.js +400 -1
- package/lib/index.test.js.map +1 -1
- package/lib/panel-html.d.ts +2 -2
- package/lib/panel-html.d.ts.map +1 -1
- package/lib/panel-html.js +504 -44
- package/lib/panel-html.js.map +1 -1
- package/lib/serve.d.ts +25 -0
- package/lib/serve.d.ts.map +1 -0
- package/lib/serve.js +47 -0
- package/lib/serve.js.map +1 -0
- package/lib/sqlite.d.ts +29 -0
- package/lib/sqlite.d.ts.map +1 -0
- package/lib/sqlite.js +155 -0
- package/lib/sqlite.js.map +1 -0
- package/lib/sqlite.test.d.ts +2 -0
- package/lib/sqlite.test.d.ts.map +1 -0
- package/lib/sqlite.test.js +75 -0
- package/lib/sqlite.test.js.map +1 -0
- package/package.json +10 -2
- package/src/index.test.ts +514 -2
- package/src/index.ts +804 -54
- package/src/panel-html.ts +504 -44
- package/src/serve.ts +65 -0
- package/src/sqlite.test.ts +96 -0
- package/src/sqlite.ts +213 -0
package/lib/panel-html.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** the operator panel ui
|
|
1
|
+
/** the operator panel ui - a single static page: token login, then the live chat view. */
|
|
2
2
|
export const PANEL_HTML = `<!doctype html>
|
|
3
3
|
<html lang="en">
|
|
4
4
|
<head>
|
|
@@ -6,58 +6,490 @@ export const PANEL_HTML = `<!doctype html>
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
7
|
<title>yaebal panel</title>
|
|
8
8
|
<style>
|
|
9
|
-
:root {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light;
|
|
11
|
+
--primary:#ffffff; --secondary:#000000; --white:#ffffff; --gray:#75757e;
|
|
12
|
+
--blue:#2f8af9; --red:#ed2236;
|
|
13
|
+
--button:#f4f4f4; --button-hover:#ededed; --button-press:#e8e8e8;
|
|
14
|
+
--button-stroke:rgba(0,0,0,.06); --button-text:#282828;
|
|
15
|
+
--sidebar-bg:#fbfbfb; --sidebar-stroke:rgba(0,0,0,.06);
|
|
16
|
+
--content-border:rgba(0,0,0,.08); --input-border:#adadb7;
|
|
17
|
+
--radius:18px; --radius-sm:12px; --sidebar-width:320px;
|
|
18
|
+
--shadow:0 18px 70px rgba(0,0,0,.14);
|
|
19
|
+
}
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
color-scheme: dark;
|
|
23
|
+
--primary:#000000; --secondary:#e1e1e1; --gray:#818181;
|
|
24
|
+
--blue:#2a7ce1; --red:#ff5b70;
|
|
25
|
+
--button:#191919; --button-hover:#242424; --button-press:#2a2a2a;
|
|
26
|
+
--button-stroke:rgba(255,255,255,.05); --button-text:#e1e1e1;
|
|
27
|
+
--sidebar-bg:#0c0c0c; --sidebar-stroke:rgba(255,255,255,.05);
|
|
28
|
+
--content-border:rgba(255,255,255,.08); --input-border:#383838;
|
|
29
|
+
--shadow:0 18px 70px rgba(0,0,0,.42);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
* { box-sizing:border-box; margin:0; }
|
|
33
|
+
html, body { height:100%; }
|
|
34
|
+
body {
|
|
35
|
+
min-height:100%; overflow:hidden; background:var(--primary); color:var(--secondary);
|
|
36
|
+
font:15px/1.5 "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
37
|
+
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
|
38
|
+
}
|
|
39
|
+
button, input { font:inherit; }
|
|
40
|
+
button { color:inherit; }
|
|
41
|
+
svg { display:block; width:18px; height:18px; }
|
|
42
|
+
.ico { display:inline-flex; align-items:center; justify-content:center; flex:none; }
|
|
43
|
+
:focus-visible { outline:solid 2px var(--blue); outline-offset:-2px; }
|
|
44
|
+
::selection { background:var(--secondary); color:var(--primary); }
|
|
45
|
+
::-webkit-scrollbar { width:10px; height:10px; }
|
|
46
|
+
::-webkit-scrollbar-thumb { background:var(--button-press); border-radius:10px; border:2px solid var(--primary); }
|
|
47
|
+
|
|
48
|
+
@keyframes fade-in { from { opacity:0; } to { opacity:1; } }
|
|
49
|
+
@keyframes rise-in { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
|
|
50
|
+
@keyframes slide-in { from { opacity:0; transform:translateX(-10px); } to { opacity:1; transform:translateX(0); } }
|
|
51
|
+
@keyframes msg-in { from { opacity:0; transform:translateY(8px) scale(.985); } to { opacity:1; transform:translateY(0) scale(1); } }
|
|
52
|
+
@keyframes viewer-in { from { opacity:0; backdrop-filter:blur(0); } to { opacity:1; backdrop-filter:blur(10px); } }
|
|
53
|
+
@keyframes soft-pulse { 0%, 100% { transform:scaleY(.72); opacity:.45; } 50% { transform:scaleY(1); opacity:.95; } }
|
|
54
|
+
|
|
55
|
+
#login { width:min(380px, calc(100vw - 32px)); min-height:100%; margin:auto; display:flex; align-items:center; justify-content:center; padding:24px; }
|
|
56
|
+
#login .card { width:100%; padding:24px; border:1px solid var(--content-border); border-radius:20px; background:var(--sidebar-bg); animation:rise-in .32s ease both; }
|
|
57
|
+
#login .mark { width:44px; height:44px; border-radius:14px; display:grid; place-items:center; margin:0 auto 18px; background:var(--secondary); color:var(--primary); }
|
|
58
|
+
#login .mark svg { width:22px; height:22px; }
|
|
59
|
+
#login .brand { text-align:center; font-size:20px; font-weight:700; letter-spacing:-.5px; }
|
|
60
|
+
#login .brand b { font-weight:700; }
|
|
61
|
+
#login .sub { margin:4px 0 20px; text-align:center; color:var(--gray); font-size:13px; font-weight:500; }
|
|
62
|
+
#login input, #login button { width:100%; height:44px; border-radius:14px; padding:0 14px; }
|
|
63
|
+
#login input { border:1px solid var(--input-border); background:var(--primary); color:var(--secondary); text-align:center; outline:none; transition:border-color .16s ease, box-shadow .16s ease; }
|
|
64
|
+
#login input:focus { border-color:var(--blue); box-shadow:0 0 0 4px color-mix(in srgb, var(--blue) 15%, transparent); }
|
|
65
|
+
#login button { margin-top:10px; border:0; background:var(--secondary); color:var(--primary); font-weight:600; cursor:pointer; transition:opacity .16s ease, transform .16s ease; }
|
|
66
|
+
#login button:hover { opacity:.86; }
|
|
67
|
+
#login button:active { transform:translateY(1px); }
|
|
68
|
+
#login button:disabled { opacity:.55; cursor:default; transform:none; }
|
|
69
|
+
#login .err { min-height:18px; margin-top:12px; text-align:center; color:var(--red); font-size:12px; }
|
|
70
|
+
|
|
71
|
+
#app { height:100%; display:none; animation:fade-in .2s ease both; }
|
|
72
|
+
body.authed #login { display:none; }
|
|
73
|
+
body.authed #app { display:flex; }
|
|
74
|
+
#chats { width:var(--sidebar-width); flex:none; display:flex; flex-direction:column; min-height:0; background:var(--sidebar-bg); border-right:1px solid var(--sidebar-stroke); }
|
|
75
|
+
#main { flex:1; min-width:0; min-height:0; display:flex; flex-direction:column; background:var(--primary); }
|
|
76
|
+
|
|
77
|
+
.side-top { flex:none; padding:20px 16px 18px; display:flex; align-items:center; justify-content:space-between; gap:12px; border-bottom:1px solid var(--sidebar-stroke); }
|
|
78
|
+
.side-title { min-width:0; display:flex; align-items:center; gap:12px; }
|
|
79
|
+
.side-logo { width:38px; height:38px; border-radius:13px; display:grid; place-items:center; background:var(--secondary); color:var(--primary); flex:none; }
|
|
80
|
+
.side-logo svg { width:18px; height:18px; }
|
|
81
|
+
.side-title h1 { font-size:15px; font-weight:600; letter-spacing:-.3px; }
|
|
82
|
+
.side-title p { margin-top:1px; color:var(--gray); font-size:12.5px; font-weight:500; }
|
|
83
|
+
.ghost { width:38px; height:38px; padding:0; border:0; border-radius:14px; display:grid; place-items:center; background:var(--button); color:var(--button-text); box-shadow:0 0 0 1px var(--button-stroke) inset; cursor:pointer; transition:background .16s ease, transform .16s ease; }
|
|
84
|
+
.ghost:hover { background:var(--button-hover); }
|
|
85
|
+
.ghost:active { transform:scale(.97); }
|
|
86
|
+
|
|
87
|
+
#chat-list { flex:1; min-height:0; overflow:auto; padding:10px 8px; }
|
|
88
|
+
.chat { width:100%; border:0; background:transparent; color:inherit; display:grid; grid-template-columns:42px 1fr; gap:11px; padding:9px 10px; border-radius:13px; text-align:left; cursor:pointer; animation:slide-in .24s ease both; transition:background .16s ease, color .16s ease, transform .16s ease; }
|
|
89
|
+
.chat:hover { background:var(--button-hover); }
|
|
90
|
+
.chat:active { transform:scale(.99); }
|
|
91
|
+
.chat.on { background:var(--secondary); color:var(--primary); }
|
|
92
|
+
.avatar { width:42px; height:42px; border-radius:13px; display:grid; place-items:center; background:var(--button); color:var(--button-text); box-shadow:0 0 0 1px var(--button-stroke) inset; flex:none; }
|
|
93
|
+
.chat.on .avatar { background:var(--primary); color:var(--secondary); }
|
|
94
|
+
.avatar.small { width:38px; height:38px; border-radius:12px; }
|
|
95
|
+
.avatar svg { width:18px; height:18px; }
|
|
96
|
+
.chat-body { min-width:0; align-self:center; }
|
|
97
|
+
.chat-line { display:flex; align-items:baseline; justify-content:space-between; gap:8px; min-width:0; }
|
|
98
|
+
.chat-name { font-weight:600; letter-spacing:-.2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
99
|
+
.chat-handle { color:var(--gray); font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:1px; }
|
|
100
|
+
.chat.on .chat-handle, .chat.on .chat-time, .chat.on .preview { color:color-mix(in srgb, var(--primary) 70%, transparent); }
|
|
101
|
+
.chat-time { color:var(--gray); font-size:11.5px; flex:none; }
|
|
102
|
+
.preview { min-width:0; display:flex; align-items:center; gap:5px; margin-top:4px; color:var(--gray); }
|
|
103
|
+
.preview .ico svg { width:13px; height:13px; }
|
|
104
|
+
.preview-label { font-weight:600; font-size:12px; flex:none; color:inherit; }
|
|
105
|
+
.preview-text { min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:12.5px; }
|
|
106
|
+
|
|
107
|
+
#empty { flex:1; display:grid; place-items:center; color:var(--gray); animation:fade-in .22s ease both; }
|
|
108
|
+
#empty .empty-card { text-align:center; padding:28px; }
|
|
109
|
+
#empty .empty-icon { width:58px; height:58px; border:1px solid var(--content-border); border-radius:18px; display:grid; place-items:center; margin:0 auto 14px; background:var(--sidebar-bg); }
|
|
110
|
+
#empty svg { width:26px; height:26px; color:var(--gray); }
|
|
111
|
+
.thread-head { flex:none; min-height:68px; padding:14px 20px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--content-border); background:var(--primary); }
|
|
112
|
+
.thread-head .meta { min-width:0; }
|
|
113
|
+
.thread-head .name { font-weight:600; letter-spacing:-.25px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
114
|
+
.thread-head .sub { color:var(--gray); font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
115
|
+
.back { display:none; }
|
|
116
|
+
#log { flex:1; min-height:0; overflow:auto; padding:22px 24px; display:flex; flex-direction:column; gap:8px; }
|
|
117
|
+
#log .more { align-self:center; border:1px solid var(--content-border); border-radius:999px; background:var(--sidebar-bg); color:var(--gray); padding:6px 13px; cursor:pointer; font-size:12px; transition:border-color .16s ease, color .16s ease; }
|
|
118
|
+
#log .more:hover { border-color:var(--blue); color:var(--secondary); }
|
|
119
|
+
.msg { display:flex; flex-direction:column; max-width:min(560px, 72%); gap:4px; animation:msg-in .22s ease both; }
|
|
120
|
+
.msg.in { align-self:flex-start; transform-origin:left bottom; }
|
|
121
|
+
.msg.out { align-self:flex-end; align-items:flex-end; transform-origin:right bottom; }
|
|
122
|
+
.bubble { border:1px solid var(--content-border); border-radius:16px; padding:9px 11px; background:var(--sidebar-bg); color:var(--secondary); }
|
|
123
|
+
.out .bubble { border-color:var(--blue); background:var(--blue); color:var(--white); }
|
|
124
|
+
.cap { padding:3px 0 0; white-space:pre-wrap; word-break:break-word; }
|
|
125
|
+
.time { color:var(--gray); font-size:11.5px; padding:0 4px; }
|
|
126
|
+
.out .time { color:var(--gray); }
|
|
127
|
+
|
|
128
|
+
.media-frame { position:relative; overflow:hidden; border:0; padding:0; cursor:pointer; color:inherit; background:var(--button); border-radius:14px; display:block; max-width:340px; transition:transform .18s ease, filter .18s ease; }
|
|
129
|
+
.media-frame:hover { transform:translateY(-1px); filter:brightness(1.03); }
|
|
130
|
+
.media-frame img, .media-frame video { display:block; width:100%; max-width:340px; max-height:380px; object-fit:cover; border-radius:14px; }
|
|
131
|
+
.expand { position:absolute; right:8px; top:8px; width:30px; height:30px; border:1px solid rgba(255,255,255,.28); border-radius:10px; background:rgba(0,0,0,.42); color:#fff; display:grid; place-items:center; opacity:0; transform:translateY(-4px); transition:opacity .16s ease, transform .16s ease; }
|
|
132
|
+
.media-frame:hover .expand, .video-card:hover .expand { opacity:1; transform:translateY(0); }
|
|
133
|
+
.video-card { position:relative; border:1px solid var(--content-border); border-radius:15px; padding:4px; background:var(--button); transition:transform .18s ease; }
|
|
134
|
+
.video-card:hover { transform:translateY(-1px); }
|
|
135
|
+
.video-card video { border-radius:12px; background:#000; width:min(390px, 70vw); max-height:380px; display:block; }
|
|
136
|
+
.video-badge { position:absolute; left:12px; top:12px; display:inline-flex; align-items:center; gap:6px; padding:4px 8px; border-radius:999px; background:rgba(0,0,0,.56); color:#fff; font-size:11px; font-weight:600; }
|
|
137
|
+
.video-badge svg { width:13px; height:13px; }
|
|
138
|
+
.voice-card, .audio-card, .doc-card { display:flex; align-items:center; gap:11px; min-width:min(300px, 68vw); padding:10px; border-radius:14px; background:var(--button); border:1px solid var(--button-stroke); color:inherit; text-decoration:none; }
|
|
139
|
+
.out .voice-card, .out .audio-card, .out .doc-card { background:rgba(255,255,255,.14); border-color:rgba(255,255,255,.18); }
|
|
140
|
+
.voice-mark, .doc-mark { width:38px; height:38px; border-radius:12px; display:grid; place-items:center; flex:none; background:var(--primary); color:var(--secondary); box-shadow:0 0 0 1px var(--button-stroke) inset; }
|
|
141
|
+
.out .voice-mark, .out .doc-mark { background:rgba(255,255,255,.18); color:#fff; }
|
|
142
|
+
.wave { display:flex; align-items:center; gap:3px; height:26px; flex:1; min-width:62px; }
|
|
143
|
+
.wave i { display:block; width:3px; border-radius:999px; background:currentColor; transform-origin:center; animation:soft-pulse 1.45s ease-in-out infinite; }
|
|
144
|
+
.wave i:nth-child(1) { height:10px; animation-delay:0s; } .wave i:nth-child(2) { height:18px; animation-delay:.08s; } .wave i:nth-child(3) { height:24px; animation-delay:.16s; }
|
|
145
|
+
.wave i:nth-child(4) { height:14px; animation-delay:.24s; } .wave i:nth-child(5) { height:22px; animation-delay:.32s; } .wave i:nth-child(6) { height:12px; animation-delay:.40s; }
|
|
146
|
+
.voice-card audio, .audio-card audio { width:142px; max-width:36vw; height:30px; }
|
|
147
|
+
.doc-title { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:600; }
|
|
148
|
+
.doc-sub { color:var(--gray); font-size:12px; }
|
|
149
|
+
.out .doc-sub { color:rgba(255,255,255,.72); }
|
|
150
|
+
.album-grid { display:grid; grid-template-columns:repeat(2, minmax(108px, 1fr)); gap:5px; max-width:340px; }
|
|
151
|
+
.album-grid .media-frame, .album-grid .video-card { max-width:none; }
|
|
152
|
+
.album-grid img, .album-grid video { height:142px; max-height:142px; }
|
|
153
|
+
.keyboard { display:flex; flex-direction:column; gap:5px; margin-top:8px; min-width:min(280px, 58vw); }
|
|
154
|
+
.keyboard-row { display:flex; gap:5px; }
|
|
155
|
+
.key { flex:1; min-width:0; display:flex; align-items:center; justify-content:center; gap:6px; padding:7px 9px; border-radius:11px; border:1px solid var(--content-border); background:var(--primary); color:inherit; font-size:12.5px; font-weight:600; text-decoration:none; transition:background .16s ease, transform .16s ease; }
|
|
156
|
+
.out .key { background:rgba(255,255,255,.14); border-color:rgba(255,255,255,.18); }
|
|
157
|
+
.key:hover { background:var(--button-hover); transform:translateY(-1px); }
|
|
158
|
+
.out .key:hover { background:rgba(255,255,255,.2); }
|
|
159
|
+
.key span:last-child { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
160
|
+
.key svg { width:13px; height:13px; opacity:.82; }
|
|
161
|
+
.eventline { align-self:center; max-width:min(520px, 86%); display:flex; align-items:center; gap:9px; padding:7px 12px; border:1px solid var(--content-border); border-radius:999px; background:var(--sidebar-bg); color:var(--gray); font-size:12.5px; animation:msg-in .22s ease both; }
|
|
162
|
+
.eventline .event-title { color:var(--secondary); font-weight:600; }
|
|
163
|
+
.eventline svg { width:14px; height:14px; color:var(--blue); }
|
|
164
|
+
|
|
165
|
+
#composer { flex:none; display:flex; align-items:center; gap:10px; padding:14px 18px; border-top:1px solid var(--content-border); background:var(--primary); }
|
|
166
|
+
#composer input.text { flex:1; height:44px; min-width:0; background:var(--button); border:1px solid var(--button-stroke); color:var(--secondary); border-radius:15px; padding:0 14px; outline:none; transition:border-color .16s ease, box-shadow .16s ease, background .16s ease; }
|
|
167
|
+
#composer input.text:focus { background:var(--primary); border-color:var(--blue); box-shadow:0 0 0 4px color-mix(in srgb, var(--blue) 14%, transparent); }
|
|
168
|
+
#composer button { height:44px; border:0; border-radius:15px; cursor:pointer; transition:background .16s ease, transform .16s ease, opacity .16s ease; }
|
|
169
|
+
#composer button:active { transform:scale(.98); }
|
|
170
|
+
#composer .attach { width:44px; background:var(--button); color:var(--button-text); box-shadow:0 0 0 1px var(--button-stroke) inset; display:grid; place-items:center; }
|
|
171
|
+
#composer .attach:hover { background:var(--button-hover); }
|
|
172
|
+
#composer .send { display:inline-flex; align-items:center; gap:8px; padding:0 17px; font-weight:600; background:var(--secondary); color:var(--primary); }
|
|
173
|
+
#composer .send:hover { opacity:.86; }
|
|
174
|
+
|
|
175
|
+
#viewer[hidden] { display:none; }
|
|
176
|
+
#viewer { position:fixed; inset:0; z-index:20; display:grid; place-items:center; padding:22px; background:rgba(0,0,0,.72); animation:viewer-in .18s ease both; }
|
|
177
|
+
#viewer .viewer-card { max-width:min(980px, 100%); max-height:100%; display:flex; flex-direction:column; gap:12px; animation:rise-in .22s ease both; }
|
|
178
|
+
#viewer .viewer-top { display:flex; justify-content:flex-end; }
|
|
179
|
+
#viewer .viewer-close { width:40px; height:40px; border:1px solid rgba(255,255,255,.16); border-radius:14px; background:rgba(255,255,255,.08); color:#fff; display:grid; place-items:center; cursor:pointer; }
|
|
180
|
+
#viewer .viewer-frame { display:grid; place-items:center; min-height:0; }
|
|
181
|
+
#viewer img, #viewer video { max-width:100%; max-height:78vh; border-radius:18px; box-shadow:var(--shadow); background:#000; }
|
|
182
|
+
#viewer .viewer-caption { color:rgba(255,255,255,.82); text-align:center; max-width:760px; }
|
|
183
|
+
|
|
184
|
+
@media (max-width: 780px) {
|
|
185
|
+
#chats { width:100%; }
|
|
186
|
+
#main { display:none; }
|
|
187
|
+
body.chat-open #chats { display:none; }
|
|
188
|
+
body.chat-open #main { display:flex; }
|
|
189
|
+
.back { display:grid; }
|
|
190
|
+
.msg { max-width:88%; }
|
|
191
|
+
#log { padding:16px 14px; }
|
|
192
|
+
#composer { padding:10px; }
|
|
193
|
+
#composer .send span:last-child { display:none; }
|
|
194
|
+
}
|
|
195
|
+
@media (prefers-reduced-motion: reduce) {
|
|
196
|
+
*, *::before, *::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; }
|
|
197
|
+
}
|
|
27
198
|
</style>
|
|
28
199
|
</head>
|
|
29
200
|
<body>
|
|
30
|
-
<
|
|
31
|
-
<div
|
|
201
|
+
<form id="login">
|
|
202
|
+
<div class="card">
|
|
203
|
+
<div class="mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 7.5 12 3l8 4.5v9L12 21l-8-4.5v-9Z"/><path d="M8.5 10.5h7M8.5 14h4.25"/></svg></div>
|
|
204
|
+
<div class="brand"><b>@yaebal</b>/panel</div>
|
|
205
|
+
<div class="sub">secure operator console</div>
|
|
206
|
+
<input id="token" type="password" placeholder="access token" autocomplete="off" autofocus />
|
|
207
|
+
<button id="go" type="submit">authorize</button>
|
|
208
|
+
<div class="err" id="err"></div>
|
|
209
|
+
</div>
|
|
210
|
+
</form>
|
|
211
|
+
|
|
212
|
+
<div id="app">
|
|
213
|
+
<aside id="chats">
|
|
214
|
+
<div class="side-top">
|
|
215
|
+
<div class="side-title">
|
|
216
|
+
<div class="side-logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 7.5 12 3l8 4.5v9L12 21l-8-4.5v-9Z"/><path d="M8.5 10.5h7M8.5 14h4.25"/></svg></div>
|
|
217
|
+
<div><h1>operator panel</h1><p>live private chats</p></div>
|
|
218
|
+
</div>
|
|
219
|
+
<button id="logout" class="ghost" type="button" aria-label="log out"></button>
|
|
220
|
+
</div>
|
|
221
|
+
<div id="chat-list"></div>
|
|
222
|
+
</aside>
|
|
223
|
+
<main id="main"><div id="empty"><div class="empty-card"><div class="empty-icon"></div><div>select a chat</div></div></div></main>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div id="viewer" hidden>
|
|
227
|
+
<div class="viewer-card">
|
|
228
|
+
<div class="viewer-top"><button class="viewer-close" type="button" aria-label="close viewer"></button></div>
|
|
229
|
+
<div class="viewer-frame"></div>
|
|
230
|
+
<div class="viewer-caption"></div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
32
234
|
<script>
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
let
|
|
235
|
+
const BASE = "__BASE__";
|
|
236
|
+
const KEY = "yaebal-panel-token" + BASE;
|
|
237
|
+
let token = sessionStorage.getItem(KEY) || "";
|
|
238
|
+
let active = null, oldest = null, es = null, chatsCache = [];
|
|
239
|
+
|
|
240
|
+
const ICONS = {
|
|
241
|
+
logout:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M10 6H6.8A1.8 1.8 0 0 0 5 7.8v8.4A1.8 1.8 0 0 0 6.8 18H10"/><path d="M14 8l4 4-4 4"/><path d="M18 12H9"/></svg>',
|
|
242
|
+
empty:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M5 7.5A3.5 3.5 0 0 1 8.5 4h7A3.5 3.5 0 0 1 19 7.5v4A3.5 3.5 0 0 1 15.5 15H12l-4.2 3.6V15A3.5 3.5 0 0 1 5 11.5v-4Z"/></svg>',
|
|
243
|
+
attach:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="m8.5 12.5 5.9-5.9a3.2 3.2 0 0 1 4.5 4.5l-7.1 7.1a4.6 4.6 0 0 1-6.5-6.5l7.3-7.3"/><path d="m9.5 15.1 7-7"/></svg>',
|
|
244
|
+
send:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M20 4 10.6 20l-1.8-7.2L4 10.5 20 4Z"/><path d="m8.8 12.8 5.4-3.5"/></svg>',
|
|
245
|
+
back:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M15 6 9 12l6 6"/></svg>',
|
|
246
|
+
close:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M7 7l10 10M17 7 7 17"/></svg>',
|
|
247
|
+
image:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="5" width="16" height="14" rx="3"/><path d="m7 16 3.5-3.5 2.7 2.7 1.8-1.8L18 16"/><circle cx="9" cy="9" r="1.2"/></svg>',
|
|
248
|
+
video:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="6" width="11" height="12" rx="3"/><path d="m15 10 5-3v10l-5-3"/></svg>',
|
|
249
|
+
mic:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="9" y="4" width="6" height="10" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg>',
|
|
250
|
+
audio:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 18V7l10-2v11"/><circle cx="6" cy="18" r="3"/><circle cx="16" cy="16" r="3"/></svg>',
|
|
251
|
+
file:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M7 3h6l4 4v14H7z"/><path d="M13 3v5h5M9.5 13h5M9.5 16h5"/></svg>',
|
|
252
|
+
sticker:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M6 4h12v9l-6 7H6z"/><path d="M12 20v-7h6M9 9h.01M14 9h.01"/></svg>',
|
|
253
|
+
callback:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="7" width="16" height="10" rx="4"/><path d="M8 12h8"/></svg>',
|
|
254
|
+
event:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 3v3M12 18v3M4.6 7.5l2.6 1.5M16.8 15l2.6 1.5M4.6 16.5 7.2 15M16.8 9l2.6-1.5"/><circle cx="12" cy="12" r="3.2"/></svg>',
|
|
255
|
+
keyboard:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="6" width="16" height="12" rx="3"/><path d="M7 10h.01M10.5 10h.01M14 10h.01M17.5 10h.01M7 14h7M16.5 14h.01"/></svg>',
|
|
256
|
+
search:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="6"/><path d="m16 16 4 4"/></svg>',
|
|
257
|
+
link:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M10 13a4 4 0 0 0 5.7 0l2-2a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 11a4 4 0 0 0-5.7 0l-2 2A4 4 0 0 0 12 18.7l1-1"/></svg>',
|
|
258
|
+
user:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.5"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/></svg>'
|
|
259
|
+
};
|
|
260
|
+
|
|
36
261
|
const el = (t, c, x) => { const e = document.createElement(t); if (c) e.className = c; if (x != null) e.textContent = x; return e; };
|
|
262
|
+
const icon = (name, c) => { const s = el("span", "ico" + (c ? " " + c : "")); s.innerHTML = ICONS[name] || ICONS.file; return s; };
|
|
263
|
+
const api = (p, opt = {}) => fetch(BASE + p, { ...opt, headers: { ...(opt.headers||{}), authorization: "Bearer " + token } });
|
|
264
|
+
|
|
265
|
+
document.getElementById("logout").append(icon("logout"));
|
|
266
|
+
document.querySelector("#empty .empty-icon").append(icon("empty"));
|
|
267
|
+
document.querySelector(".viewer-close").append(icon("close"));
|
|
268
|
+
|
|
269
|
+
const login = document.getElementById("login");
|
|
270
|
+
login.onsubmit = async (e) => {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
const go = document.getElementById("go"), err = document.getElementById("err");
|
|
273
|
+
token = document.getElementById("token").value.trim();
|
|
274
|
+
if (!token) return;
|
|
275
|
+
go.disabled = true; err.textContent = "";
|
|
276
|
+
const res = await api("/api/chats").catch(() => null);
|
|
277
|
+
go.disabled = false;
|
|
278
|
+
if (!res || !res.ok) { err.textContent = "invalid token"; return; }
|
|
279
|
+
sessionStorage.setItem(KEY, token);
|
|
280
|
+
enter();
|
|
281
|
+
};
|
|
282
|
+
document.getElementById("logout").onclick = () => {
|
|
283
|
+
sessionStorage.removeItem(KEY); token = ""; active = null; chatsCache = [];
|
|
284
|
+
if (es) { es.close(); es = null; }
|
|
285
|
+
document.body.classList.remove("authed", "chat-open");
|
|
286
|
+
document.getElementById("token").value = "";
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
function enter() { document.body.classList.add("authed"); loadChats(); openStream(); }
|
|
290
|
+
function openStream() {
|
|
291
|
+
if (es || !window.EventSource) return;
|
|
292
|
+
es = new EventSource(BASE + "/api/stream?token=" + encodeURIComponent(token));
|
|
293
|
+
es.addEventListener("record", (ev) => {
|
|
294
|
+
const e = JSON.parse(ev.data);
|
|
295
|
+
if (active && e.chatId === active) openChat(active, true);
|
|
296
|
+
loadChats();
|
|
297
|
+
});
|
|
298
|
+
es.onerror = () => {};
|
|
299
|
+
}
|
|
300
|
+
setInterval(() => { if (token) (active ? openChat(active, true) : loadChats()); }, 8000);
|
|
301
|
+
|
|
302
|
+
function displayName(c) {
|
|
303
|
+
const full = [c.firstName, c.lastName].filter(Boolean).join(" ").trim();
|
|
304
|
+
return full || (c.username ? "@" + c.username : c.name || "chat " + c.id);
|
|
305
|
+
}
|
|
306
|
+
function handleName(c) {
|
|
307
|
+
if (c.username) return "@" + c.username;
|
|
308
|
+
const name = displayName(c);
|
|
309
|
+
return c.name && c.name !== name ? c.name : "";
|
|
310
|
+
}
|
|
311
|
+
function avatar(c, small) {
|
|
312
|
+
const a = el("div", "avatar" + (small ? " small" : ""));
|
|
313
|
+
a.append(icon("user"));
|
|
314
|
+
return a;
|
|
315
|
+
}
|
|
316
|
+
function time(ts) {
|
|
317
|
+
if (!ts) return "";
|
|
318
|
+
const d = new Date(ts * 1000);
|
|
319
|
+
return d.toLocaleTimeString([], { hour:"2-digit", minute:"2-digit" });
|
|
320
|
+
}
|
|
321
|
+
const isPlaceholder = (t) => /^\\[[a-z_]+\\]$/.test(t || "");
|
|
322
|
+
const placeholderKind = (t) => { const m = /^\\[([a-z_]+)\\]$/.exec(t || ""); return m && m[1]; };
|
|
323
|
+
function mediaIcon(type) {
|
|
324
|
+
if (type === "photo") return "image";
|
|
325
|
+
if (type === "video" || type === "animation" || type === "video_note") return "video";
|
|
326
|
+
if (type === "voice") return "mic";
|
|
327
|
+
if (type === "audio") return "audio";
|
|
328
|
+
if (type === "sticker") return "sticker";
|
|
329
|
+
return "file";
|
|
330
|
+
}
|
|
331
|
+
function mediaLabel(type) { return String(type || "media").replace(/_/g, " "); }
|
|
332
|
+
function eventIcon(type) { return type === "callback" ? "callback" : "event"; }
|
|
37
333
|
|
|
38
334
|
async function loadChats() {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
box.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
335
|
+
const res = await api("/api/chats"); if (!res.ok) return;
|
|
336
|
+
chatsCache = await res.json();
|
|
337
|
+
const box = document.getElementById("chat-list");
|
|
338
|
+
box.innerHTML = "";
|
|
339
|
+
for (const c of chatsCache) box.append(chatRow(c));
|
|
340
|
+
}
|
|
341
|
+
function chatRow(c) {
|
|
342
|
+
const row = el("button", "chat" + (c.id === active ? " on" : "")); row.type = "button";
|
|
343
|
+
row.append(avatar(c));
|
|
344
|
+
const body = el("div", "chat-body");
|
|
345
|
+
const line = el("div", "chat-line");
|
|
346
|
+
line.append(el("div", "chat-name", displayName(c)), el("div", "chat-time", time(c.lastDate)));
|
|
347
|
+
body.append(line);
|
|
348
|
+
const handle = handleName(c); if (handle) body.append(el("div", "chat-handle", handle));
|
|
349
|
+
const preview = el("div", "preview");
|
|
350
|
+
const kind = c.lastAttachmentType || placeholderKind(c.lastText);
|
|
351
|
+
if (c.lastEventType) {
|
|
352
|
+
preview.append(icon(eventIcon(c.lastEventType)), el("span", "preview-label", "event"), el("span", "preview-text", c.lastText));
|
|
353
|
+
} else if (kind) {
|
|
354
|
+
const caption = isPlaceholder(c.lastText) ? "" : c.lastText;
|
|
355
|
+
preview.append(icon(mediaIcon(kind)), el("span", "preview-label", mediaLabel(kind)), el("span", "preview-text", caption || "media message"));
|
|
356
|
+
} else {
|
|
357
|
+
preview.append(el("span", "preview-text", c.lastText || "message"));
|
|
358
|
+
}
|
|
359
|
+
body.append(preview);
|
|
360
|
+
row.append(body);
|
|
361
|
+
row.onclick = () => openChat(c.id);
|
|
362
|
+
return row;
|
|
363
|
+
}
|
|
364
|
+
function chatById(id) { return chatsCache.find((c) => c.id === id) || { id, name:"chat " + id, lastText:"", lastDate:0 }; }
|
|
365
|
+
|
|
366
|
+
const fileSrc = (att) => BASE + "/api/file?id=" + encodeURIComponent(att.fileId) + "&token=" + encodeURIComponent(token);
|
|
367
|
+
function attEl(att, caption) {
|
|
368
|
+
const src = fileSrc(att);
|
|
369
|
+
if (att.type === "photo" || att.type === "sticker") {
|
|
370
|
+
const btn = el("button", "media-frame image"); btn.type = "button";
|
|
371
|
+
const img = el("img"); img.src = src; img.loading = "lazy"; img.alt = att.type;
|
|
372
|
+
btn.append(img, el("span", "expand")); btn.querySelector(".expand").append(icon("search"));
|
|
373
|
+
btn.onclick = () => openViewer(att, caption);
|
|
374
|
+
return btn;
|
|
375
|
+
}
|
|
376
|
+
if (att.type === "video" || att.type === "animation" || att.type === "video_note") {
|
|
377
|
+
const box = el("div", "video-card");
|
|
378
|
+
const v = el("video"); v.src = src; v.controls = true; v.preload = "metadata";
|
|
379
|
+
const badge = el("div", "video-badge"); badge.append(icon("video"), el("span", null, mediaLabel(att.type)));
|
|
380
|
+
const expand = el("button", "expand"); expand.type = "button"; expand.append(icon("search")); expand.onclick = () => openViewer(att, caption);
|
|
381
|
+
box.append(v, badge, expand);
|
|
382
|
+
return box;
|
|
383
|
+
}
|
|
384
|
+
if (att.type === "voice") {
|
|
385
|
+
const card = el("div", "voice-card");
|
|
386
|
+
const mark = el("div", "voice-mark"); mark.append(icon("mic"));
|
|
387
|
+
const wave = el("div", "wave"); for (let i = 0; i < 6; i++) wave.append(el("i"));
|
|
388
|
+
const a = el("audio"); a.src = src; a.controls = true; a.preload = "metadata";
|
|
389
|
+
card.append(mark, wave, a);
|
|
390
|
+
return card;
|
|
391
|
+
}
|
|
392
|
+
if (att.type === "audio") {
|
|
393
|
+
const card = el("div", "audio-card");
|
|
394
|
+
const mark = el("div", "voice-mark"); mark.append(icon("audio"));
|
|
395
|
+
const a = el("audio"); a.src = src; a.controls = true; a.preload = "metadata";
|
|
396
|
+
card.append(mark, a);
|
|
397
|
+
return card;
|
|
398
|
+
}
|
|
399
|
+
const link = el("a", "doc-card"); link.href = src; link.target = "_blank"; link.rel = "noreferrer";
|
|
400
|
+
const mark = el("div", "doc-mark"); mark.append(icon("file"));
|
|
401
|
+
const meta = el("div"); meta.append(el("div", "doc-title", att.fileName || mediaLabel(att.type)), el("div", "doc-sub", att.mimeType || "telegram file"));
|
|
402
|
+
link.append(mark, meta);
|
|
403
|
+
return link;
|
|
404
|
+
}
|
|
405
|
+
function keyboardEl(k) {
|
|
406
|
+
const box = el("div", "keyboard");
|
|
407
|
+
for (const row of k.rows || []) {
|
|
408
|
+
const r = el("div", "keyboard-row");
|
|
409
|
+
for (const b of row) {
|
|
410
|
+
const node = b.url ? el("a", "key") : el("div", "key");
|
|
411
|
+
if (b.url) { node.href = b.url; node.target = "_blank"; node.rel = "noreferrer"; }
|
|
412
|
+
node.append(icon(b.kind === "url" ? "link" : "keyboard"), el("span", null, b.text));
|
|
413
|
+
r.append(node);
|
|
414
|
+
}
|
|
415
|
+
box.append(r);
|
|
416
|
+
}
|
|
417
|
+
return box;
|
|
418
|
+
}
|
|
419
|
+
function eventBubble(m) {
|
|
420
|
+
const e = el("div", "eventline");
|
|
421
|
+
e.append(icon(eventIcon(m.event.type)), el("span", "event-title", m.event.title));
|
|
422
|
+
if (m.event.detail) e.append(el("span", null, m.event.detail));
|
|
423
|
+
return e;
|
|
424
|
+
}
|
|
425
|
+
function bubble(m) {
|
|
426
|
+
if (m.event) return eventBubble(m);
|
|
427
|
+
const wrap = el("div", "msg " + m.direction);
|
|
428
|
+
const card = el("div", "bubble");
|
|
429
|
+
const atts = m.attachments || [];
|
|
430
|
+
if (m.mediaGroupId && atts.length) {
|
|
431
|
+
const grid = el("div", "album-grid");
|
|
432
|
+
for (const a of atts) grid.append(attEl(a, m.text));
|
|
433
|
+
card.append(grid);
|
|
434
|
+
} else {
|
|
435
|
+
for (const a of atts) card.append(attEl(a, m.text));
|
|
47
436
|
}
|
|
437
|
+
if (m.text && !(atts.length && isPlaceholder(m.text))) card.append(el("div", atts.length ? "cap" : "cap", m.text));
|
|
438
|
+
if (m.keyboard) card.append(keyboardEl(m.keyboard));
|
|
439
|
+
wrap.append(card, el("div", "time", time(m.date)));
|
|
440
|
+
return wrap;
|
|
48
441
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
442
|
+
function addToAlbum(node, m) {
|
|
443
|
+
const grid = node.querySelector(".album-grid"); if (!grid) return;
|
|
444
|
+
for (const a of m.attachments || []) grid.append(attEl(a, m.text));
|
|
445
|
+
if (m.text && !isPlaceholder(m.text)) node.querySelector(".bubble").append(el("div", "cap", m.text));
|
|
446
|
+
}
|
|
447
|
+
function renderMsgs(msgs) {
|
|
448
|
+
const out = [];
|
|
449
|
+
let group = null, groupId = null;
|
|
450
|
+
for (const m of msgs) {
|
|
451
|
+
if (!m.event && m.mediaGroupId && m.mediaGroupId === groupId && group) { addToAlbum(group, m); continue; }
|
|
452
|
+
const b = bubble(m);
|
|
453
|
+
if (!m.event && m.mediaGroupId) { group = b; groupId = m.mediaGroupId; }
|
|
454
|
+
else { group = null; groupId = null; }
|
|
455
|
+
out.push(b);
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function openChat(id, keepScroll) {
|
|
461
|
+
active = id; document.body.classList.add("chat-open");
|
|
462
|
+
if (!keepScroll) loadChats();
|
|
463
|
+
const res = await api("/api/chats/" + id + "?limit=200"); if (!res.ok) return;
|
|
464
|
+
const msgs = await res.json(); oldest = msgs.length ? msgs[0].date : null;
|
|
465
|
+
const c = chatById(id);
|
|
53
466
|
const main = document.getElementById("main");
|
|
467
|
+
const prevTop = keepScroll ? (main.querySelector("#log")?.scrollTop ?? null) : null;
|
|
54
468
|
main.innerHTML = "";
|
|
469
|
+
|
|
470
|
+
const head = el("div", "thread-head");
|
|
471
|
+
const back = el("button", "ghost back"); back.type = "button"; back.append(icon("back")); back.onclick = () => { document.body.classList.remove("chat-open"); };
|
|
472
|
+
const meta = el("div", "meta"); meta.append(el("div", "name", displayName(c)), el("div", "sub", handleName(c) || "private chat"));
|
|
473
|
+
head.append(back, avatar(c, true), meta);
|
|
55
474
|
const log = el("div"); log.id = "log";
|
|
56
|
-
|
|
475
|
+
if (msgs.length >= 200) { const more = el("button", "more", "load earlier"); more.onclick = () => loadEarlier(id, log); log.append(more); }
|
|
476
|
+
for (const b of renderMsgs(msgs)) log.append(b);
|
|
477
|
+
|
|
57
478
|
const form = el("form"); form.id = "composer";
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
479
|
+
const fileInput = el("input"); fileInput.type = "file"; fileInput.style.display = "none";
|
|
480
|
+
const attach = el("button", "attach"); attach.type = "button"; attach.title = "send a file"; attach.append(icon("attach")); attach.onclick = () => fileInput.click();
|
|
481
|
+
const input = el("input", "text"); input.placeholder = "reply..."; input.autocomplete = "off";
|
|
482
|
+
const btn = el("button", "send"); btn.type = "submit"; btn.append(icon("send"), el("span", null, "send"));
|
|
483
|
+
form.append(attach, fileInput, input, btn);
|
|
484
|
+
|
|
485
|
+
fileInput.onchange = async () => {
|
|
486
|
+
const file = fileInput.files && fileInput.files[0]; if (!file) return;
|
|
487
|
+
const fd = new FormData(); fd.append("file", file);
|
|
488
|
+
if (input.value.trim()) fd.append("caption", input.value.trim());
|
|
489
|
+
input.value = ""; fileInput.value = "";
|
|
490
|
+
await api("/api/chats/" + id + "/send", { method: "POST", body: fd });
|
|
491
|
+
openChat(id);
|
|
492
|
+
};
|
|
61
493
|
form.onsubmit = async (e) => {
|
|
62
494
|
e.preventDefault();
|
|
63
495
|
const text = input.value.trim(); if (!text) return;
|
|
@@ -65,11 +497,39 @@ async function openChat(id) {
|
|
|
65
497
|
await api("/api/chats/" + id + "/send", { method:"POST", headers:{"content-type":"application/json"}, body: JSON.stringify({ text }) });
|
|
66
498
|
openChat(id);
|
|
67
499
|
};
|
|
68
|
-
main.append(log, form);
|
|
69
|
-
log.scrollTop = log.scrollHeight;
|
|
500
|
+
main.append(head, log, form);
|
|
501
|
+
log.scrollTop = prevTop != null ? prevTop : log.scrollHeight;
|
|
70
502
|
}
|
|
71
|
-
|
|
72
|
-
|
|
503
|
+
async function loadEarlier(id, log) {
|
|
504
|
+
if (oldest == null) return;
|
|
505
|
+
const res = await api("/api/chats/" + id + "?limit=200&before=" + oldest); if (!res.ok) return;
|
|
506
|
+
const older = await res.json();
|
|
507
|
+
if (!older.length) { log.querySelector(".more")?.remove(); return; }
|
|
508
|
+
oldest = older[0].date;
|
|
509
|
+
const anchor = log.querySelector(".more")?.nextSibling ?? log.firstChild;
|
|
510
|
+
const frag = document.createDocumentFragment();
|
|
511
|
+
if (older.length >= 200) { const more = el("button", "more", "load earlier"); more.onclick = () => loadEarlier(id, log); frag.append(more); }
|
|
512
|
+
for (const b of renderMsgs(older)) frag.append(b);
|
|
513
|
+
log.querySelector(".more")?.remove(); log.insertBefore(frag, anchor);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function openViewer(att, caption) {
|
|
517
|
+
const viewer = document.getElementById("viewer"), frame = viewer.querySelector(".viewer-frame"), cap = viewer.querySelector(".viewer-caption");
|
|
518
|
+
frame.innerHTML = ""; cap.textContent = caption && !isPlaceholder(caption) ? caption : "";
|
|
519
|
+
const src = fileSrc(att);
|
|
520
|
+
if (att.type === "video" || att.type === "animation" || att.type === "video_note") {
|
|
521
|
+
const v = el("video"); v.src = src; v.controls = true; v.autoplay = true; frame.append(v);
|
|
522
|
+
} else {
|
|
523
|
+
const img = el("img"); img.src = src; img.alt = att.type; frame.append(img);
|
|
524
|
+
}
|
|
525
|
+
viewer.hidden = false;
|
|
526
|
+
}
|
|
527
|
+
function closeViewer() { document.getElementById("viewer").hidden = true; }
|
|
528
|
+
document.querySelector(".viewer-close").onclick = closeViewer;
|
|
529
|
+
document.getElementById("viewer").onclick = (e) => { if (e.target.id === "viewer") closeViewer(); };
|
|
530
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeViewer(); });
|
|
531
|
+
|
|
532
|
+
if (token) enter();
|
|
73
533
|
</script>
|
|
74
534
|
</body>
|
|
75
535
|
</html>`;
|
package/lib/panel-html.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panel-html.js","sourceRoot":"","sources":["../src/panel-html.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"panel-html.js","sourceRoot":"","sources":["../src/panel-html.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAqhBlB,CAAC"}
|
package/lib/serve.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
/** options for the node {@link serve} helper. */
|
|
3
|
+
export interface ServeOptions {
|
|
4
|
+
/** port to listen on. */
|
|
5
|
+
port: number;
|
|
6
|
+
/** host/interface to bind. defaults to node's default (all interfaces). */
|
|
7
|
+
host?: string;
|
|
8
|
+
/** invoked once the server is listening. */
|
|
9
|
+
onListen?: (info: {
|
|
10
|
+
port: number;
|
|
11
|
+
host?: string;
|
|
12
|
+
}) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* start a native node `http` server for a fetch-style handler (e.g. {@link panelHandler}).
|
|
16
|
+
* zero third-party deps — just `node:http`. on bun/deno use their built-in `serve` instead.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { panelHandler } from "@yaebal/panel";
|
|
20
|
+
* import { serve } from "@yaebal/panel/serve";
|
|
21
|
+
* serve(panelHandler(bot.api, store, { token }), { port: 8080 });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function serve(handler: (request: Request) => Promise<Response> | Response, options: ServeOptions): Server;
|
|
25
|
+
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAEjG,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC5B,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC3D;AA0BD;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CACpB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAC3D,OAAO,EAAE,YAAY,GACnB,MAAM,CAeR"}
|