Skip to content

feat(ui): AITokenPool static HTML UI prototype (marketplace + enterprise + login) - #7

Merged
argszero merged 7 commits into
mainfrom
feat/ui-prototype
Aug 14, 2026
Merged

argszero merged 7 commits into
mainfrom
feat/ui-prototype

Conversation

@argszero

@argszero argszero commented Aug 13, 2026

Copy link
Copy Markdown
Owner

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.html in 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

  • Public edition (6 pages): Dashboard (balance / monthly usage / earnings / recent trades), Marketplace (browse, search, provider filter, price/context sort), Sharing (list-a-key form with working submit, my-listings with pause/resume/relist), Wallet (balance, top-up/withdraw placeholders, paged ledger), Transactions (consume/earn tabs + pagination), Settings (account, API key management incl. generate, notifications, preferences)
  • Enterprise edition (2 pages): Admin (key pool management add/revoke, employee quota adjustment, usage reports by model/employee, org settings) and Employee Portal (my points, available models, quota request placeholder)
  • Login page: unified entry (email login + enterprise SSO placeholder, market/enterprise mode switch before login)
  • Dark-first theme with restrained palette, brand accent #4ecdc4; desktop-first responsive layout with narrow-screen adaptations pre-reserved
  • Inline mock data derived from data/models.example.json (points rule: 1 USD = 1,000 points; CNY converted at ~7.2 for display)
  • ui/README.md explains how to open/browse the prototype

Tests

  • node --check on both JS files (syntax)
  • DOM-stub smoke test covering: login/logout, mode toggle, marketplace search/filter/sort, share-form submit (list grows), API key generate, admin add-key, quota request — no exceptions
  • All local asset references verified present
  • cargo test — not applicable: no Rust code touched (pure frontend prototype)

Checklist

  • Branch naming follows convention (feat/ui-prototype)
  • Commit message uses Conventional Commits format
  • Single purpose, minimal change set

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):

  • Removed the public/enterprise mode switch from both the login page and the sidebar
  • Removed the standalone Employee Portal page — employees are ordinary users on the same UI
  • Admin is now a role view (key pool / member quota / usage reports / org settings, incl. a "close external registration" enterprise-deployment toggle), not a separate edition
  • Single login entry; what you see after login is decided by your account role
  • ui/README.md updated to state the corrected model explicitly

Acceptance 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 divider
  • ui/js/app.js: removed the sso-btn click handler (placeholder toast)
  • ui/README.md: login page described as single email-login entry only

The 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:
    • submit no longer reads sf-price; price = auto-computed from the model's output price (points / 1M tokens)
    • selecting a model shows "参考单价:XX 点 / 1M 输出(自动)" (fallback "按默认价" with a default if the model has no pricing data — never errors)
    • SHARINGS records use the auto-computed price
  • ui/js/data.js: seed SHARINGS prices updated to match the auto-computed output prices
  • ui/css/style.css: .price-view read-only display style
  • ui/README.md: documents that price is auto-computed by the platform; sharer only fills the declared quota

Verified: no sf-price input remains (only the sf-price-view display); 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, placeholder sk-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 users
  • ui/js/app.js: submit validates the key is non-empty (toast otherwise, no submit); new listings store the key field; the share list displays it masked (sk-****1234, first 3 + last 4); success toast mentions "key 已加密托管"
  • ui/js/data.js: seed SHARINGS records now include masked keys
  • ui/css/style.css: .form-hint helper style

2. Delete / permanent delist (rant 12:26:19)

  • Each share list row now has a red 删除 button (distinct from the ghost pause/resume toggle)
  • Deleting asks for confirmation (window.confirm), then removes the record from SHARINGS and refreshes the list + stats
  • Semantics: delete = permanent removal (key delisted from the platform, not recoverable), distinct from pause (temporary, recoverable); paused keys can also be deleted
  • .btn-danger style added

The auto-computed unit price from the previous update is preserved — no manual price input remains. Verified: sf-key required + validated; masked display; delete with confirm; no id="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 now hidden by default; added a "+ 添加 / 上架新 key" button above the list; added a 取消 (Cancel) button next to the submit button inside the form
  • ui/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 cancel
  • Default view shows only the stats + "我的共享列表" — form appears only on demand

All 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):

  • Implemented a lightweight generic table renderer buildDataTable({ columns, rows, state, onState }) in ui/js/app.js; Wallet ledger and Transactions share it
  • Column sorting: click a header to toggle asc/desc (▲/▼ indicator); Shift+click stacks multiple sort keys
  • Column filters: text columns → fuzzy text input; type/status columns → dropdown; points column → min/max number range; filtering applies immediately
  • Pagination: existing pagination kept + per-page size switch (5/10/25/50)
  • Sort/filter/page state lives in page-level variables and survives view switches
  • The Transactions page keeps its 全部/消费/收益 tabs; tab selection stacks with column filters
  • ui/README.md mentions the table capabilities
  • Minor consistency fix: the Wallet "类型" column now shows Chinese labels (same as Transactions) so its dropdown filter matches displayed values

Verified: 15/15 functional checks pass (render, text/select/range filters, single & multi-key sort, pagination, page-size, state restore), no stale wallet-body/tx-body references, node --check passes.

argszero and others added 7 commits August 13, 2026 21:57
纯 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 与列筛选叠加生效;钱包明细与交易记录共用渲染器
@argszero
argszero merged commit d70e032 into main Aug 14, 2026
@argszero
argszero deleted the feat/ui-prototype branch August 14, 2026 06:15
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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant