Spaces:
Running
Running
trajectory reader: page
Browse files- index.html +609 -17
index.html
CHANGED
|
@@ -1,19 +1,611 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 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 |
+
<title>Trajectory Reader</title>
|
| 7 |
+
<!--
|
| 8 |
+
Trajectory Reader — render the `messages` list of any Hugging Face dataset row.
|
| 9 |
+
|
| 10 |
+
A single static file. No server, no bundled data, no bundled token.
|
| 11 |
+
Open it in a browser (double-click, or `python3 -m http.server`), paste YOUR
|
| 12 |
+
HF token, pick a dataset + split, and page through rows. Every request goes
|
| 13 |
+
straight from your browser to huggingface.co with your token, so the Hub's own
|
| 14 |
+
permission check applies: someone without access to a private dataset gets
|
| 15 |
+
401/403 and sees nothing. The token is kept only in this browser's
|
| 16 |
+
localStorage ("忘记 token" clears it). Share the file freely; never share a
|
| 17 |
+
token.
|
| 18 |
+
|
| 19 |
+
Data path: datasets-server.huggingface.co /splits + /rows (paged, no download).
|
| 20 |
+
Fallback when the dataset isn't processed by the viewer backend: stream the
|
| 21 |
+
raw .jsonl via /resolve and split lines client-side.
|
| 22 |
+
-->
|
| 23 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 24 |
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
| 25 |
+
<style>
|
| 26 |
+
:root{
|
| 27 |
+
--bg:#F6F5F1; --panel:#FFFFFF; --ink:#1B1A17; --muted:#6E6A60; --line:#DDD9CF; --line-soft:#ECE9E1;
|
| 28 |
+
--accent:#0F8A78; --accent-ink:#0B6B5D; --danger:#B4373A;
|
| 29 |
+
--user:#3D5AD6; --assistant:#0F8A78; --tool:#B0700E; --system:#7A766C; --reason:#7A5AB8;
|
| 30 |
+
--user-bg:#EEF1FC; --assistant-bg:#EAF5F2; --tool-bg:#FBF3E4; --system-bg:#F0EFEA; --reason-bg:#F3EFFA;
|
| 31 |
+
--code-bg:#F1EFE9; --shadow:0 1px 2px rgba(27,26,23,.06);
|
| 32 |
+
--sans:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;
|
| 33 |
+
--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
| 34 |
+
}
|
| 35 |
+
@media (prefers-color-scheme: dark){
|
| 36 |
+
:root:not([data-theme="light"]){
|
| 37 |
+
--bg:#17181C; --panel:#1F2126; --ink:#ECEAE3; --muted:#9A968C; --line:#2E3038; --line-soft:#272930;
|
| 38 |
+
--accent:#3FB8A4; --accent-ink:#6FD3C2; --danger:#E06B6E;
|
| 39 |
+
--user:#8C9DF0; --assistant:#4FC2AD; --tool:#E0A54A; --system:#A5A197; --reason:#B39BE6;
|
| 40 |
+
--user-bg:#20253A; --assistant-bg:#1B2E2B; --tool-bg:#302819; --system-bg:#24262B; --reason-bg:#2A2436;
|
| 41 |
+
--code-bg:#15161A; --shadow:none;
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
:root[data-theme="dark"]{
|
| 45 |
+
--bg:#17181C; --panel:#1F2126; --ink:#ECEAE3; --muted:#9A968C; --line:#2E3038; --line-soft:#272930;
|
| 46 |
+
--accent:#3FB8A4; --accent-ink:#6FD3C2; --danger:#E06B6E;
|
| 47 |
+
--user:#8C9DF0; --assistant:#4FC2AD; --tool:#E0A54A; --system:#A5A197; --reason:#B39BE6;
|
| 48 |
+
--user-bg:#20253A; --assistant-bg:#1B2E2B; --tool-bg:#302819; --system-bg:#24262B; --reason-bg:#2A2436;
|
| 49 |
+
--code-bg:#15161A; --shadow:none;
|
| 50 |
+
}
|
| 51 |
+
*{box-sizing:border-box}
|
| 52 |
+
[hidden]{display:none!important}
|
| 53 |
+
html,body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 var(--sans)}
|
| 54 |
+
button,input,select{font:inherit;color:inherit}
|
| 55 |
+
a{color:var(--accent-ink)}
|
| 56 |
+
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
| 57 |
+
@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}
|
| 58 |
+
|
| 59 |
+
/* ---------- toolbar ---------- */
|
| 60 |
+
.bar{position:sticky;top:0;z-index:5;background:var(--panel);border-bottom:1px solid var(--line);padding-block:10px;padding-inline:20px}
|
| 61 |
+
.bar-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;max-width:1400px;margin:0 auto}
|
| 62 |
+
.brand{font-weight:600;letter-spacing:.01em;margin-right:6px;white-space:nowrap}
|
| 63 |
+
.brand small{color:var(--muted);font-weight:400;margin-left:6px}
|
| 64 |
+
.field{display:flex;align-items:center;gap:6px}
|
| 65 |
+
.field label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap}
|
| 66 |
+
input[type=text],input[type=password],input[type=number],select{
|
| 67 |
+
background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:6px 9px;min-width:0}
|
| 68 |
+
input[type=text]{width:300px;max-width:100%}
|
| 69 |
+
input[type=password]{width:210px}
|
| 70 |
+
input[type=number]{width:84px;font-variant-numeric:tabular-nums}
|
| 71 |
+
select{max-width:260px}
|
| 72 |
+
.btn{background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:6px 11px;cursor:pointer;white-space:nowrap}
|
| 73 |
+
.btn:hover{border-color:var(--accent)}
|
| 74 |
+
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
| 75 |
+
.btn.primary:hover{filter:brightness(1.05)}
|
| 76 |
+
.btn.quiet{border-color:transparent;color:var(--muted)}
|
| 77 |
+
.btn:disabled{opacity:.45;cursor:default}
|
| 78 |
+
.btn.hf{background:#FFD21E;border-color:#E8BE10;color:#1B1A17;font-weight:600}
|
| 79 |
+
.btn.hf::before{content:"🤗";margin-right:6px}
|
| 80 |
+
.who-name{color:var(--muted);font-size:13px;white-space:nowrap}
|
| 81 |
+
.who-name b{color:var(--ink);font-weight:500}
|
| 82 |
+
.nav{display:flex;align-items:center;gap:6px;margin-left:auto}
|
| 83 |
+
.nav .count{color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
|
| 84 |
+
.status{max-width:1400px;margin:8px auto 0;font-size:12.5px;color:var(--muted);min-height:1.2em;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
| 85 |
+
.status.err{color:var(--danger)}
|
| 86 |
+
.progress{height:3px;background:var(--line-soft);border-radius:2px;flex:1 1 160px;overflow:hidden}
|
| 87 |
+
.progress i{display:block;height:100%;width:0;background:var(--accent);transition:width .2s}
|
| 88 |
+
|
| 89 |
+
/* ---------- layout ---------- */
|
| 90 |
+
.wrap{max-width:1400px;margin:0 auto;padding-block:18px;padding-inline:20px;display:grid;grid-template-columns:300px minmax(0,1fr);gap:22px}
|
| 91 |
+
@media (max-width:900px){.wrap{grid-template-columns:1fr}}
|
| 92 |
+
.rail{display:flex;flex-direction:column;gap:14px;align-self:start;position:sticky;top:74px;max-height:calc(100vh - 90px);overflow:auto}
|
| 93 |
+
@media (max-width:900px){.rail{position:static;max-height:none}}
|
| 94 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px 14px;box-shadow:var(--shadow)}
|
| 95 |
+
.card h3{margin:0 0 8px;font-size:12px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);font-weight:600}
|
| 96 |
+
.kv{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 10px;font-size:13px}
|
| 97 |
+
.kv dt{color:var(--muted);white-space:nowrap}
|
| 98 |
+
.kv dd{margin:0;overflow-wrap:anywhere}
|
| 99 |
+
.kv dd.num{font-variant-numeric:tabular-nums}
|
| 100 |
+
.opts{display:flex;flex-direction:column;gap:6px;font-size:13px}
|
| 101 |
+
.opts label{display:flex;align-items:center;gap:8px;cursor:pointer}
|
| 102 |
+
details{border-top:1px solid var(--line-soft)}
|
| 103 |
+
details:first-of-type{border-top:0}
|
| 104 |
+
summary{cursor:pointer;padding:5px 0;color:var(--ink);list-style:none;display:flex;gap:8px;align-items:baseline}
|
| 105 |
+
summary::-webkit-details-marker{display:none}
|
| 106 |
+
summary::before{content:"▸";color:var(--muted);font-size:11px;flex:none}
|
| 107 |
+
details[open]>summary::before{content:"▾"}
|
| 108 |
+
summary .hint{color:var(--muted);font-size:12px;margin-left:auto;white-space:nowrap}
|
| 109 |
+
pre{margin:0;font:12.5px/1.5 var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;background:var(--code-bg);border-radius:6px;padding:8px 10px;max-height:420px;overflow:auto}
|
| 110 |
+
.rail pre{max-height:260px}
|
| 111 |
+
|
| 112 |
+
/* ---------- transcript ---------- */
|
| 113 |
+
.main{min-width:0;max-width:880px}
|
| 114 |
+
.empty{color:var(--muted);padding:40px 0;text-align:center}
|
| 115 |
+
.turn{display:grid;grid-template-columns:82px minmax(0,1fr);gap:12px;padding:12px 0;border-top:1px solid var(--line-soft)}
|
| 116 |
+
.turn:first-child{border-top:0}
|
| 117 |
+
.who{display:flex;flex-direction:column;gap:3px;align-items:flex-start}
|
| 118 |
+
.pill{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;padding:2px 8px;border-radius:999px;color:#fff;background:var(--system)}
|
| 119 |
+
.turn[data-role=user] .pill{background:var(--user)}
|
| 120 |
+
.turn[data-role=assistant] .pill{background:var(--assistant)}
|
| 121 |
+
.turn[data-role=tool] .pill{background:var(--tool)}
|
| 122 |
+
.who .n{color:var(--muted);font-size:11px;font-variant-numeric:tabular-nums}
|
| 123 |
+
.body{display:flex;flex-direction:column;gap:8px;min-width:0}
|
| 124 |
+
.bubble{border-radius:8px;padding:10px 12px;background:var(--system-bg);border:1px solid transparent}
|
| 125 |
+
.turn[data-role=user] .bubble.text{background:var(--user-bg)}
|
| 126 |
+
.turn[data-role=assistant] .bubble.text{background:var(--assistant-bg)}
|
| 127 |
+
.turn[data-role=tool] .bubble.text{background:var(--tool-bg)}
|
| 128 |
+
.text .t{white-space:pre-wrap;overflow-wrap:anywhere}
|
| 129 |
+
.reason{background:var(--reason-bg);border-left:3px solid var(--reason)}
|
| 130 |
+
.reason summary{color:var(--reason);font-weight:500}
|
| 131 |
+
.reason .t{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--ink);opacity:.9;padding:4px 0 2px}
|
| 132 |
+
.call{background:var(--tool-bg);border-left:3px solid var(--tool)}
|
| 133 |
+
.call .head{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap;font-family:var(--mono);font-size:12.5px}
|
| 134 |
+
.call .head b{color:var(--tool);font-weight:600}
|
| 135 |
+
.call .head .id{color:var(--muted);font-size:11px;margin-left:auto}
|
| 136 |
+
.call pre{margin-top:6px;background:transparent;padding:0;max-height:360px}
|
| 137 |
+
.meta{font-family:var(--mono);font-size:11.5px;color:var(--muted);display:flex;gap:10px;flex-wrap:wrap}
|
| 138 |
+
.more{margin-top:6px}
|
| 139 |
+
.more .btn{font-size:12px;padding:3px 9px}
|
| 140 |
+
.hidden-note{color:var(--muted);font-size:12px;padding:6px 0;border-top:1px dashed var(--line)}
|
| 141 |
+
.section-h{font-size:12px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);font-weight:600;margin:14px 0 4px}
|
| 142 |
+
.rawrow pre{max-height:70vh}
|
| 143 |
+
kbd{font:11px var(--mono);background:var(--code-bg);border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;padding:0 5px}
|
| 144 |
+
.help{color:var(--muted);font-size:12px}
|
| 145 |
+
.help kbd{margin:0 2px}
|
| 146 |
+
.tools-list{display:flex;flex-direction:column}
|
| 147 |
+
.tools-list summary b{font-family:var(--mono);font-weight:500}
|
| 148 |
+
.tools-list .d{color:var(--muted);font-size:12px;padding-bottom:6px}
|
| 149 |
+
</style>
|
| 150 |
+
</head>
|
| 151 |
+
<body>
|
| 152 |
+
|
| 153 |
+
<div class="bar">
|
| 154 |
+
<div class="bar-row">
|
| 155 |
+
<div class="brand">Trajectory Reader<small>HF messages</small></div>
|
| 156 |
+
<div class="field"><label for="repo">dataset</label><input id="repo" type="text" placeholder="org/name" spellcheck="false"></div>
|
| 157 |
+
<div class="field" id="authOauth" hidden>
|
| 158 |
+
<button id="signin" class="btn hf">Sign in with Hugging Face</button>
|
| 159 |
+
<span id="who" class="who-name" hidden></span>
|
| 160 |
+
<button id="signout" class="btn quiet" hidden>退出</button>
|
| 161 |
+
</div>
|
| 162 |
+
<div class="field" id="authToken"><label for="token">token</label><input id="token" type="password" placeholder="hf_… (只存本地)" autocomplete="off"><button id="forget" class="btn quiet" title="从这个浏览器删除 token">忘记</button></div>
|
| 163 |
+
<button id="load" class="btn primary">加载</button>
|
| 164 |
+
<div class="field"><label for="split">split</label><select id="split" disabled><option>—</option></select></div>
|
| 165 |
+
<div class="nav">
|
| 166 |
+
<button id="prev" class="btn" disabled title="上一行 (←)">‹</button>
|
| 167 |
+
<input id="idx" type="number" min="0" value="0" disabled>
|
| 168 |
+
<span class="count" id="count">/ —</span>
|
| 169 |
+
<button id="next" class="btn" disabled title="下一行 (→)">›</button>
|
| 170 |
+
<button id="rand" class="btn" disabled title="随机 (r)">随机</button>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
<div class="status" id="status"></div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<div class="wrap">
|
| 177 |
+
<aside class="rail">
|
| 178 |
+
<div class="card">
|
| 179 |
+
<h3>行信息</h3>
|
| 180 |
+
<dl class="kv" id="rowmeta"><dt>状态</dt><dd>尚未加载</dd></dl>
|
| 181 |
+
</div>
|
| 182 |
+
<div class="card">
|
| 183 |
+
<h3>显示</h3>
|
| 184 |
+
<div class="opts">
|
| 185 |
+
<label><input type="checkbox" id="optReason" checked> 默认展开 reasoning</label>
|
| 186 |
+
<label><input type="checkbox" id="optTools"> 隐藏 tool 返回</label>
|
| 187 |
+
<label><input type="checkbox" id="optSystem"> 隐藏 system</label>
|
| 188 |
+
<label><input type="checkbox" id="optRaw"> 显示整行原始 JSON</label>
|
| 189 |
+
</div>
|
| 190 |
+
<p class="help" style="margin:10px 0 0"><kbd>←</kbd><kbd>→</kbd> 翻行 · <kbd>r</kbd> 随机</p>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="card" id="toolsCard" hidden>
|
| 193 |
+
<h3>tools <span id="toolsN"></span></h3>
|
| 194 |
+
<div class="tools-list" id="toolsList"></div>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="card" id="colsCard" hidden>
|
| 197 |
+
<h3>其它列</h3>
|
| 198 |
+
<div id="cols"></div>
|
| 199 |
+
</div>
|
| 200 |
+
</aside>
|
| 201 |
+
|
| 202 |
+
<main class="main" id="main">
|
| 203 |
+
<div class="empty" id="intro">用 Hugging Face 账号登录(或粘贴你的 HF token),输入数据集名,点「加载」。<br>页面不带任何数据,所有请求从你的浏览器直连 huggingface.co,能看到什么由你的 HF 权限决定。</div>
|
| 204 |
+
</main>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<script>
|
| 208 |
+
(() => {
|
| 209 |
+
'use strict';
|
| 210 |
+
const DS = 'https://datasets-server.huggingface.co';
|
| 211 |
+
const HUB = 'https://huggingface.co';
|
| 212 |
+
const PAGE = 20; // rows per /rows request (server max 100)
|
| 213 |
+
const CLIP = 1800; // chars shown before "展开" on long blocks
|
| 214 |
+
const LS_TOKEN = 'trajreader.token', LS_REPO = 'trajreader.repo', LS_OAUTH = 'trajreader.oauth', SS_HASH = 'trajreader.pendingHash';
|
| 215 |
+
const HFJS = 'https://cdn.jsdelivr.net/npm/@huggingface/hub@2.17.1/+esm'; // OAuth PKCE helper (Space mode only)
|
| 216 |
+
const ORG_ID = '6a2a067c086133dad7d362e4'; // NewsBreak — preselects the org on the consent screen
|
| 217 |
+
|
| 218 |
+
const $ = id => document.getElementById(id);
|
| 219 |
+
const el = {
|
| 220 |
+
repo: $('repo'), token: $('token'), forget: $('forget'), load: $('load'), split: $('split'),
|
| 221 |
+
authOauth: $('authOauth'), authToken: $('authToken'), signin: $('signin'), signout: $('signout'), who: $('who'),
|
| 222 |
+
prev: $('prev'), next: $('next'), rand: $('rand'), idx: $('idx'), count: $('count'),
|
| 223 |
+
status: $('status'), rowmeta: $('rowmeta'), main: $('main'), cols: $('cols'), colsCard: $('colsCard'),
|
| 224 |
+
toolsCard: $('toolsCard'), toolsList: $('toolsList'), toolsN: $('toolsN'),
|
| 225 |
+
optReason: $('optReason'), optTools: $('optTools'), optSystem: $('optSystem'), optRaw: $('optRaw'),
|
| 226 |
+
};
|
| 227 |
+
|
| 228 |
+
const S = {
|
| 229 |
+
repo: '', splits: [], sel: null, total: null, idx: 0,
|
| 230 |
+
mode: 'server', // 'server' (datasets-server /rows) | 'raw' (stream .jsonl)
|
| 231 |
+
cache: new Map(), // row_idx -> row object
|
| 232 |
+
rawLines: null, // raw mode: array of parsed rows
|
| 233 |
+
files: [], // raw mode: candidate files
|
| 234 |
+
current: null, loadingRow: null, features: [], gen: 0,
|
| 235 |
+
};
|
| 236 |
+
|
| 237 |
+
/* ---------- storage (token stays in this browser only) ---------- */
|
| 238 |
+
const ls = {
|
| 239 |
+
get(k){ try { return localStorage.getItem(k) || ''; } catch { return ''; } },
|
| 240 |
+
set(k,v){ try { v ? localStorage.setItem(k,v) : localStorage.removeItem(k); } catch {} },
|
| 241 |
+
};
|
| 242 |
+
el.token.value = ls.get(LS_TOKEN);
|
| 243 |
+
el.repo.value = ls.get(LS_REPO) || 'NewsBreak/local-trajectories';
|
| 244 |
+
el.forget.onclick = () => { el.token.value=''; ls.set(LS_TOKEN,''); setStatus('已从这个浏览器删除 token'); };
|
| 245 |
+
|
| 246 |
+
/* ---------- auth: OAuth (inside a static Space) or manual token (local file) ---------- */
|
| 247 |
+
const auth = { mode: 'token', accessToken: '', user: '' };
|
| 248 |
+
const bearer = () => auth.mode === 'oauth' && auth.accessToken ? auth.accessToken : el.token.value.trim();
|
| 249 |
+
|
| 250 |
+
async function initAuth(){
|
| 251 |
+
const vars = (window.huggingface && window.huggingface.variables) || null;
|
| 252 |
+
if (!vars || !vars.OAUTH_CLIENT_ID) return; // not in a Space: manual token
|
| 253 |
+
auth.mode = 'oauth'; el.authOauth.hidden = false; el.authToken.hidden = true;
|
| 254 |
+
let hub;
|
| 255 |
+
try { hub = await import(HFJS); }
|
| 256 |
+
catch (e) { auth.mode = 'token'; el.authOauth.hidden = true; el.authToken.hidden = false; setStatus('OAuth 组件加载失败,改用手动 token:' + e.message, true); return; }
|
| 257 |
+
|
| 258 |
+
let res = null;
|
| 259 |
+
try { res = await hub.oauthHandleRedirectIfPresent(); }
|
| 260 |
+
catch (e) { setStatus('登录失败:' + e.message, true); }
|
| 261 |
+
if (res) {
|
| 262 |
+
ls.set(LS_OAUTH, JSON.stringify(res));
|
| 263 |
+
let pending = ''; try { pending = sessionStorage.getItem(SS_HASH) || ''; sessionStorage.removeItem(SS_HASH); } catch {}
|
| 264 |
+
history.replaceState(null, '', location.pathname + pending);
|
| 265 |
+
} else {
|
| 266 |
+
try { const saved = JSON.parse(ls.get(LS_OAUTH) || 'null'); if (saved && new Date(saved.accessTokenExpiresAt).getTime() > Date.now() + 60e3) res = saved; } catch {}
|
| 267 |
+
}
|
| 268 |
+
if (res) {
|
| 269 |
+
auth.accessToken = res.accessToken;
|
| 270 |
+
auth.user = (res.userInfo && (res.userInfo.preferred_username || res.userInfo.name)) || '';
|
| 271 |
+
el.signin.hidden = true; el.who.hidden = false; el.signout.hidden = false;
|
| 272 |
+
el.who.innerHTML = `已登录 <b>${esc(auth.user || 'HF user')}</b>`;
|
| 273 |
+
} else {
|
| 274 |
+
el.signin.hidden = false;
|
| 275 |
+
}
|
| 276 |
+
el.signin.onclick = async () => {
|
| 277 |
+
try {
|
| 278 |
+
try { sessionStorage.setItem(SS_HASH, location.hash); } catch {}
|
| 279 |
+
setStatus('跳转到 Hugging Face 登录…');
|
| 280 |
+
const url = (await hub.oauthLoginUrl({ redirectUrl: location.origin + location.pathname, scopes: vars.OAUTH_SCOPES || 'openid profile read-repos' })) + '&orgIds=' + ORG_ID;
|
| 281 |
+
if (window.self !== window.top) { window.open(url, '_blank'); setStatus('已在新标签页打开登录,登录后请在新标签页里继续使用'); }
|
| 282 |
+
else location.href = url;
|
| 283 |
+
} catch (e) { setStatus('无法发起登录:' + (e.message || e), true); }
|
| 284 |
+
};
|
| 285 |
+
el.signout.onclick = () => { ls.set(LS_OAUTH, ''); auth.accessToken = ''; location.reload(); };
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/* ---------- helpers ---------- */
|
| 289 |
+
const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 290 |
+
const fmtN = n => n == null ? '—' : Number(n).toLocaleString('en-US');
|
| 291 |
+
const tryJSON = v => { if (typeof v !== 'string') return v; const t=v.trim(); if (!(t.startsWith('{')||t.startsWith('['))) return v; try { return JSON.parse(t); } catch { return v; } };
|
| 292 |
+
const pretty = v => typeof v === 'string' ? v : JSON.stringify(v, null, 2);
|
| 293 |
+
const isObj = v => v && typeof v === 'object' && !Array.isArray(v);
|
| 294 |
+
|
| 295 |
+
function setStatus(msg, err=false, progress=null){
|
| 296 |
+
el.status.className = 'status' + (err ? ' err' : '');
|
| 297 |
+
el.status.innerHTML = esc(msg) + (progress != null ? `<span class="progress"><i style="width:${Math.round(progress*100)}%"></i></span>` : '');
|
| 298 |
+
}
|
| 299 |
+
class HttpError extends Error { constructor(status, body, url){ super(`HTTP ${status}`); this.status=status; this.body=body; this.url=url; } }
|
| 300 |
+
async function hfFetch(url, opts={}){
|
| 301 |
+
const h = Object.assign({}, opts.headers||{});
|
| 302 |
+
const tok = bearer();
|
| 303 |
+
if (tok) h.Authorization = 'Bearer ' + tok;
|
| 304 |
+
const r = await fetch(url, Object.assign({}, opts, {headers:h}));
|
| 305 |
+
if (!r.ok){ let b=''; try { b = await r.text(); } catch {} throw new HttpError(r.status, b, url); }
|
| 306 |
+
return r;
|
| 307 |
+
}
|
| 308 |
+
function explain(e){
|
| 309 |
+
if (e instanceof HttpError){
|
| 310 |
+
let detail=''; try { detail = JSON.parse(e.body).error || ''; } catch { detail = (e.body||'').slice(0,160); }
|
| 311 |
+
if (e.status===401) return auth.mode==='oauth' ? '401:请先登录 Hugging Face(登录可能已过期)。' : '401:需要 token(或 token 无效)。这个页面不带 token,请粘贴你自己的。';
|
| 312 |
+
if (e.status===403) return '403:你的账号没有访问该数据集的权限' + (auth.mode==='oauth' ? '(登录时要勾选授权 NewsBreak 组织)。' : '。');
|
| 313 |
+
if (e.status===404) return '404:数据集不存在,或 viewer 后端尚未处理。' + (detail?` (${detail})`:'');
|
| 314 |
+
return `${e.status}: ${detail || e.message}`;
|
| 315 |
+
}
|
| 316 |
+
if (e instanceof TypeError) return '网络/CORS 错误:' + e.message;
|
| 317 |
+
return e.message || String(e);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/* ---------- URL hash (shareable, never carries the token) ---------- */
|
| 321 |
+
function writeHash(){
|
| 322 |
+
if (!S.repo) return;
|
| 323 |
+
const p = new URLSearchParams({repo:S.repo});
|
| 324 |
+
if (S.sel) { p.set('config', S.sel.config); p.set('split', S.sel.split); }
|
| 325 |
+
if (S.mode==='raw' && S.sel) p.set('file', S.sel.file);
|
| 326 |
+
p.set('idx', S.idx);
|
| 327 |
+
history.replaceState(null, '', '#' + p.toString());
|
| 328 |
+
}
|
| 329 |
+
function readHash(){
|
| 330 |
+
const p = new URLSearchParams(location.hash.slice(1));
|
| 331 |
+
return { repo:p.get('repo'), config:p.get('config'), split:p.get('split'), file:p.get('file'), idx:parseInt(p.get('idx')||'0',10)||0 };
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/* ---------- dataset / splits ---------- */
|
| 335 |
+
async function loadDataset(want){
|
| 336 |
+
const repo = el.repo.value.trim().replace(/^datasets\//,'');
|
| 337 |
+
if (!repo) { setStatus('请输入数据集名,如 NewsBreak/local-trajectories', true); return; }
|
| 338 |
+
if (auth.mode==='oauth' && !auth.accessToken) { setStatus('请先点「Sign in with Hugging Face」登录', true); return; }
|
| 339 |
+
if (auth.mode==='token') ls.set(LS_TOKEN, el.token.value.trim());
|
| 340 |
+
ls.set(LS_REPO, repo);
|
| 341 |
+
const gen = ++S.gen; // a newer 加载 supersedes this one
|
| 342 |
+
Object.assign(S, {repo, splits:[], sel:null, total:null, idx:0, cache:new Map(), rawLines:null, files:[], current:null});
|
| 343 |
+
el.split.disabled = true; el.split.innerHTML = '<option>…</option>';
|
| 344 |
+
setStatus(`读取 ${repo} 的 splits…`);
|
| 345 |
+
try {
|
| 346 |
+
const r = await hfFetch(`${DS}/splits?dataset=${encodeURIComponent(repo)}`);
|
| 347 |
+
const j = await r.json();
|
| 348 |
+
S.mode = 'server';
|
| 349 |
+
S.splits = (j.splits||[]).map(s => ({config:s.config, split:s.split, label: (j.splits.some(o=>o.config!==s.config) ? `${s.config} / ` : '') + s.split}));
|
| 350 |
+
if (!S.splits.length) throw new Error('viewer 后端返回了 0 个 split');
|
| 351 |
+
} catch (e) {
|
| 352 |
+
// Fallback: list repo files and stream .jsonl directly.
|
| 353 |
+
setStatus(`viewer 后端不可用(${explain(e)}),改为直接读取仓库文件…`);
|
| 354 |
+
try {
|
| 355 |
+
const r = await hfFetch(`${HUB}/api/datasets/${repo}/tree/main?recursive=true`);
|
| 356 |
+
const files = (await r.json()).filter(f => f.type==='file' && /\.jsonl?$/i.test(f.path));
|
| 357 |
+
if (!files.length) throw new Error('仓库里没有 .jsonl / .json 文件');
|
| 358 |
+
S.mode = 'raw';
|
| 359 |
+
S.splits = files.map(f => ({config:'raw', split:f.path.replace(/\.jsonl?$/i,''), file:f.path, size:f.size, label:`${f.path} (${(f.size/1e6).toFixed(1)} MB)`}));
|
| 360 |
+
} catch (e2) { if (gen===S.gen) setStatus(explain(e2), true); return; }
|
| 361 |
+
}
|
| 362 |
+
if (gen !== S.gen) return;
|
| 363 |
+
el.split.innerHTML = S.splits.map((s,i)=>`<option value="${i}">${esc(s.label)}</option>`).join('');
|
| 364 |
+
el.split.disabled = false;
|
| 365 |
+
let pick = 0;
|
| 366 |
+
if (want){
|
| 367 |
+
const i = S.splits.findIndex(s => (want.file && s.file===want.file) || (s.split===want.split && (!want.config || s.config===want.config)));
|
| 368 |
+
if (i>=0) pick = i;
|
| 369 |
+
}
|
| 370 |
+
el.split.value = String(pick);
|
| 371 |
+
setStatus(`${repo} · ${S.splits.length} 个 split · ${S.mode==='server'?'viewer API':'直接读文件'}`);
|
| 372 |
+
await selectSplit(pick, want ? want.idx : 0);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
async function selectSplit(i, idx=0){
|
| 376 |
+
S.sel = S.splits[i]; S.cache = new Map(); S.rawLines = null; S.total = null; S.idx = idx;
|
| 377 |
+
el.count.textContent = '/ …';
|
| 378 |
+
for (const b of [el.prev, el.next, el.rand, el.idx]) b.disabled = false;
|
| 379 |
+
if (S.mode==='raw') await streamRaw(S.sel);
|
| 380 |
+
await showRow(idx);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
/* ---------- rows: datasets-server ---------- */
|
| 384 |
+
async function fetchPage(offset){
|
| 385 |
+
const {config, split} = S.sel;
|
| 386 |
+
const url = `${DS}/rows?dataset=${encodeURIComponent(S.repo)}&config=${encodeURIComponent(config)}&split=${encodeURIComponent(split)}&offset=${offset}&length=${PAGE}`;
|
| 387 |
+
const j = await (await hfFetch(url)).json();
|
| 388 |
+
S.total = j.num_rows_total; S.features = j.features || [];
|
| 389 |
+
for (const r of j.rows) S.cache.set(r.row_idx, {row:r.row, truncated:r.truncated_cells||[]});
|
| 390 |
+
}
|
| 391 |
+
async function getRow(idx){
|
| 392 |
+
if (S.mode==='raw') return S.rawLines[idx] ? {row:S.rawLines[idx], truncated:[]} : null;
|
| 393 |
+
if (!S.cache.has(idx)) await fetchPage(Math.floor(idx/PAGE)*PAGE);
|
| 394 |
+
return S.cache.get(idx) || null;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
/* ---------- rows: raw .jsonl streaming (fallback) ---------- */
|
| 398 |
+
async function streamRaw(sel){
|
| 399 |
+
const url = `${HUB}/datasets/${S.repo}/resolve/main/${sel.file}`;
|
| 400 |
+
setStatus(`下载 ${sel.file}…`, false, 0);
|
| 401 |
+
const r = await hfFetch(url);
|
| 402 |
+
const reader = r.body.getReader(); const dec = new TextDecoder();
|
| 403 |
+
const total = sel.size || Number(r.headers.get('content-length')) || 0;
|
| 404 |
+
let got = 0, buf = '', rows = [];
|
| 405 |
+
S.rawLines = rows;
|
| 406 |
+
for (;;) {
|
| 407 |
+
const {done, value} = await reader.read();
|
| 408 |
+
if (done) break;
|
| 409 |
+
got += value.length; buf += dec.decode(value, {stream:true});
|
| 410 |
+
let nl;
|
| 411 |
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
| 412 |
+
const line = buf.slice(0, nl).trim(); buf = buf.slice(nl+1);
|
| 413 |
+
if (line) { try { rows.push(JSON.parse(line)); } catch { rows.push({_parse_error:line.slice(0,200)}); } }
|
| 414 |
+
}
|
| 415 |
+
S.total = rows.length; el.count.textContent = `/ ${fmtN(rows.length)}…`;
|
| 416 |
+
setStatus(`下载 ${sel.file}… ${(got/1e6).toFixed(1)} MB,${fmtN(rows.length)} 行`, false, total ? got/total : null);
|
| 417 |
+
}
|
| 418 |
+
const tail = buf.trim(); if (tail) { try { rows.push(JSON.parse(tail)); } catch {} }
|
| 419 |
+
S.total = rows.length;
|
| 420 |
+
setStatus(`${sel.file} · ${fmtN(rows.length)} 行(已全部载入浏览器内存)`);
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
/* ---------- conversation detection & normalisation ---------- */
|
| 424 |
+
const CONV_KEYS = ['messages','conversations','conversation','chat','turns','trajectory','dialog','dialogue'];
|
| 425 |
+
const FROM_MAP = {human:'user', user:'user', gpt:'assistant', assistant:'assistant', model:'assistant', bot:'assistant',
|
| 426 |
+
system:'system', developer:'system', tool:'tool', observation:'tool', toolresult:'tool', tool_result:'tool', function:'tool', function_call:'assistant'};
|
| 427 |
+
const isMsgList = v => Array.isArray(v) && v.length>0 && v.every(m => isObj(m) && (('role' in m) || ('from' in m)));
|
| 428 |
+
|
| 429 |
+
function findConversation(row){
|
| 430 |
+
const keys = [...CONV_KEYS.filter(k=>k in row), ...Object.keys(row).filter(k=>!CONV_KEYS.includes(k))];
|
| 431 |
+
for (const k of keys){ const v = tryJSON(row[k]); if (isMsgList(v)) return {key:k, msgs:v}; }
|
| 432 |
+
// prompt/completion, chosen/rejected style
|
| 433 |
+
const sections = [];
|
| 434 |
+
for (const k of ['prompt','completion','chosen','rejected']) { const v = tryJSON(row[k]); if (isMsgList(v)) sections.push({key:k, msgs:v}); }
|
| 435 |
+
if (sections.length) return {key:sections.map(s=>s.key).join(' + '), sections};
|
| 436 |
+
return null;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
function normMsg(m){
|
| 440 |
+
const role = (m.role || FROM_MAP[String(m.from||'').toLowerCase()] || m.from || 'unknown').toLowerCase();
|
| 441 |
+
let content = m.content ?? m.value ?? m.text ?? '';
|
| 442 |
+
let reasoning = m.reasoning ?? m.reasoning_content ?? m.reasoningContent ?? m.thinking ?? m.thought ?? '';
|
| 443 |
+
let calls = m.tool_calls ?? m.toolCalls ?? m.function_call ?? null;
|
| 444 |
+
if (calls && !Array.isArray(calls)) calls = [calls];
|
| 445 |
+
calls = calls ? calls.slice() : [];
|
| 446 |
+
// Anthropic-style content parts
|
| 447 |
+
if (Array.isArray(content)){
|
| 448 |
+
const texts = [];
|
| 449 |
+
for (const p of content){
|
| 450 |
+
if (!isObj(p)) { texts.push(String(p)); continue; }
|
| 451 |
+
const t = p.type;
|
| 452 |
+
if (t==='text' || t==='output_text' || t==='input_text') texts.push(p.text ?? '');
|
| 453 |
+
else if (t==='thinking' || t==='reasoning') reasoning = (reasoning ? reasoning+'\n\n' : '') + (p.thinking ?? p.text ?? '');
|
| 454 |
+
else if (t==='tool_use' || t==='toolCall' || t==='tool_call' || t==='function_call') calls.push(p);
|
| 455 |
+
else if (t==='tool_result' || t==='toolResult') texts.push(typeof p.content==='string' ? p.content : pretty(p.content));
|
| 456 |
+
else if (t==='image' || t==='image_url') texts.push('[image]');
|
| 457 |
+
else texts.push(pretty(p));
|
| 458 |
+
}
|
| 459 |
+
content = texts.join('\n');
|
| 460 |
+
} else if (content != null && typeof content !== 'string') content = pretty(content);
|
| 461 |
+
if (!reasoning && m.thinking_blocks) reasoning = (Array.isArray(m.thinking_blocks)?m.thinking_blocks:[m.thinking_blocks]).map(b => isObj(b) ? (b.thinking ?? b.text ?? '') : String(b)).filter(Boolean).join('\n\n');
|
| 462 |
+
// <think>…</think> embedded in content
|
| 463 |
+
if (!reasoning && typeof content==='string'){
|
| 464 |
+
const mt = content.match(/^\s*<think>([\s\S]*?)<\/think>\s*/);
|
| 465 |
+
if (mt) { reasoning = mt[1].trim(); content = content.slice(mt[0].length); }
|
| 466 |
+
}
|
| 467 |
+
const toolCalls = calls.map(c => {
|
| 468 |
+
const f = c.function || {};
|
| 469 |
+
let args = c.arguments ?? f.arguments ?? c.args ?? c.input ?? c.parameters ?? null;
|
| 470 |
+
args = tryJSON(args);
|
| 471 |
+
return { id: c.id ?? c.tool_call_id ?? c.call_id ?? '', name: c.name ?? f.name ?? c.tool ?? '(unnamed)', args, type: c.type };
|
| 472 |
+
});
|
| 473 |
+
return { role, content: String(content ?? ''), reasoning: String(reasoning ?? ''), toolCalls,
|
| 474 |
+
toolCallId: m.tool_call_id ?? m.toolCallId ?? m.call_id ?? '', name: m.name ?? m.toolName ?? m.tool_name ?? '', raw: m };
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
/* ---------- rendering ---------- */
|
| 478 |
+
function clipBlock(text, cls='t'){
|
| 479 |
+
const s = String(text);
|
| 480 |
+
if (s.length <= CLIP) return `<div class="${cls}">${esc(s)}</div>`;
|
| 481 |
+
const id = 'c' + Math.random().toString(36).slice(2,9);
|
| 482 |
+
return `<div class="${cls}" id="${id}" data-full="${esc(s)}">${esc(s.slice(0,CLIP))}<span class="muted">…</span></div>
|
| 483 |
+
<div class="more"><button class="btn" data-expand="${id}">展开全部 (${fmtN(s.length)} 字符)</button></div>`;
|
| 484 |
+
}
|
| 485 |
+
function renderTurn(m, i, opts){
|
| 486 |
+
const parts = [];
|
| 487 |
+
if (m.reasoning) parts.push(`<details class="bubble reason" ${opts.reason?'open':''}><summary>reasoning<span class="hint">${fmtN(m.reasoning.length)} 字符</span></summary>${clipBlock(m.reasoning)}</details>`);
|
| 488 |
+
if (m.content.trim()) parts.push(`<div class="bubble text">${clipBlock(m.content)}</div>`);
|
| 489 |
+
for (const c of m.toolCalls){
|
| 490 |
+
parts.push(`<div class="bubble call"><div class="head"><span>→</span><b>${esc(c.name)}</b>${c.id?`<span class="id">${esc(c.id)}</span>`:''}</div>${c.args!=null ? `<pre>${esc(pretty(c.args))}</pre>` : ''}</div>`);
|
| 491 |
+
}
|
| 492 |
+
const meta = [];
|
| 493 |
+
if (m.role==='tool' && (m.name||m.toolCallId)) meta.push(`${m.name?esc(m.name):''}${m.toolCallId?` ← ${esc(m.toolCallId)}`:''}`);
|
| 494 |
+
if (!parts.length) parts.push(`<div class="bubble text"><span class="help">(空消息)</span></div>`);
|
| 495 |
+
return `<section class="turn" data-role="${esc(m.role)}"><div class="who"><span class="pill">${esc(m.role)}</span><span class="n">#${i}</span></div>
|
| 496 |
+
<div class="body">${meta.length?`<div class="meta">${meta.join(' ')}</div>`:''}${parts.join('')}</div></section>`;
|
| 497 |
+
}
|
| 498 |
+
function renderMessages(msgs, opts){
|
| 499 |
+
let hidden = 0, out = [];
|
| 500 |
+
msgs.forEach((raw, i) => {
|
| 501 |
+
const m = normMsg(raw);
|
| 502 |
+
if ((opts.hideTools && m.role==='tool') || (opts.hideSystem && m.role==='system')) { hidden++; return; }
|
| 503 |
+
if (hidden) { out.push(`<div class="hidden-note">已隐藏 ${hidden} 条</div>`); hidden = 0; }
|
| 504 |
+
out.push(renderTurn(m, i, opts));
|
| 505 |
+
});
|
| 506 |
+
if (hidden) out.push(`<div class="hidden-note">已隐藏 ${hidden} 条</div>`);
|
| 507 |
+
return out.join('');
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
function renderRail(row, convKey, truncated){
|
| 511 |
+
// row meta
|
| 512 |
+
const conv = convKey ? tryJSON(row[convKey]) : null;
|
| 513 |
+
const n = Array.isArray(conv) ? conv.length : null;
|
| 514 |
+
const roles = Array.isArray(conv) ? conv.reduce((a,m)=>{ const r=normMsg(m).role; a[r]=(a[r]||0)+1; return a; },{}) : null;
|
| 515 |
+
const kv = [
|
| 516 |
+
['行', `<span class="num">${fmtN(S.idx)}</span> / ${fmtN(S.total)}`],
|
| 517 |
+
['split', esc(S.sel.label)],
|
| 518 |
+
['来源', S.mode==='server' ? 'viewer API(分页)' : '原始 .jsonl'],
|
| 519 |
+
];
|
| 520 |
+
if (convKey) kv.push(['messages 列', `<code>${esc(convKey)}</code>`]);
|
| 521 |
+
if (n != null) kv.push(['轮次', `${n}` + (roles ? ` <span class="help">(${Object.entries(roles).map(([r,c])=>`${r} ${c}`).join(', ')})</span>` : '')]);
|
| 522 |
+
if (truncated.length) kv.push(['⚠ 截断', `viewer API 截断了:${truncated.map(esc).join(', ')}`]);
|
| 523 |
+
el.rowmeta.innerHTML = kv.map(([k,v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('');
|
| 524 |
+
|
| 525 |
+
// tools
|
| 526 |
+
const tools = tryJSON(row.tools);
|
| 527 |
+
if (Array.isArray(tools) && tools.length){
|
| 528 |
+
el.toolsCard.hidden = false; el.toolsN.textContent = `(${tools.length})`;
|
| 529 |
+
el.toolsList.innerHTML = tools.map(t => {
|
| 530 |
+
const f = t.function || t; const name = f.name || t.name || '(unnamed)'; const d = f.description || t.description || '';
|
| 531 |
+
return `<details><summary><b>${esc(name)}</b></summary>${d?`<div class="d">${esc(d)}</div>`:''}<pre>${esc(pretty(f.parameters || f.input_schema || t))}</pre></details>`;
|
| 532 |
+
}).join('');
|
| 533 |
+
} else el.toolsCard.hidden = true;
|
| 534 |
+
|
| 535 |
+
// other columns
|
| 536 |
+
const skip = new Set([convKey, 'tools']);
|
| 537 |
+
const cols = Object.keys(row).filter(k => !skip.has(k));
|
| 538 |
+
el.colsCard.hidden = !cols.length;
|
| 539 |
+
el.cols.innerHTML = cols.map(k => {
|
| 540 |
+
const v = row[k]; const p = tryJSON(v);
|
| 541 |
+
if (v == null) return `<details><summary>${esc(k)}<span class="hint">null</span></summary></details>`;
|
| 542 |
+
if (typeof p === 'object') return `<details><summary>${esc(k)}<span class="hint">${Array.isArray(p)?`array[${p.length}]`:'object'}</span></summary><pre>${esc(pretty(p))}</pre></details>`;
|
| 543 |
+
const s = String(v);
|
| 544 |
+
if (s.length <= 80 && !s.includes('\n')) return `<details><summary>${esc(k)}<span class="hint" style="max-width:55%;overflow:hidden;text-overflow:ellipsis">${esc(s)}</span></summary></details>`;
|
| 545 |
+
return `<details><summary>${esc(k)}<span class="hint">${fmtN(s.length)} 字符</span></summary><pre>${esc(s)}</pre></details>`;
|
| 546 |
+
}).join('');
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
function paint(){
|
| 550 |
+
const cur = S.current; if (!cur) return;
|
| 551 |
+
const {row, truncated} = cur;
|
| 552 |
+
const opts = { reason: el.optReason.checked, hideTools: el.optTools.checked, hideSystem: el.optSystem.checked };
|
| 553 |
+
const conv = findConversation(row);
|
| 554 |
+
renderRail(row, conv && !conv.sections ? conv.key : null, truncated);
|
| 555 |
+
let html = '';
|
| 556 |
+
if (el.optRaw.checked) html += `<div class="rawrow"><div class="section-h">原始行 JSON</div><pre>${esc(JSON.stringify(row, (k,v)=>tryJSON(v), 2))}</pre></div>`;
|
| 557 |
+
if (!conv) html += `<div class="empty">这一行没有可识别的 messages 列(找 role/from 的对象数组)。打开「显示整行原始 JSON」看结构。</div>`;
|
| 558 |
+
else if (conv.sections) html += conv.sections.map(s => `<div class="section-h">${esc(s.key)}</div>${renderMessages(s.msgs, opts)}`).join('');
|
| 559 |
+
else html += renderMessages(conv.msgs, opts);
|
| 560 |
+
el.main.innerHTML = html;
|
| 561 |
+
window.scrollTo({top:0});
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
async function showRow(idx){
|
| 565 |
+
if (S.total != null) idx = Math.max(0, Math.min(idx, S.total-1));
|
| 566 |
+
idx = Math.max(0, idx|0);
|
| 567 |
+
S.idx = idx; el.idx.value = idx; S.loadingRow = idx;
|
| 568 |
+
try {
|
| 569 |
+
const r = await getRow(idx);
|
| 570 |
+
if (S.loadingRow !== idx) return; // superseded by a newer navigation
|
| 571 |
+
if (!r) { setStatus(`第 ${idx} 行不存在`, true); return; }
|
| 572 |
+
S.current = r; el.count.textContent = `/ ${fmtN(S.total)}`;
|
| 573 |
+
el.prev.disabled = idx<=0; el.next.disabled = S.total!=null && idx>=S.total-1;
|
| 574 |
+
paint(); writeHash();
|
| 575 |
+
if (S.mode==='server') setStatus(`${S.repo} · ${S.sel.label} · 第 ${fmtN(idx)} 行`);
|
| 576 |
+
} catch (e) { setStatus(explain(e), true); }
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
/* ---------- events ---------- */
|
| 580 |
+
el.load.onclick = () => loadDataset(null);
|
| 581 |
+
el.repo.addEventListener('keydown', e => { if (e.key==='Enter') loadDataset(null); });
|
| 582 |
+
el.token.addEventListener('keydown', e => { if (e.key==='Enter') loadDataset(null); });
|
| 583 |
+
el.split.onchange = () => selectSplit(parseInt(el.split.value,10), 0);
|
| 584 |
+
el.prev.onclick = () => showRow(S.idx-1);
|
| 585 |
+
el.next.onclick = () => showRow(S.idx+1);
|
| 586 |
+
el.rand.onclick = () => { if (S.total) showRow(Math.floor(Math.random()*S.total)); };
|
| 587 |
+
el.idx.addEventListener('change', () => showRow(parseInt(el.idx.value,10)||0));
|
| 588 |
+
for (const o of [el.optReason, el.optTools, el.optSystem, el.optRaw]) o.addEventListener('change', paint);
|
| 589 |
+
el.main.addEventListener('click', e => {
|
| 590 |
+
const b = e.target.closest('[data-expand]'); if (!b) return;
|
| 591 |
+
const box = document.getElementById(b.dataset.expand); if (!box) return;
|
| 592 |
+
box.textContent = box.dataset.full; b.parentElement.remove();
|
| 593 |
+
});
|
| 594 |
+
document.addEventListener('keydown', e => {
|
| 595 |
+
if (e.target.matches('input,select,textarea') || !S.sel) return;
|
| 596 |
+
if (e.key==='ArrowLeft') { e.preventDefault(); el.prev.click(); }
|
| 597 |
+
else if (e.key==='ArrowRight') { e.preventDefault(); el.next.click(); }
|
| 598 |
+
else if (e.key==='r') el.rand.click();
|
| 599 |
+
});
|
| 600 |
+
|
| 601 |
+
/* ---------- boot: resolve auth, then restore from hash if we can already authenticate ---------- */
|
| 602 |
+
(async () => {
|
| 603 |
+
await initAuth();
|
| 604 |
+
const h = readHash();
|
| 605 |
+
if (h.repo) el.repo.value = h.repo;
|
| 606 |
+
if (h.repo && bearer()) loadDataset(h);
|
| 607 |
+
})();
|
| 608 |
+
})();
|
| 609 |
+
</script>
|
| 610 |
+
</body>
|
| 611 |
</html>
|