@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.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/** the operator panel ui
|
|
2
|
-
export declare const PANEL_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>yaebal panel</title>\n<style>\n :root { color-scheme: light dark; --bg:#0f1115; --panel:#171a21; --line:#252a33; --muted:#8b93a1; --accent:#229ED9; --text:#e6e8eb; }\n * { box-sizing: border-box; }\n body { margin:0; font:14px/1.5 system-ui,sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; }\n #chats { width:280px; border-right:1px solid var(--line); overflow-y:auto; flex:none; }\n #chats h1 { font-size:13px; color:var(--muted); padding:14px 16px; margin:0; letter-spacing:.5px; text-transform:lowercase; }\n .chat { padding:10px 16px; border-bottom:1px solid var(--line); cursor:pointer; }\n .chat:hover, .chat.on { background:var(--panel); }\n .chat .n { font-weight:500; }\n .chat .l { color:var(--muted); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n #main { flex:1; display:flex; flex-direction:column; min-width:0; }\n #log { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:8px; }\n .msg { max-width:70%; padding:8px 12px; border-radius:12px; white-space:pre-wrap; word-break:break-word; }\n .msg.in { background:var(--panel); align-self:flex-start; }\n .msg.out { background:var(--accent); color:#fff; align-self:flex-end; }\n #composer { display:flex; gap:8px; padding:12px; border-top:1px solid var(--line); }\n #composer input { flex:1; background:var(--panel); border:1px solid var(--line); color:var(--text); border-radius:8px; padding:9px 12px; font:inherit; }\n #composer button { background:var(--accent); color:#fff; border:0; border-radius:8px; padding:0 16px; cursor:pointer; font:inherit; }\n #empty { margin:auto; color:var(--muted); }\n</style>\n</head>\n<body>\n<div id=\"chats\"><h1>chats</h1></div>\n<div id=\"main\"><div id=\"empty\">select a chat</div></div>\n<script>\nconst token = new URLSearchParams(location.search).get(\"token\") || \"\";\nconst api = (p, opt = {}) => fetch(p, { ...opt, headers: { ...(opt.headers||{}), authorization: \"Bearer \" + token } });\nlet active = null;\nconst el = (t, c, x) => { const e = document.createElement(t); if (c) e.className = c; if (x != null) e.textContent = x; return e; };\n\nasync function loadChats() {\n const chats = await (await api(\"/api/chats\")).json();\n const box = document.getElementById(\"chats\");\n box.querySelectorAll(\".chat\").forEach(n => n.remove());\n for (const c of chats) {\n const d = el(\"div\", \"chat\" + (c.id === active ? \" on\" : \"\"));\n d.append(el(\"div\", \"n\", c.name), el(\"div\", \"l\", c.lastText));\n d.onclick = () => openChat(c.id);\n box.append(d);\n }\n}\nasync function openChat(id) {\n active = id;\n await loadChats();\n const msgs = await (await api(\"/api/chats/\" + id)).json();\n const main = document.getElementById(\"main\");\n main.innerHTML = \"\";\n const log = el(\"div\"); log.id = \"log\";\n for (const m of msgs) log.append(el(\"div\", \"msg \" + m.direction, m.text));\n const form = el(\"form\"); form.id = \"composer\";\n const input = el(\"input\"); input.placeholder = \"reply\u2026\"; input.autocomplete = \"off\";\n const btn = el(\"button\", null, \"send\"); btn.type = \"submit\";\n form.append(input, btn);\n form.onsubmit = async (e) => {\n e.preventDefault();\n const text = input.value.trim(); if (!text) return;\n input.value = \"\";\n await api(\"/api/chats/\" + id + \"/send\", { method:\"POST\", headers:{\"content-type\":\"application/json\"}, body: JSON.stringify({ text }) });\n openChat(id);\n };\n main.append(log, form);\n log.scrollTop = log.scrollHeight;\n}\nloadChats();\nsetInterval(() => (active ? openChat(active) : loadChats()), 4000);\n</script>\n</body>\n</html>";
|
|
1
|
+
/** the operator panel ui - a single static page: token login, then the live chat view. */
|
|
2
|
+
export declare const PANEL_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>yaebal panel</title>\n<style>\n :root {\n color-scheme: light;\n --primary:#ffffff; --secondary:#000000; --white:#ffffff; --gray:#75757e;\n --blue:#2f8af9; --red:#ed2236;\n --button:#f4f4f4; --button-hover:#ededed; --button-press:#e8e8e8;\n --button-stroke:rgba(0,0,0,.06); --button-text:#282828;\n --sidebar-bg:#fbfbfb; --sidebar-stroke:rgba(0,0,0,.06);\n --content-border:rgba(0,0,0,.08); --input-border:#adadb7;\n --radius:18px; --radius-sm:12px; --sidebar-width:320px;\n --shadow:0 18px 70px rgba(0,0,0,.14);\n }\n @media (prefers-color-scheme: dark) {\n :root {\n color-scheme: dark;\n --primary:#000000; --secondary:#e1e1e1; --gray:#818181;\n --blue:#2a7ce1; --red:#ff5b70;\n --button:#191919; --button-hover:#242424; --button-press:#2a2a2a;\n --button-stroke:rgba(255,255,255,.05); --button-text:#e1e1e1;\n --sidebar-bg:#0c0c0c; --sidebar-stroke:rgba(255,255,255,.05);\n --content-border:rgba(255,255,255,.08); --input-border:#383838;\n --shadow:0 18px 70px rgba(0,0,0,.42);\n }\n }\n * { box-sizing:border-box; margin:0; }\n html, body { height:100%; }\n body {\n min-height:100%; overflow:hidden; background:var(--primary); color:var(--secondary);\n font:15px/1.5 \"IBM Plex Sans\", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;\n }\n button, input { font:inherit; }\n button { color:inherit; }\n svg { display:block; width:18px; height:18px; }\n .ico { display:inline-flex; align-items:center; justify-content:center; flex:none; }\n :focus-visible { outline:solid 2px var(--blue); outline-offset:-2px; }\n ::selection { background:var(--secondary); color:var(--primary); }\n ::-webkit-scrollbar { width:10px; height:10px; }\n ::-webkit-scrollbar-thumb { background:var(--button-press); border-radius:10px; border:2px solid var(--primary); }\n\n @keyframes fade-in { from { opacity:0; } to { opacity:1; } }\n @keyframes rise-in { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }\n @keyframes slide-in { from { opacity:0; transform:translateX(-10px); } to { opacity:1; transform:translateX(0); } }\n @keyframes msg-in { from { opacity:0; transform:translateY(8px) scale(.985); } to { opacity:1; transform:translateY(0) scale(1); } }\n @keyframes viewer-in { from { opacity:0; backdrop-filter:blur(0); } to { opacity:1; backdrop-filter:blur(10px); } }\n @keyframes soft-pulse { 0%, 100% { transform:scaleY(.72); opacity:.45; } 50% { transform:scaleY(1); opacity:.95; } }\n\n #login { width:min(380px, calc(100vw - 32px)); min-height:100%; margin:auto; display:flex; align-items:center; justify-content:center; padding:24px; }\n #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; }\n #login .mark { width:44px; height:44px; border-radius:14px; display:grid; place-items:center; margin:0 auto 18px; background:var(--secondary); color:var(--primary); }\n #login .mark svg { width:22px; height:22px; }\n #login .brand { text-align:center; font-size:20px; font-weight:700; letter-spacing:-.5px; }\n #login .brand b { font-weight:700; }\n #login .sub { margin:4px 0 20px; text-align:center; color:var(--gray); font-size:13px; font-weight:500; }\n #login input, #login button { width:100%; height:44px; border-radius:14px; padding:0 14px; }\n #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; }\n #login input:focus { border-color:var(--blue); box-shadow:0 0 0 4px color-mix(in srgb, var(--blue) 15%, transparent); }\n #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; }\n #login button:hover { opacity:.86; }\n #login button:active { transform:translateY(1px); }\n #login button:disabled { opacity:.55; cursor:default; transform:none; }\n #login .err { min-height:18px; margin-top:12px; text-align:center; color:var(--red); font-size:12px; }\n\n #app { height:100%; display:none; animation:fade-in .2s ease both; }\n body.authed #login { display:none; }\n body.authed #app { display:flex; }\n #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); }\n #main { flex:1; min-width:0; min-height:0; display:flex; flex-direction:column; background:var(--primary); }\n\n .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); }\n .side-title { min-width:0; display:flex; align-items:center; gap:12px; }\n .side-logo { width:38px; height:38px; border-radius:13px; display:grid; place-items:center; background:var(--secondary); color:var(--primary); flex:none; }\n .side-logo svg { width:18px; height:18px; }\n .side-title h1 { font-size:15px; font-weight:600; letter-spacing:-.3px; }\n .side-title p { margin-top:1px; color:var(--gray); font-size:12.5px; font-weight:500; }\n .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; }\n .ghost:hover { background:var(--button-hover); }\n .ghost:active { transform:scale(.97); }\n\n #chat-list { flex:1; min-height:0; overflow:auto; padding:10px 8px; }\n .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; }\n .chat:hover { background:var(--button-hover); }\n .chat:active { transform:scale(.99); }\n .chat.on { background:var(--secondary); color:var(--primary); }\n .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; }\n .chat.on .avatar { background:var(--primary); color:var(--secondary); }\n .avatar.small { width:38px; height:38px; border-radius:12px; }\n .avatar svg { width:18px; height:18px; }\n .chat-body { min-width:0; align-self:center; }\n .chat-line { display:flex; align-items:baseline; justify-content:space-between; gap:8px; min-width:0; }\n .chat-name { font-weight:600; letter-spacing:-.2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .chat-handle { color:var(--gray); font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:1px; }\n .chat.on .chat-handle, .chat.on .chat-time, .chat.on .preview { color:color-mix(in srgb, var(--primary) 70%, transparent); }\n .chat-time { color:var(--gray); font-size:11.5px; flex:none; }\n .preview { min-width:0; display:flex; align-items:center; gap:5px; margin-top:4px; color:var(--gray); }\n .preview .ico svg { width:13px; height:13px; }\n .preview-label { font-weight:600; font-size:12px; flex:none; color:inherit; }\n .preview-text { min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:12.5px; }\n\n #empty { flex:1; display:grid; place-items:center; color:var(--gray); animation:fade-in .22s ease both; }\n #empty .empty-card { text-align:center; padding:28px; }\n #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); }\n #empty svg { width:26px; height:26px; color:var(--gray); }\n .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); }\n .thread-head .meta { min-width:0; }\n .thread-head .name { font-weight:600; letter-spacing:-.25px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .thread-head .sub { color:var(--gray); font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .back { display:none; }\n #log { flex:1; min-height:0; overflow:auto; padding:22px 24px; display:flex; flex-direction:column; gap:8px; }\n #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; }\n #log .more:hover { border-color:var(--blue); color:var(--secondary); }\n .msg { display:flex; flex-direction:column; max-width:min(560px, 72%); gap:4px; animation:msg-in .22s ease both; }\n .msg.in { align-self:flex-start; transform-origin:left bottom; }\n .msg.out { align-self:flex-end; align-items:flex-end; transform-origin:right bottom; }\n .bubble { border:1px solid var(--content-border); border-radius:16px; padding:9px 11px; background:var(--sidebar-bg); color:var(--secondary); }\n .out .bubble { border-color:var(--blue); background:var(--blue); color:var(--white); }\n .cap { padding:3px 0 0; white-space:pre-wrap; word-break:break-word; }\n .time { color:var(--gray); font-size:11.5px; padding:0 4px; }\n .out .time { color:var(--gray); }\n\n .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; }\n .media-frame:hover { transform:translateY(-1px); filter:brightness(1.03); }\n .media-frame img, .media-frame video { display:block; width:100%; max-width:340px; max-height:380px; object-fit:cover; border-radius:14px; }\n .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; }\n .media-frame:hover .expand, .video-card:hover .expand { opacity:1; transform:translateY(0); }\n .video-card { position:relative; border:1px solid var(--content-border); border-radius:15px; padding:4px; background:var(--button); transition:transform .18s ease; }\n .video-card:hover { transform:translateY(-1px); }\n .video-card video { border-radius:12px; background:#000; width:min(390px, 70vw); max-height:380px; display:block; }\n .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; }\n .video-badge svg { width:13px; height:13px; }\n .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; }\n .out .voice-card, .out .audio-card, .out .doc-card { background:rgba(255,255,255,.14); border-color:rgba(255,255,255,.18); }\n .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; }\n .out .voice-mark, .out .doc-mark { background:rgba(255,255,255,.18); color:#fff; }\n .wave { display:flex; align-items:center; gap:3px; height:26px; flex:1; min-width:62px; }\n .wave i { display:block; width:3px; border-radius:999px; background:currentColor; transform-origin:center; animation:soft-pulse 1.45s ease-in-out infinite; }\n .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; }\n .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; }\n .voice-card audio, .audio-card audio { width:142px; max-width:36vw; height:30px; }\n .doc-title { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:600; }\n .doc-sub { color:var(--gray); font-size:12px; }\n .out .doc-sub { color:rgba(255,255,255,.72); }\n .album-grid { display:grid; grid-template-columns:repeat(2, minmax(108px, 1fr)); gap:5px; max-width:340px; }\n .album-grid .media-frame, .album-grid .video-card { max-width:none; }\n .album-grid img, .album-grid video { height:142px; max-height:142px; }\n .keyboard { display:flex; flex-direction:column; gap:5px; margin-top:8px; min-width:min(280px, 58vw); }\n .keyboard-row { display:flex; gap:5px; }\n .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; }\n .out .key { background:rgba(255,255,255,.14); border-color:rgba(255,255,255,.18); }\n .key:hover { background:var(--button-hover); transform:translateY(-1px); }\n .out .key:hover { background:rgba(255,255,255,.2); }\n .key span:last-child { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n .key svg { width:13px; height:13px; opacity:.82; }\n .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; }\n .eventline .event-title { color:var(--secondary); font-weight:600; }\n .eventline svg { width:14px; height:14px; color:var(--blue); }\n\n #composer { flex:none; display:flex; align-items:center; gap:10px; padding:14px 18px; border-top:1px solid var(--content-border); background:var(--primary); }\n #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; }\n #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); }\n #composer button { height:44px; border:0; border-radius:15px; cursor:pointer; transition:background .16s ease, transform .16s ease, opacity .16s ease; }\n #composer button:active { transform:scale(.98); }\n #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; }\n #composer .attach:hover { background:var(--button-hover); }\n #composer .send { display:inline-flex; align-items:center; gap:8px; padding:0 17px; font-weight:600; background:var(--secondary); color:var(--primary); }\n #composer .send:hover { opacity:.86; }\n\n #viewer[hidden] { display:none; }\n #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; }\n #viewer .viewer-card { max-width:min(980px, 100%); max-height:100%; display:flex; flex-direction:column; gap:12px; animation:rise-in .22s ease both; }\n #viewer .viewer-top { display:flex; justify-content:flex-end; }\n #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; }\n #viewer .viewer-frame { display:grid; place-items:center; min-height:0; }\n #viewer img, #viewer video { max-width:100%; max-height:78vh; border-radius:18px; box-shadow:var(--shadow); background:#000; }\n #viewer .viewer-caption { color:rgba(255,255,255,.82); text-align:center; max-width:760px; }\n\n @media (max-width: 780px) {\n #chats { width:100%; }\n #main { display:none; }\n body.chat-open #chats { display:none; }\n body.chat-open #main { display:flex; }\n .back { display:grid; }\n .msg { max-width:88%; }\n #log { padding:16px 14px; }\n #composer { padding:10px; }\n #composer .send span:last-child { display:none; }\n }\n @media (prefers-reduced-motion: reduce) {\n *, *::before, *::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; }\n }\n</style>\n</head>\n<body>\n<form id=\"login\">\n <div class=\"card\">\n <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>\n <div class=\"brand\"><b>@yaebal</b>/panel</div>\n <div class=\"sub\">secure operator console</div>\n <input id=\"token\" type=\"password\" placeholder=\"access token\" autocomplete=\"off\" autofocus />\n <button id=\"go\" type=\"submit\">authorize</button>\n <div class=\"err\" id=\"err\"></div>\n </div>\n</form>\n\n<div id=\"app\">\n <aside id=\"chats\">\n <div class=\"side-top\">\n <div class=\"side-title\">\n <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>\n <div><h1>operator panel</h1><p>live private chats</p></div>\n </div>\n <button id=\"logout\" class=\"ghost\" type=\"button\" aria-label=\"log out\"></button>\n </div>\n <div id=\"chat-list\"></div>\n </aside>\n <main id=\"main\"><div id=\"empty\"><div class=\"empty-card\"><div class=\"empty-icon\"></div><div>select a chat</div></div></div></main>\n</div>\n\n<div id=\"viewer\" hidden>\n <div class=\"viewer-card\">\n <div class=\"viewer-top\"><button class=\"viewer-close\" type=\"button\" aria-label=\"close viewer\"></button></div>\n <div class=\"viewer-frame\"></div>\n <div class=\"viewer-caption\"></div>\n </div>\n</div>\n\n<script>\nconst BASE = \"__BASE__\";\nconst KEY = \"yaebal-panel-token\" + BASE;\nlet token = sessionStorage.getItem(KEY) || \"\";\nlet active = null, oldest = null, es = null, chatsCache = [];\n\nconst ICONS = {\n 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>',\n 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>',\n 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>',\n 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>',\n back:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\"><path d=\"M15 6 9 12l6 6\"/></svg>',\n close:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\"><path d=\"M7 7l10 10M17 7 7 17\"/></svg>',\n 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>',\n 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>',\n 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>',\n 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>',\n 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>',\n 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>',\n 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>',\n 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>',\n 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>',\n 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>',\n 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>',\n 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>'\n};\n\nconst el = (t, c, x) => { const e = document.createElement(t); if (c) e.className = c; if (x != null) e.textContent = x; return e; };\nconst icon = (name, c) => { const s = el(\"span\", \"ico\" + (c ? \" \" + c : \"\")); s.innerHTML = ICONS[name] || ICONS.file; return s; };\nconst api = (p, opt = {}) => fetch(BASE + p, { ...opt, headers: { ...(opt.headers||{}), authorization: \"Bearer \" + token } });\n\ndocument.getElementById(\"logout\").append(icon(\"logout\"));\ndocument.querySelector(\"#empty .empty-icon\").append(icon(\"empty\"));\ndocument.querySelector(\".viewer-close\").append(icon(\"close\"));\n\nconst login = document.getElementById(\"login\");\nlogin.onsubmit = async (e) => {\n e.preventDefault();\n const go = document.getElementById(\"go\"), err = document.getElementById(\"err\");\n token = document.getElementById(\"token\").value.trim();\n if (!token) return;\n go.disabled = true; err.textContent = \"\";\n const res = await api(\"/api/chats\").catch(() => null);\n go.disabled = false;\n if (!res || !res.ok) { err.textContent = \"invalid token\"; return; }\n sessionStorage.setItem(KEY, token);\n enter();\n};\ndocument.getElementById(\"logout\").onclick = () => {\n sessionStorage.removeItem(KEY); token = \"\"; active = null; chatsCache = [];\n if (es) { es.close(); es = null; }\n document.body.classList.remove(\"authed\", \"chat-open\");\n document.getElementById(\"token\").value = \"\";\n};\n\nfunction enter() { document.body.classList.add(\"authed\"); loadChats(); openStream(); }\nfunction openStream() {\n if (es || !window.EventSource) return;\n es = new EventSource(BASE + \"/api/stream?token=\" + encodeURIComponent(token));\n es.addEventListener(\"record\", (ev) => {\n const e = JSON.parse(ev.data);\n if (active && e.chatId === active) openChat(active, true);\n loadChats();\n });\n es.onerror = () => {};\n}\nsetInterval(() => { if (token) (active ? openChat(active, true) : loadChats()); }, 8000);\n\nfunction displayName(c) {\n const full = [c.firstName, c.lastName].filter(Boolean).join(\" \").trim();\n return full || (c.username ? \"@\" + c.username : c.name || \"chat \" + c.id);\n}\nfunction handleName(c) {\n if (c.username) return \"@\" + c.username;\n const name = displayName(c);\n return c.name && c.name !== name ? c.name : \"\";\n}\nfunction avatar(c, small) {\n const a = el(\"div\", \"avatar\" + (small ? \" small\" : \"\"));\n a.append(icon(\"user\"));\n return a;\n}\nfunction time(ts) {\n if (!ts) return \"\";\n const d = new Date(ts * 1000);\n return d.toLocaleTimeString([], { hour:\"2-digit\", minute:\"2-digit\" });\n}\nconst isPlaceholder = (t) => /^\\[[a-z_]+\\]$/.test(t || \"\");\nconst placeholderKind = (t) => { const m = /^\\[([a-z_]+)\\]$/.exec(t || \"\"); return m && m[1]; };\nfunction mediaIcon(type) {\n if (type === \"photo\") return \"image\";\n if (type === \"video\" || type === \"animation\" || type === \"video_note\") return \"video\";\n if (type === \"voice\") return \"mic\";\n if (type === \"audio\") return \"audio\";\n if (type === \"sticker\") return \"sticker\";\n return \"file\";\n}\nfunction mediaLabel(type) { return String(type || \"media\").replace(/_/g, \" \"); }\nfunction eventIcon(type) { return type === \"callback\" ? \"callback\" : \"event\"; }\n\nasync function loadChats() {\n const res = await api(\"/api/chats\"); if (!res.ok) return;\n chatsCache = await res.json();\n const box = document.getElementById(\"chat-list\");\n box.innerHTML = \"\";\n for (const c of chatsCache) box.append(chatRow(c));\n}\nfunction chatRow(c) {\n const row = el(\"button\", \"chat\" + (c.id === active ? \" on\" : \"\")); row.type = \"button\";\n row.append(avatar(c));\n const body = el(\"div\", \"chat-body\");\n const line = el(\"div\", \"chat-line\");\n line.append(el(\"div\", \"chat-name\", displayName(c)), el(\"div\", \"chat-time\", time(c.lastDate)));\n body.append(line);\n const handle = handleName(c); if (handle) body.append(el(\"div\", \"chat-handle\", handle));\n const preview = el(\"div\", \"preview\");\n const kind = c.lastAttachmentType || placeholderKind(c.lastText);\n if (c.lastEventType) {\n preview.append(icon(eventIcon(c.lastEventType)), el(\"span\", \"preview-label\", \"event\"), el(\"span\", \"preview-text\", c.lastText));\n } else if (kind) {\n const caption = isPlaceholder(c.lastText) ? \"\" : c.lastText;\n preview.append(icon(mediaIcon(kind)), el(\"span\", \"preview-label\", mediaLabel(kind)), el(\"span\", \"preview-text\", caption || \"media message\"));\n } else {\n preview.append(el(\"span\", \"preview-text\", c.lastText || \"message\"));\n }\n body.append(preview);\n row.append(body);\n row.onclick = () => openChat(c.id);\n return row;\n}\nfunction chatById(id) { return chatsCache.find((c) => c.id === id) || { id, name:\"chat \" + id, lastText:\"\", lastDate:0 }; }\n\nconst fileSrc = (att) => BASE + \"/api/file?id=\" + encodeURIComponent(att.fileId) + \"&token=\" + encodeURIComponent(token);\nfunction attEl(att, caption) {\n const src = fileSrc(att);\n if (att.type === \"photo\" || att.type === \"sticker\") {\n const btn = el(\"button\", \"media-frame image\"); btn.type = \"button\";\n const img = el(\"img\"); img.src = src; img.loading = \"lazy\"; img.alt = att.type;\n btn.append(img, el(\"span\", \"expand\")); btn.querySelector(\".expand\").append(icon(\"search\"));\n btn.onclick = () => openViewer(att, caption);\n return btn;\n }\n if (att.type === \"video\" || att.type === \"animation\" || att.type === \"video_note\") {\n const box = el(\"div\", \"video-card\");\n const v = el(\"video\"); v.src = src; v.controls = true; v.preload = \"metadata\";\n const badge = el(\"div\", \"video-badge\"); badge.append(icon(\"video\"), el(\"span\", null, mediaLabel(att.type)));\n const expand = el(\"button\", \"expand\"); expand.type = \"button\"; expand.append(icon(\"search\")); expand.onclick = () => openViewer(att, caption);\n box.append(v, badge, expand);\n return box;\n }\n if (att.type === \"voice\") {\n const card = el(\"div\", \"voice-card\");\n const mark = el(\"div\", \"voice-mark\"); mark.append(icon(\"mic\"));\n const wave = el(\"div\", \"wave\"); for (let i = 0; i < 6; i++) wave.append(el(\"i\"));\n const a = el(\"audio\"); a.src = src; a.controls = true; a.preload = \"metadata\";\n card.append(mark, wave, a);\n return card;\n }\n if (att.type === \"audio\") {\n const card = el(\"div\", \"audio-card\");\n const mark = el(\"div\", \"voice-mark\"); mark.append(icon(\"audio\"));\n const a = el(\"audio\"); a.src = src; a.controls = true; a.preload = \"metadata\";\n card.append(mark, a);\n return card;\n }\n const link = el(\"a\", \"doc-card\"); link.href = src; link.target = \"_blank\"; link.rel = \"noreferrer\";\n const mark = el(\"div\", \"doc-mark\"); mark.append(icon(\"file\"));\n const meta = el(\"div\"); meta.append(el(\"div\", \"doc-title\", att.fileName || mediaLabel(att.type)), el(\"div\", \"doc-sub\", att.mimeType || \"telegram file\"));\n link.append(mark, meta);\n return link;\n}\nfunction keyboardEl(k) {\n const box = el(\"div\", \"keyboard\");\n for (const row of k.rows || []) {\n const r = el(\"div\", \"keyboard-row\");\n for (const b of row) {\n const node = b.url ? el(\"a\", \"key\") : el(\"div\", \"key\");\n if (b.url) { node.href = b.url; node.target = \"_blank\"; node.rel = \"noreferrer\"; }\n node.append(icon(b.kind === \"url\" ? \"link\" : \"keyboard\"), el(\"span\", null, b.text));\n r.append(node);\n }\n box.append(r);\n }\n return box;\n}\nfunction eventBubble(m) {\n const e = el(\"div\", \"eventline\");\n e.append(icon(eventIcon(m.event.type)), el(\"span\", \"event-title\", m.event.title));\n if (m.event.detail) e.append(el(\"span\", null, m.event.detail));\n return e;\n}\nfunction bubble(m) {\n if (m.event) return eventBubble(m);\n const wrap = el(\"div\", \"msg \" + m.direction);\n const card = el(\"div\", \"bubble\");\n const atts = m.attachments || [];\n if (m.mediaGroupId && atts.length) {\n const grid = el(\"div\", \"album-grid\");\n for (const a of atts) grid.append(attEl(a, m.text));\n card.append(grid);\n } else {\n for (const a of atts) card.append(attEl(a, m.text));\n }\n if (m.text && !(atts.length && isPlaceholder(m.text))) card.append(el(\"div\", atts.length ? \"cap\" : \"cap\", m.text));\n if (m.keyboard) card.append(keyboardEl(m.keyboard));\n wrap.append(card, el(\"div\", \"time\", time(m.date)));\n return wrap;\n}\nfunction addToAlbum(node, m) {\n const grid = node.querySelector(\".album-grid\"); if (!grid) return;\n for (const a of m.attachments || []) grid.append(attEl(a, m.text));\n if (m.text && !isPlaceholder(m.text)) node.querySelector(\".bubble\").append(el(\"div\", \"cap\", m.text));\n}\nfunction renderMsgs(msgs) {\n const out = [];\n let group = null, groupId = null;\n for (const m of msgs) {\n if (!m.event && m.mediaGroupId && m.mediaGroupId === groupId && group) { addToAlbum(group, m); continue; }\n const b = bubble(m);\n if (!m.event && m.mediaGroupId) { group = b; groupId = m.mediaGroupId; }\n else { group = null; groupId = null; }\n out.push(b);\n }\n return out;\n}\n\nasync function openChat(id, keepScroll) {\n active = id; document.body.classList.add(\"chat-open\");\n if (!keepScroll) loadChats();\n const res = await api(\"/api/chats/\" + id + \"?limit=200\"); if (!res.ok) return;\n const msgs = await res.json(); oldest = msgs.length ? msgs[0].date : null;\n const c = chatById(id);\n const main = document.getElementById(\"main\");\n const prevTop = keepScroll ? (main.querySelector(\"#log\")?.scrollTop ?? null) : null;\n main.innerHTML = \"\";\n\n const head = el(\"div\", \"thread-head\");\n const back = el(\"button\", \"ghost back\"); back.type = \"button\"; back.append(icon(\"back\")); back.onclick = () => { document.body.classList.remove(\"chat-open\"); };\n const meta = el(\"div\", \"meta\"); meta.append(el(\"div\", \"name\", displayName(c)), el(\"div\", \"sub\", handleName(c) || \"private chat\"));\n head.append(back, avatar(c, true), meta);\n const log = el(\"div\"); log.id = \"log\";\n if (msgs.length >= 200) { const more = el(\"button\", \"more\", \"load earlier\"); more.onclick = () => loadEarlier(id, log); log.append(more); }\n for (const b of renderMsgs(msgs)) log.append(b);\n\n const form = el(\"form\"); form.id = \"composer\";\n const fileInput = el(\"input\"); fileInput.type = \"file\"; fileInput.style.display = \"none\";\n const attach = el(\"button\", \"attach\"); attach.type = \"button\"; attach.title = \"send a file\"; attach.append(icon(\"attach\")); attach.onclick = () => fileInput.click();\n const input = el(\"input\", \"text\"); input.placeholder = \"reply...\"; input.autocomplete = \"off\";\n const btn = el(\"button\", \"send\"); btn.type = \"submit\"; btn.append(icon(\"send\"), el(\"span\", null, \"send\"));\n form.append(attach, fileInput, input, btn);\n\n fileInput.onchange = async () => {\n const file = fileInput.files && fileInput.files[0]; if (!file) return;\n const fd = new FormData(); fd.append(\"file\", file);\n if (input.value.trim()) fd.append(\"caption\", input.value.trim());\n input.value = \"\"; fileInput.value = \"\";\n await api(\"/api/chats/\" + id + \"/send\", { method: \"POST\", body: fd });\n openChat(id);\n };\n form.onsubmit = async (e) => {\n e.preventDefault();\n const text = input.value.trim(); if (!text) return;\n input.value = \"\";\n await api(\"/api/chats/\" + id + \"/send\", { method:\"POST\", headers:{\"content-type\":\"application/json\"}, body: JSON.stringify({ text }) });\n openChat(id);\n };\n main.append(head, log, form);\n log.scrollTop = prevTop != null ? prevTop : log.scrollHeight;\n}\nasync function loadEarlier(id, log) {\n if (oldest == null) return;\n const res = await api(\"/api/chats/\" + id + \"?limit=200&before=\" + oldest); if (!res.ok) return;\n const older = await res.json();\n if (!older.length) { log.querySelector(\".more\")?.remove(); return; }\n oldest = older[0].date;\n const anchor = log.querySelector(\".more\")?.nextSibling ?? log.firstChild;\n const frag = document.createDocumentFragment();\n if (older.length >= 200) { const more = el(\"button\", \"more\", \"load earlier\"); more.onclick = () => loadEarlier(id, log); frag.append(more); }\n for (const b of renderMsgs(older)) frag.append(b);\n log.querySelector(\".more\")?.remove(); log.insertBefore(frag, anchor);\n}\n\nfunction openViewer(att, caption) {\n const viewer = document.getElementById(\"viewer\"), frame = viewer.querySelector(\".viewer-frame\"), cap = viewer.querySelector(\".viewer-caption\");\n frame.innerHTML = \"\"; cap.textContent = caption && !isPlaceholder(caption) ? caption : \"\";\n const src = fileSrc(att);\n if (att.type === \"video\" || att.type === \"animation\" || att.type === \"video_note\") {\n const v = el(\"video\"); v.src = src; v.controls = true; v.autoplay = true; frame.append(v);\n } else {\n const img = el(\"img\"); img.src = src; img.alt = att.type; frame.append(img);\n }\n viewer.hidden = false;\n}\nfunction closeViewer() { document.getElementById(\"viewer\").hidden = true; }\ndocument.querySelector(\".viewer-close\").onclick = closeViewer;\ndocument.getElementById(\"viewer\").onclick = (e) => { if (e.target.id === \"viewer\") closeViewer(); };\ndocument.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") closeViewer(); });\n\nif (token) enter();\n</script>\n</body>\n</html>";
|
|
3
3
|
//# sourceMappingURL=panel-html.d.ts.map
|
package/lib/panel-html.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panel-html.d.ts","sourceRoot":"","sources":["../src/panel-html.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"panel-html.d.ts","sourceRoot":"","sources":["../src/panel-html.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,eAAO,MAAM,UAAU,09mCAqhBf,CAAC"}
|