feat(ui): AITokenPool static HTML UI prototype (marketplace + enterprise + login) - #7
Merged
Merged
Conversation
纯 HTML+CSS+JS 原型,无框架/无构建/无外部依赖,覆盖: - 公共版:仪表盘 / 模型市场(筛选排序)/ 共享管理(上架表单)/ 钱包 / 交易记录 / 设置 - 企业版:企业管理台(Key 池/员工/用量报表/组织)/ 员工自助面板 - 登录页(邮箱 + 企业 SSO 占位) 深色主题 + 强调色 #4ecdc4;mock 数据折算自 data/models.example.json(1 USD = 1000 点)。 ui/index.html 双击即可浏览;node --check + DOM 冒烟测试通过。
- 移除登录页/侧边栏的公共版·企业版模式切换(login-mode-switch / toggle-mode) - 移除独立 Employee Portal 页面;员工即普通用户,用同一套界面 - Admin 收敛为管理员角色视图(Key 池 / 成员管理 / 用量报表 / 组织设置,含'关闭外部注册'企业部署开关) - 单一登录入口,角色由账号决定 - 更新 README 说明'一套产品、两种部署场景、角色区分'模型
宿主 rant 2026-08-14T12:19:07:企业单点登录(SSO)暂不考虑, 从 UI 原型移除 SSO 按钮、事件绑定与相关文案。
宿主 rant 2026-08-14T12:23:14:单价是模型×厂商的客观属性, 分享者只填声明额度;选择模型后自动显示参考单价(输出价点数/1M), 无定价数据时给出默认价兜底。种子数据 SHARINGS price 同步为自动值。
宿主 rant 2026-08-14T12:25:20 + 12:26:19: - 上架表单增加 API Key(password)必填校验,平台加密托管,列表仅脱敏展示(前3后4) - 共享列表每行增加删除按钮(confirm 确认后彻底下架,暂停态也可删,统计随之刷新) - 保留自动单价逻辑,无手工单价残留
宿主 rant 2026-08-14T12:28:10:共享页默认只显示统计与我的共享列表; 新增'+ 添加 / 上架新 key'按钮展开表单,提交成功或取消后自动收起。 既有 API Key 输入、自动单价、删除功能不变。
宿主 rant 2026-08-14T12:30:50:纯静态原型无外部依赖,实现轻量
MRT 风格表格渲染器 buildDataTable({columns, rows, state, onState}):
- 列排序:点表头升/降切换(▲/▼),Shift+点击叠加多列排序
- 列筛选:文本模糊 / 下拉(状态、类型)/ 数字范围(点数)
- 分页 + 每页行数切换(5/10/25/50),排序/筛选状态跨页保留
- 交易页 Tab 与列筛选叠加生效;钱包明细与交易记录共用渲染器
4 tasks
This was referenced Sep 21, 2026
argszero
added a commit
that referenced
this pull request
Sep 22, 2026
) The transactions table's column headers carry a sort arrow, and that arrow is a claim about the WHOLE dataset: the pager below it counts the backend's `total` (30 rows, "1 2 3 / 3 · 30 rows") and the summary card above it is a backend aggregate. The ordering, however, only ever applied to the page in hand. `#135` turned the list into a server-paged table (`serverPaging` = backend total, one page of rows per request, `LIMIT/OFFSET` over `ORDER BY t.id DESC`), while sorting stayed where it was born in the static prototype (`#7`, `d70e032`): client-side, over whatever rows the caller passed in -- if (state.sort.length) { data = data.slice().sort(...) } const pageRows = serverPaging ? data : data.slice(...) a page. So clicking "Points" ▲ reordered the ten rows on screen and painted ▲, while the row that actually holds the minimum sat on page 2/3 and was never fetched; the request never changed. The column FILTERS travelled the other half of that road long ago (`#232` `e7bfc6f` / C2114: "local filter of the current page" -> "backend full-dataset filter"); the sort half never did. Fix: one claim, three carriers, and a server that renders `ORDER BY` from a whitelist. * `ui/js/app.js` — `txSortParams()` projects the single sort state (`txTable.sort`) into the LIST request only (the trend endpoint buckets by time; row order means nothing to it). `txQuerySig()` now covers the sort, so the reload guard refetches when a header is clicked -- without that line the arrow would move over a list that does not, which is worse than the defect being fixed. The local sort steps aside when the call site declares `serverSort`: `if (state.sort.length && !(serverPaging && serverSort))`. * `src/routes/wallet.rs` — `TX_SORT_KEYS` (11 keys, exactly the sortable columns of `TX_COLUMNS`) + `tx_sort_expr()` + `tx_order_by()`. The user string never reaches SQL; the `ORDER BY` fragment is rendered from the whitelist, always with a trailing `, t.id DESC` (a non-unique sort makes `LIMIT/OFFSET` page boundaries indeterminate -- one row twice, another never), and an unknown key / mismatched key-direction count / bad direction is a 400. Validation runs before the DB lock, as `type` does. Expressions match what the cell shows (`pts` via `signed_pts_expr`, the four token columns on their raw values, `model`/`key` byte-identical to the filter expressions). Tests: * `state_gate::the_sort_indicator_and_the_order_by_share_one_source` — four rules, each with its own mutant leg, plus a `the_r164_rules_have_teeth` self-proof over self-contained mini sources and a `the_r164_roster_is_real` positive control. Roster == whitelist == match arms, `q.sort`/`q.dir` bound in exactly one place, the guard reads the WHOLE condition (a `[^)]*` reader cannot see `!(a && b)`) and a control rule keeps the filter half of the same road honest. * `src/routes/wallet.rs` — three behaviour tests: the whole set is ordered (not the page), ties are stable across pages, and anything outside the whitelist is rejected. * jsdom probe (7 legs, both directions): on the pre-fix tree the two global-extremum legs and the "did the request carry sort" leg fail; on the landed bytes all seven pass. The two competing "fixes" (keep sorting the page and caption the arrow " (this page)"; stop painting the arrow) are rejected by the same legs. Scope, stated honestly: the gate is lexical -- it proves the three carriers agree and that the whitelist IS the column roster; it does not prove that the rows on screen are globally ordered (that is the jsdom probe's job, and there is no JS runner in `cargo test`), nor the `ORDER BY` semantics themselves (those are the wallet behaviour tests). Recorded in `ui/README.md`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Static HTML UI prototype for AITokenPool — the first product UI draft, serving as the visual & interaction baseline for the upcoming Rust backend + web frontend.
Pure HTML + CSS + JS. No frameworks, no build step, no external CDN dependencies (inline SVG/emoji icons, system fonts). Open
ui/index.htmlin a browser to browse the whole prototype.Rant reference:
2026-08-13T21:43:47— AITokenPool UI 设计与静态 HTML 原型.Related Issue
None (host rant instruction, no issue filed).
Changes
#4ecdc4; desktop-first responsive layout with narrow-screen adaptations pre-reserveddata/models.example.json(points rule: 1 USD = 1,000 points; CNY converted at ~7.2 for display)ui/README.mdexplains how to open/browse the prototypeTests
node --checkon both JS files (syntax)cargo test— not applicable: no Rust code touched (pure frontend prototype)Checklist
feat/ui-prototype)Model correction (v2, rant 2026-08-13T22:01:37)
Per host review: the initial version incorrectly modeled Public and Enterprise as two different feature sets. The correct product model is one product, two deployment scenarios — role is a permission difference, not a product difference.
Changes applied on top of v1 (same branch, PR auto-updated):
ui/README.mdupdated to state the corrected model explicitlyAcceptance per rant: no public/enterprise mode selection anywhere; same UI with Marketplace / Sharing / Wallet / Transactions / Settings + admin role view; no standalone Employee Portal; single login; README documents the model.
Update (v3, rant 2026-08-14T12:19:07)
Per host instruction: enterprise SSO is out of scope for now. Removed all SSO placeholders from the prototype (same branch, PR auto-updated):
ui/index.html: removed the "企业 SSO 单点登录 (SSO)" button (sso-btn) and its dividerui/js/app.js: removed thesso-btnclick handler (placeholder toast)ui/README.md: login page described as single email-login entry onlyThe single-login-entry model is unchanged (role decided by account). Login method is TBD; the prototype keeps only the email login placeholder. Verified:
grep -i 'sso\|单点登录' ui/returns nothing.Update (v4, rant 2026-08-14T12:23:14)
Per host instruction: the unit price is not entered by the sharer — it is an objective attribute of model × provider, auto-computed by the platform from the model price table. Changes on the same branch (PR auto-updated):
ui/index.html: removed the manual "单价(点数 / 1M 输出)" input (sf-price); replaced with a read-only "参考单价" display (sf-price-view)ui/js/app.js:sf-price; price = auto-computed from the model's output price (points / 1M tokens)SHARINGSrecords use the auto-computed priceui/js/data.js: seedSHARINGSprices updated to match the auto-computed output pricesui/css/style.css:.price-viewread-only display styleui/README.md: documents that price is auto-computed by the platform; sharer only fills the declared quotaVerified: no
sf-priceinput remains (only thesf-price-viewdisplay); auto-price matches model output prices; unknown model falls back to the default (300); JS syntax check passes.Update (v5, rants 2026-08-14T12:25:20 + 2026-08-14T12:26:19)
Per host instructions — sharing form & list enhancements (same branch, PR auto-updated):
1. API Key input for listing (rant 12:25:20)
ui/index.html: added a required API Key password input (sf-key, placeholdersk-xxx) to the share form, with helper text: the key is encrypted-hosted by the platform, used only for proxying calls, never shown to other usersui/js/app.js: submit validates the key is non-empty (toast otherwise, no submit); new listings store thekeyfield; the share list displays it masked (sk-****1234, first 3 + last 4); success toast mentions "key 已加密托管"ui/js/data.js: seedSHARINGSrecords now include masked keysui/css/style.css:.form-hinthelper style2. Delete / permanent delist (rant 12:26:19)
window.confirm), then removes the record fromSHARINGSand refreshes the list + stats.btn-dangerstyle addedThe auto-computed unit price from the previous update is preserved — no manual price input remains. Verified:
sf-keyrequired + validated; masked display; delete with confirm; noid="sf-price"residual; JS syntax checks pass.Update (v6, rant 2026-08-14T12:28:10)
Per host instruction — the share form should not be open by default (same branch, PR auto-updated):
ui/index.html: the "上架新 key" form card is nowhiddenby default; added a "+ 添加 / 上架新 key" button above the list; added a 取消 (Cancel) button next to the submit button inside the formui/js/app.js: clicking the add button expands the form (and focuses the API Key field); the form collapses again after a successful submit or on cancelAll previous changes (API Key input + encrypted hosting note, auto-computed unit price, masked key display, delete/delist with confirm) are preserved. Verified: form card hidden by default; add/cancel/submit-hide wiring present; no manual price input; JS syntax checks pass.
Update (v7, rant 2026-08-14T12:30:50)
Per host instruction — upgrade the ledger (Wallet) and Transactions tables to an MRT-style data table (sort / column filter / pagination / page-size switch) without external dependencies (same branch, PR auto-updated):
buildDataTable({ columns, rows, state, onState })inui/js/app.js; Wallet ledger and Transactions share itui/README.mdmentions the table capabilitiesVerified: 15/15 functional checks pass (render, text/select/range filters, single & multi-key sort, pagination, page-size, state restore), no stale
wallet-body/tx-bodyreferences,node --checkpasses.