feat(frontend): adopt UI v0.8.0 and refine form feedback

Unify theme namespaces and plugin theme delivery, improve validation and empty states, and pin UI v0.8.0 plus plugin workbench v0.9.0.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-11-01 01:40:09 +08:00
co-authored by Codex
parent fbdfe22757
commit 3cbebb5f9b
262 changed files with 2763 additions and 2576 deletions
+41 -36
View File
@@ -90,12 +90,12 @@
```mermaid
flowchart LR
A[Pinia 持久化配置] --> B[resolveTheme]
A[Pinia 持久化配置] --> B[createTheme]
B --> C[Seed Token]
C --> D[Map Token]
D --> E[Alias Token]
E --> F[Component Token]
F --> G[#cp-runtime-theme]
F --> G[#z-runtime-theme]
G --> H[Vue / Tailwind / ECharts]
B --> I[Theme Editor 草稿]
@@ -119,7 +119,7 @@ flowchart LR
[SDK 页面与宿主桥](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#页面宿主桥-v2)。
`theme/` 根目录只保留公开入口 `index.ts` 和唯一类型文件 `types.ts`;内部实现按 `core/`、`derive/`、`runtime/` 分层,不增加嵌套 barrel。
纯派生模块不访问 DOM,`theme/runtime/browser.ts` 不包含派生规则,Theme Store 不复制算法。
普通 Map 字段按 camelCase → kebab-case 统一生成 `--cp-*`;Semantic 与 Preset Color 仅维护各自的短角色表。
普通 Map 字段按 camelCase → kebab-case 统一生成 `--z-*`;Semantic 与 Preset Color 仅维护各自的短角色表。
`ThemeTokenName` 在 `types.ts` 中由 Map 契约推导,无需手写重复的联合类型和对象映射
下文未带宿主路径的 `theme/`、`styles/` 均指 `codex-proxy-ui/src/` 下的目录,联调入口见[开发文档](development.md#源码联调)
@@ -215,20 +215,20 @@ Quaternary;主按钮白字与功能色文字遵循各自的容器配对规则
### Alias Token
Alias 按视觉角色命名,使用 `--cp-` 命名空间;整体分层对齐 Ant Design,并以 Container / On Container 表达成对的
Alias 按视觉角色命名,使用 `--z-` 命名空间;整体分层对齐 Ant Design,并以 Container / On Container 表达成对的
语义表面与前景:
| 角色 | CSS Token | 主要消费者 |
| --- | --- | --- |
| 品牌交互 | `--cp-color-primary*` | 主操作、选中态、焦点与强调 |
| 链接 | `--cp-color-link*` | 普通文本链接 |
| 表面 | `--cp-color-bg-*` | 页面、容器、浮层和遮罩 |
| 填充 | `--cp-color-fill-*` | 轨道、弱背景和静态层级 |
| 文字 | `--cp-color-text*` | 标题、正文和辅助信息 |
| 选择 | `--cp-control-item-bg-active*` | Segmented、Select 和选择控件 |
| 焦点 | `--cp-control-outline` | 键盘焦点和输入反馈 |
| 功能色 | `--cp-color-info / success / warning / error-*` | 系统反馈与状态 |
| 预设彩色 | `--cp-color-{blue,cyan,green,orange,purple,red}-{container,container-strong,border,solid,text,on-container}` | 分类标签与数据强调 |
| 品牌交互 | `--z-color-primary*` | 主操作、选中态、焦点与强调 |
| 链接 | `--z-color-link*` | 普通文本链接 |
| 表面 | `--z-color-bg-*` | 页面、容器、浮层和遮罩 |
| 填充 | `--z-color-fill-*` | 轨道、弱背景和静态层级 |
| 文字 | `--z-color-text*` | 标题、正文和辅助信息 |
| 选择 | `--z-control-item-bg-active*` | Segmented、Select 和选择控件 |
| 焦点 | `--z-control-outline` | 键盘焦点和输入反馈 |
| 功能色 | `--z-color-info / success / warning / error-*` | 系统反馈与状态 |
| 预设彩色 | `--z-color-{blue,cyan,green,orange,purple,red}-{container,container-strong,border,solid,text,on-container}` | 分类标签与数据强调 |
Alias 不在页面中追加修色;中性与彩色文字在派生层执行各自的背景配对约束。Component
Token 直接覆盖时不会自动重算同组件的其他状态;需要保持梯度关系时应修改 Seed,而不是逐个覆盖 Map Token
@@ -239,16 +239,16 @@ Token 直接覆盖时不会自动重算同组件的其他状态;需要保持
| 组件 | 代表 Token |
| --- | --- |
| Button | `--cp-button-primary-color / bg / hover-bg / active-bg`、`--cp-button-secondary-bg / hover-bg / active-bg` |
| IconButton | `--cp-icon-button-secondary-bg / hover-bg / active-bg` |
| Input | `--cp-input-bg / hover-bg / active-bg / error-active-bg` 与对应 Shadow |
| Menu | `--cp-menu-item-selected-bg` |
| Popover | `--cp-popover-header-bg` |
| Table | `--cp-table-row-bg / stripe-bg / hover-bg / selected-bg / selected-hover-bg / height` |
| Card | `--cp-card-bg / border-radius / shadow` |
| Layout | `--cp-layout-sider-bg / shadow` |
| Scrollbar | `--cp-scrollbar-thumb-bg / hover-bg` |
| BrandMark | `--cp-brand-mark-bg` |
| Button | `--z-button-primary-color / bg / hover-bg / active-bg`、`--z-button-secondary-bg / hover-bg / active-bg` |
| IconButton | `--z-icon-button-secondary-bg / hover-bg / active-bg` |
| Input | `--z-input-bg / hover-bg / active-bg / error-active-bg` 与对应 Shadow |
| Menu | `--z-menu-item-selected-bg` |
| Popover | `--z-popover-header-bg` |
| Table | `--z-table-row-bg / stripe-bg / hover-bg / selected-bg / selected-hover-bg / height` |
| Card | `--z-card-bg / border-radius / shadow` |
| Layout | `--z-layout-sider-bg / shadow` |
| Scrollbar | `--z-scrollbar-thumb-bg / hover-bg` |
| BrandMark | `--z-brand-mark-bg` |
组件状态由 UI 库的 Component 派生层统一生成,页面不维护混色系数:
@@ -308,13 +308,17 @@ Theme Store 统一读取持久化配置并初始化主题,首个 Vue 组件渲
### CSS Variables 提交
主题内部和持久化配置使用 `color-primary`、`input-bg` 等语义 Token 名,不包含 CSS 前缀。
UI 构建插件 `CodexProxyUI({ themePrefix: 'z' })` 同时配置变量生成与样式引用,默认前缀为 `z`;
它与组件导出名、DOM 类名和 utility 类名独立,修改后需重新构建。配置方式见 UI 的[主题定制](../modules/ui/playground/guide/theme.md#自定义主题前缀)
全局主题由 `<head>` 中唯一的运行时样式节点承载:
```html
<style id="cp-runtime-theme">
<style id="z-runtime-theme">
:root[data-theme='dark'][data-theme-color='relay-blue'] {
color-scheme: dark;
--cp-color-bg-layout: #0B111C;
--z-color-bg-layout: #0B111C;
/* 其余 Map、Alias 与 Component Token */
}
</style>
@@ -324,14 +328,15 @@ Theme Store 统一读取持久化配置并初始化主题,首个 Vue 组件渲
全局弹窗通过 Teleport 挂到 `body` 后仍继承根变量
> [!NOTE]
> Theme Editor 预览是例外:草稿 Token 以内联变量写在影子环境的局部根节点上,仅影响预览,不污染已保存主题
> Theme Editor 预览是例外:草稿 Token 经 `createThemeVars()` 转换后以内联变量写在影子环境的局部根节点上,仅影响预览,不污染已保存主题
### 切换与图表
- 用户主动切换时,以点击位置为圆心运行 View Transition;不支持时使用 180ms 颜色过渡
- 初始化、系统主题变化或减少动态效果时直接提交,不播放扩散动画
- 每次有效提交只增加一次 `themeRevision`,相同签名不会重复刷新
- `useThemeColor()` 在正式页面读取根 CSS Variables,在影子预览中优先读取注入的局部 Token
- `useThemeColor()` 接收语义 Token 名,通过 `getThemeVarName()` 读取当前前缀下的 CSS Variables;影子预览优先读取注入的局部变量
- 插件桥接传递语义 Token,由插件构建元数据指定的前缀生成 CSS 变量,宿主与插件可独立配置
- 图表 Option 在主题 revision 或预览 Token 改变后重算,`BaseChart` 通过 `setOption` 更新现有 Canvas
## 主题编辑器
@@ -381,24 +386,24 @@ Store 在其他入口发生变化时更新保存基线;没有未保存修改
### CSS Token
- 全局 Token:`--cp-color-bg-container`、`--cp-font-size`
- Component Token:`--cp-table-row-hover-bg`、`--cp-input-active-shadow`
- Preset Color Token:`--cp-color-purple-container-strong`、`--cp-color-cyan-on-container`
- 全局 Token:`--z-color-bg-container`、`--z-font-size`
- Component Token:`--z-table-row-hover-bg`、`--z-input-active-shadow`
- Preset Color Token:`--z-color-purple-container-strong`、`--z-color-cyan-on-container`
- 业务组件按[通用颜色消费](#通用颜色消费)选择角色,不在主题层声明业务域名称
- Map 与 Component 字段由 `theme/core/tokens.ts` 统一生成 CSS Token;禁止在解析器中再写平行的逐项映射表
- Map 与 Component 字段由 `theme/core/tokens.ts` 统一生成语义 Token,运行时通过 `createThemeVars()` 输出 CSS 变量;禁止在解析器中再写平行的逐项映射表
- 不使用 `accent`、`soft`、`current`、`subtle` 等与现有角色重叠的平行词汇
### Vue 与 Tailwind CSS 4
组件优先使用 `bg-cp-*`、`text-cp-*`、`shadow-cp-*`、`rounded-cp-*` 等由 `@theme inline` 暴露的 utility:
组件优先使用 `bg-z-*`、`text-z-*`、`shadow-z-*`、`rounded-z-*` 等由 `@theme inline` 暴露的 utility:
```vue
<section class="rounded-cp-card bg-cp-bg-container text-cp-text shadow-cp-card">
<section class="rounded-z-card bg-z-bg-container text-z-text shadow-z-card">
...
</section>
```
仅当值需要在 CSS 函数、SVG 或局部派生中参与计算时,直接读取 `var(--cp-*)`。不在页面组件中重新实现色阶、
仅当值需要在 CSS 函数、SVG 或局部派生中参与计算时,CSS 中直接读取 `var(--z-*)`,JavaScript 中通过 `getThemeVarName()` 生成变量名。不在页面组件中重新实现色阶、
对比度或明暗算法
共享包的 `styles/tailwind.css` 是唯一 `@theme inline` 注册表,只注册 Tailwind 名称,不保存主题值。注册表按基础、排版、颜色、圆角、阴影、间距与尺寸排序;
@@ -426,8 +431,8 @@ utility / arbitrary variant;Vue Transition、跨浏览器 Range、动态富文
### 增加预置主题
1. 在 `ThemeColorPresetId` 增加稳定 ID
2. 在 `THEME_COLOR_PRESETS` 声明名称、描述与主色 Seed
3. 不新增页面特判,确认预置可通过同一 `resolveTheme()` 派生
2. 在 `themeColorPresets` 声明名称、描述与主色 Seed
3. 不新增页面特判,确认预置可通过同一 `createTheme()` 派生
4. 在浅色、深色和系统模式下检查文本、浮层、Input、表格与图表
### 增加全局 Seed 或 Alias
+1 -1
View File
@@ -17,7 +17,7 @@
},
"dependencies": {
"@boxicons/vue": "1.0.1",
"@codex-proxy/ui": "github:zyycn/codex-proxy-ui#v0.7.0",
"@codex-proxy/ui": "github:zyycn/codex-proxy-ui#v0.8.0",
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@lucide/vue": "^1.61.0",
+53 -6
View File
@@ -195,8 +195,8 @@ importers:
specifier: 1.0.1
version: 1.0.1(vue@3.5.44(typescript@6.0.3))
'@codex-proxy/ui':
specifier: github:zyycn/codex-proxy-ui#v0.7.0
version: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.5(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))
specifier: github:zyycn/codex-proxy-ui#v0.8.0
version: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/d6100447010715c1203e67a4195fc6d875ff6249(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(jiti@2.7.0)(postcss@8.5.30)(vite@8.3.5(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))(yaml@2.9.0)
'@fontsource-variable/inter':
specifier: ^5.3.0
version: 5.3.0
@@ -443,9 +443,9 @@ packages:
resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==}
engines: {node: '>= 20.12.0'}
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225':
resolution: {gitHosted: true, integrity: sha512-JJesyJ1Y/QcdXhBtmKX6k0GyUgOSxiV8sJrUymvLyKjC8NhHLmtpXWt/evZcjYBDyJEIRll85bmSmtMF9/Wpqw==, tarball: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225}
version: 0.7.0
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/d6100447010715c1203e67a4195fc6d875ff6249':
resolution: {gitHosted: true, integrity: sha512-XNPErDXqq9OxvgLgSb0gVRGmI9Yf+Vx0dTjoZKzaAaCOX9exSirU5kEr9mJBeuo9E1DDw8817xV0B5Od9zH+5A==, tarball: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/d6100447010715c1203e67a4195fc6d875ff6249}
version: 0.8.0
engines: {node: '>=24'}
peerDependencies:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -2618,6 +2618,10 @@ packages:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
lilconfig@3.1.4:
resolution: {integrity: sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==}
engines: {node: '>=14'}
local-pkg@1.2.1:
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
engines: {node: '>=14'}
@@ -2645,6 +2649,11 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
marked-alert@2.1.2:
resolution: {integrity: sha512-EFNRZ08d8L/iEIPLTlQMDjvwIsj03gxWCczYTht6DCiHJIZhMk4NK5gtPY9UqAYb09eV5VGT+jD4lp396E0I+w==}
peerDependencies:
marked: '>=7.0.0'
marked@18.1.0:
resolution: {integrity: sha512-PamYXWWWg2nboG3oX5Ffzpy3EFZROqZJK9hSlMmUnekp7TxPs5DcT95vwd46m73/exfKCRVMtgXoPMI4FEJL6w==}
engines: {node: '>= 20'}
@@ -2973,6 +2982,24 @@ packages:
pnpm-workspace-yaml@1.9.1:
resolution: {integrity: sha512-Xj/T2X6DNAWbtk/9UQ0/TJRswltiHZevmcMjqoL7WrpJi67lu3qaslU4+I+zJ8wtwqmuvAE0KkB8rbv2ZdogBg==}
postcss-load-config@6.0.1:
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
engines: {node: '>= 18'}
peerDependencies:
jiti: '>=1.21.0'
postcss: '>=8.0.9'
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
jiti:
optional: true
postcss:
optional: true
tsx:
optional: true
yaml:
optional: true
postcss-selector-parser@7.1.6:
resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==}
engines: {node: '>=4'}
@@ -3525,7 +3552,7 @@ snapshots:
fast-wrap-ansi: 0.2.2
sisteransi: 1.0.5
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.5(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))':
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/d6100447010715c1203e67a4195fc6d875ff6249(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(jiti@2.7.0)(postcss@8.5.30)(vite@8.3.5(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))(yaml@2.9.0)':
dependencies:
'@ant-design/colors': 8.0.1
'@ant-design/fast-color': 3.0.1
@@ -3538,6 +3565,8 @@ snapshots:
es-toolkit: 1.53.0
gsap: 3.15.0
marked: 18.1.0
marked-alert: 2.1.2(marked@18.1.0)
postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.30)(yaml@2.9.0)
sortablejs: 1.15.7
vue: 3.5.44(typescript@6.0.3)
optionalDependencies:
@@ -3550,10 +3579,14 @@ snapshots:
- focus-trap
- fuse.js
- idb-keyval
- jiti
- jwt-decode
- nprogress
- postcss
- qrcode
- tsx
- universal-cookie
- yaml
'@dprint/formatter@0.5.1': {}
@@ -5486,6 +5519,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0
lilconfig@3.1.4: {}
local-pkg@1.2.1:
dependencies:
mlly: 1.8.2
@@ -5514,6 +5549,10 @@ snapshots:
markdown-table@3.0.4: {}
marked-alert@2.1.2(marked@18.1.0):
dependencies:
marked: 18.1.0
marked@18.1.0: {}
math-intrinsics@1.1.0: {}
@@ -6053,6 +6092,14 @@ snapshots:
dependencies:
yaml: 2.9.0
postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.30)(yaml@2.9.0):
dependencies:
lilconfig: 3.1.4
optionalDependencies:
jiti: 2.7.0
postcss: 8.5.30
yaml: 2.9.0
postcss-selector-parser@7.1.6:
dependencies:
cssesc: 3.0.0
@@ -35,7 +35,7 @@ function updateGroup(groupId: string, selected: boolean) {
<div
v-for="group in groups"
:key="group.id"
class="flex min-h-11 items-center justify-between gap-3 rounded-cp bg-cp-fill-quaternary px-3.5 py-2.5"
class="flex min-h-11 items-center justify-between gap-3 rounded-z bg-z-fill-quaternary px-3.5 py-2.5"
>
<span class="flex min-w-0 items-center gap-2.5">
<span class="size-3.5 shrink-0 rounded-sm" :style="{ backgroundColor: group.color }" />
@@ -46,10 +46,10 @@ function updateGroup(groupId: string, selected: boolean) {
@update:model-value="updateGroup(group.id, $event)"
/>
</span>
<span v-if="!group.enabled" class="shrink-0 text-cp-xs font-emphasis text-cp-text-quaternary"> {{ t('common.status.disabled') }} </span>
<span v-if="!group.enabled" class="shrink-0 text-z-xs font-emphasis text-z-text-quaternary"> {{ t('common.status.disabled') }} </span>
</div>
</div>
<p v-else class="m-0 rounded-cp bg-cp-fill-quaternary px-3.5 py-3 text-cp-sm font-emphasis text-cp-text-secondary">
<p v-else class="m-0 rounded-z bg-z-fill-quaternary px-3.5 py-3 text-z-sm font-emphasis text-z-text-secondary">
{{ loading ? t('groups.selector.loading') : t('groups.selector.empty') }}
</p>
</template>
@@ -32,5 +32,5 @@ const fullNames = computed(() =>
:title="group.name"
/>
</div>
<span v-else class="text-xs text-cp-text-quaternary">—</span>
<span v-else class="text-xs text-z-text-quaternary">—</span>
</template>
@@ -79,10 +79,10 @@ function importToClient() {
/>
</div>
<section class="overflow-hidden rounded-cp-card bg-cp-fill-quaternary shadow-cp-tertiary">
<section class="overflow-hidden rounded-z-card bg-z-fill-quaternary shadow-z-tertiary">
<div class="flex items-center justify-between gap-3 px-4 py-2.5">
<span
class="min-w-0 truncate font-mono text-cp-sm font-emphasis text-cp-text-secondary"
class="min-w-0 truncate font-mono text-z-sm font-emphasis text-z-text-secondary"
>
{{ configPath }}
</span>
@@ -96,9 +96,9 @@ function importToClient() {
</ZIconButton>
</div>
<ZScrollbar max-height="calc(100dvh - 21rem)">
<div class="mx-3 mb-3 rounded-cp bg-cp-bg-container px-3.5 py-3 shadow-cp-tertiary">
<div class="mx-3 mb-3 rounded-z bg-z-bg-container px-3.5 py-3 shadow-z-tertiary">
<pre
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-cp-sm leading-[1.65] font-emphasis text-cp-text"
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-z-sm leading-[1.65] font-emphasis text-z-text"
v-text="codexConfig"
/>
</div>
+2 -2
View File
@@ -12,7 +12,7 @@ const mistGradientId = `${id}-brand-mist`
</script>
<template>
<svg :class="{ 'text-cp-text-light-solid': props.variant === 'tile' }" xmlns="http://www.w3.org/2000/svg" viewBox="48 48 416 416" focusable="false">
<svg :class="{ 'text-z-text-light-solid': props.variant === 'tile' }" xmlns="http://www.w3.org/2000/svg" viewBox="48 48 416 416" focusable="false">
<defs v-if="props.variant === 'tile'">
<filter :id="noiseFilterId" x="0" y="0" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="1.7" numOctaves="2" seed="31" result="noise" />
@@ -35,7 +35,7 @@ const mistGradientId = `${id}-brand-mist`
</radialGradient>
</defs>
<rect v-if="props.variant === 'tile'" class="fill-(--cp-brand-mark-bg)" x="48" y="48" width="416" height="416" rx="88" />
<rect v-if="props.variant === 'tile'" class="fill-(--z-brand-mark-bg)" x="48" y="48" width="416" height="416" rx="88" />
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="36">
<path d="M156 307A112 112 0 0 1 307 156" />
<path d="M356 205A112 112 0 0 1 205 356" />
+1 -1
View File
@@ -65,7 +65,7 @@ async function selectLocale(value: Locale, close: () => void) {
:aria-pressed="option.value === locale"
:disabled="pendingLocale === option.value"
:aria-busy="pendingLocale === option.value || undefined"
class="aria-pressed:bg-cp-control-item-bg-active aria-pressed:text-cp-primary-text aria-pressed:hover:bg-cp-control-item-bg-active"
class="aria-pressed:bg-z-control-item-bg-active aria-pressed:text-z-primary-text aria-pressed:hover:bg-z-control-item-bg-active"
@click="selectLocale(option.value, close)"
>
<template #icon>
@@ -27,17 +27,17 @@ function select() {
>
<div class="grid gap-2">
<div
class="flex items-center gap-1 rounded-cp-sm pr-1 transition-colors hover:bg-cp-bg-text-hover has-[:focus-visible]:bg-cp-bg-text-hover motion-reduce:transition-none"
class="flex items-center gap-1 rounded-z-sm pr-1 transition-colors hover:bg-z-bg-text-hover has-[:focus-visible]:bg-z-bg-text-hover motion-reduce:transition-none"
>
<ZMenuItem class="min-h-18 min-w-0 py-3 hover:bg-transparent!" @click="select">
<template #icon>
<span class="grid size-9 place-items-center rounded-cp bg-cp-fill-quaternary text-cp-text-secondary">
<span class="grid size-9 place-items-center rounded-z bg-z-fill-quaternary text-z-text-secondary">
<AppWindow class="size-5" aria-hidden="true" />
</span>
</template>
<span class="grid gap-1 whitespace-normal">
<span>CC Switch</span>
<span class="text-xs leading-5 font-normal text-cp-text-secondary">{{ t('keys.configuration.ccSwitchDescription') }}</span>
<span class="text-xs leading-5 font-normal text-z-text-secondary">{{ t('keys.configuration.ccSwitchDescription') }}</span>
</span>
</ZMenuItem>
<a
@@ -46,7 +46,7 @@ function select() {
rel="noopener noreferrer"
:aria-label="t('keys.configuration.viewOnGitHub', { name: 'CC Switch' })"
:title="t('keys.configuration.viewOnGitHub', { name: 'CC Switch' })"
class="inline-grid size-9 shrink-0 place-items-center rounded-cp-sm text-cp-text-tertiary outline-none transition-colors hover:text-cp-text focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-inset motion-reduce:transition-none"
class="inline-grid size-9 shrink-0 place-items-center rounded-z-sm text-z-text-tertiary outline-none transition-colors hover:text-z-text focus-visible:ring-2 focus-visible:ring-z-control-outline focus-visible:ring-inset motion-reduce:transition-none"
>
<Github class="size-6" aria-hidden="true" />
</a>
+1 -1
View File
@@ -12,7 +12,7 @@ defineProps<{ label: string, content: string }>()
<CircleAlert class="size-3.5" aria-hidden="true" />
</ZIconButton>
</template>
<section role="dialog" :aria-label="label" class="max-w-72 p-3 text-cp-xs leading-relaxed text-cp-text-secondary">
<section role="dialog" :aria-label="label" class="max-w-72 p-3 text-z-xs leading-relaxed text-z-text-secondary">
{{ content }}
</section>
</ZPopover>
@@ -53,22 +53,22 @@ const authenticationLabel = computed(() => formatAuthenticationLabel(props.authe
<template>
<span class="inline-flex items-center whitespace-nowrap" :class="groupGapClass">
<span
class="inline-flex shrink-0 items-center justify-center bg-cp-fill-secondary/80 text-cp-text"
class="inline-flex shrink-0 items-center justify-center bg-z-fill-secondary/80 text-z-text"
:class="iconContainerClass"
:title="providerLabel"
>
<component :is="providerIconComponent" v-if="providerIconComponent" :class="iconClass" />
<span v-else class="text-[10px] font-heavy text-cp-text-quaternary">?</span>
<span v-else class="text-[10px] font-heavy text-z-text-quaternary">?</span>
</span>
<span
v-if="showAuthenticationKind"
class="inline-flex shrink-0 items-center justify-center bg-cp-fill-secondary/80 text-cp-text"
class="inline-flex shrink-0 items-center justify-center bg-z-fill-secondary/80 text-z-text"
:class="iconContainerClass"
:title="authenticationLabel"
>
<component :is="authenticationIconComponent" v-if="authenticationIconComponent" :class="iconClass" />
<span v-else class="text-[10px] font-heavy text-cp-text-quaternary">?</span>
<span v-else class="text-[10px] font-heavy text-z-text-quaternary">?</span>
</span>
<span class="sr-only">
@@ -20,7 +20,7 @@ const { t } = useI18n({ useScope: 'global' })
:disabled="disabled || loading"
@click="emit('detect')"
>
<span class="relative grid size-7 place-items-center text-cp-link">
<span class="relative grid size-7 place-items-center text-z-link">
<span v-if="loading" class="location-ripple" aria-hidden="true" />
<span v-if="loading" class="location-ripple location-ripple-delayed" aria-hidden="true" />
<LocateFixed class="relative z-10 size-4" aria-hidden="true" />
@@ -1,14 +1,31 @@
<script setup lang="ts">
import type { FormRule } from '@codex-proxy/ui'
import type { RequestLocation } from '@/api'
import { ZFormItem, ZInput } from '@codex-proxy/ui'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
defineProps<{ disabled?: boolean }>()
defineProps<{ disabled?: boolean, formProp?: string }>()
const { t } = useI18n({ useScope: 'global' })
const location = defineModel<RequestLocation>({ required: true })
const requiredRule = computed<FormRule>(() => ({
required: true,
whitespace: true,
trigger: ['blur', 'change'],
message: t('common.location.required'),
}))
const countryRules = computed<FormRule[]>(() => [
requiredRule.value,
{
pattern: /^[A-Z]{2}$/,
transform: value => value.trim().toUpperCase(),
trigger: ['blur', 'change'],
message: t('common.location.required'),
},
])
function update(field: keyof RequestLocation, value: string) {
location.value = { ...location.value, [field]: value }
@@ -16,17 +33,17 @@ function update(field: keyof RequestLocation, value: string) {
</script>
<template>
<div class="grid gap-4 sm:grid-cols-2">
<ZFormItem :label="t('common.location.countryCode')" required>
<div class="grid gap-4.5 sm:grid-cols-2">
<ZFormItem :prop="formProp ? `${formProp}.country` : undefined" :rules="countryRules" :show-message="Boolean(location.country.trim())" :label="t('common.location.countryCode')" required>
<ZInput :model-value="location.country" :disabled="disabled" :aria-label="t('common.location.countryCode')" :placeholder="t('common.location.countryCodePlaceholder')" maxlength="2" @update:model-value="update('country', $event)" />
</ZFormItem>
<ZFormItem :label="t('common.location.region')" required>
<ZFormItem :prop="formProp ? `${formProp}.region` : undefined" :rules="requiredRule" :show-message="false" :label="t('common.location.region')" required>
<ZInput :model-value="location.region" :disabled="disabled" :aria-label="t('common.location.region')" :placeholder="t('common.location.regionPlaceholder')" maxlength="128" @update:model-value="update('region', $event)" />
</ZFormItem>
<ZFormItem :label="t('common.location.city')" required>
<ZFormItem :prop="formProp ? `${formProp}.city` : undefined" :rules="requiredRule" :show-message="false" :label="t('common.location.city')" required>
<ZInput :model-value="location.city" :disabled="disabled" :aria-label="t('common.location.city')" :placeholder="t('common.location.cityPlaceholder')" maxlength="128" @update:model-value="update('city', $event)" />
</ZFormItem>
<ZFormItem :label="t('common.location.timeZone')" required>
<ZFormItem :prop="formProp ? `${formProp}.timezone` : undefined" :rules="requiredRule" :show-message="false" :label="t('common.location.timeZone')" required>
<ZInput :model-value="location.timezone" :disabled="disabled" :aria-label="t('common.location.timeZone')" :placeholder="t('common.location.timeZonePlaceholder')" @update:model-value="update('timezone', $event)" />
</ZFormItem>
</div>
@@ -20,18 +20,18 @@ const props = withDefaults(
)
const planPalettes: Record<string, string> = {
api: 'bg-cp-cyan-container-strong text-cp-cyan-on-container',
free: 'bg-cp-cyan-container text-cp-cyan-on-container',
plus: 'bg-cp-blue-container-strong text-cp-blue-on-container',
pro: 'bg-cp-purple-container-strong text-cp-purple-on-container',
prolite: 'bg-cp-purple-container text-cp-purple-on-container',
api: 'bg-z-cyan-container-strong text-z-cyan-on-container',
free: 'bg-z-cyan-container text-z-cyan-on-container',
plus: 'bg-z-blue-container-strong text-z-blue-on-container',
pro: 'bg-z-purple-container-strong text-z-purple-on-container',
prolite: 'bg-z-purple-container text-z-purple-on-container',
}
const fallbackPalettes = [
'bg-cp-blue-container text-cp-blue-on-container',
'bg-cp-green-container text-cp-green-on-container',
'bg-cp-cyan-container text-cp-cyan-on-container',
'bg-cp-orange-container text-cp-orange-on-container',
'bg-z-blue-container text-z-blue-on-container',
'bg-z-green-container text-z-green-on-container',
'bg-z-cyan-container text-z-cyan-on-container',
'bg-z-orange-container text-z-orange-on-container',
] as const
const rawPlanType = computed(() => props.authenticationKind === 'api_key'
@@ -45,8 +45,8 @@ const sizeClass = computed(() => {
if (props.size === 'xs')
return 'h-4.5 rounded-full px-1.5 text-[10px] font-bold'
return props.size === 'sm'
? 'h-5 rounded-full px-1.75 text-cp-xs font-bold'
: 'h-5.5 rounded-full px-2 text-cp-xs font-heavy'
? 'h-5 rounded-full px-1.75 text-z-xs font-bold'
: 'h-5.5 rounded-full px-2 text-z-xs font-heavy'
})
const paletteClass = computed(() => {
@@ -63,7 +63,7 @@ const paletteClass = computed(() => {
<AccountUpstreamBadge v-if="upstream?.discovered && upstream.observedAt" :size="size" />
<span
v-else
class="inline-flex min-w-0 max-w-full items-center justify-center whitespace-nowrap leading-none shadow-cp-tertiary"
class="inline-flex min-w-0 max-w-full items-center justify-center whitespace-nowrap leading-none shadow-z-tertiary"
:class="[sizeClass, paletteClass]"
:title="displayText || undefined"
>
@@ -65,9 +65,9 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
</script>
<template>
<section class="w-84 max-w-[calc(100vw-2rem)] overflow-hidden rounded-cp-lg" role="dialog" :aria-label="t('accounts.quota.details', { detailTitle })">
<header v-if="detailHeading" class="bg-cp-popover-header-bg px-3 py-2.5">
<h3 class="m-0 truncate text-cp leading-5 font-heavy text-cp-text">
<section class="w-84 max-w-[calc(100vw-2rem)] overflow-hidden rounded-z-lg" role="dialog" :aria-label="t('accounts.quota.details', { detailTitle })">
<header v-if="detailHeading" class="bg-z-popover-header-bg px-3 py-2.5">
<h3 class="m-0 truncate text-z leading-5 font-heavy text-z-text">
{{ detailHeading }}
</h3>
</header>
@@ -77,16 +77,16 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
<article v-for="item in detailItems" :key="item.key">
<div class="grid grid-cols-[32px_minmax(0,1fr)_auto] items-center gap-2.5">
<span
class="grid h-5 w-8 place-items-center rounded-sm bg-cp-blue-container-strong font-mono text-[9px] leading-none font-heavy tracking-[0.04em] whitespace-nowrap text-cp-blue-on-container"
class="grid h-5 w-8 place-items-center rounded-sm bg-z-blue-container-strong font-mono text-[9px] leading-none font-heavy tracking-[0.04em] whitespace-nowrap text-z-blue-on-container"
>
{{ item.code }}
</span>
<h4 class="m-0 min-w-0 truncate text-cp-sm leading-4 font-heavy text-cp-text-secondary">
<h4 class="m-0 min-w-0 truncate text-z-sm leading-4 font-heavy text-z-text-secondary">
{{ item.local ? item.localLabel : item.label }}
</h4>
<strong
class="font-mono text-cp-xs leading-none font-heavy tabular-nums"
:class="item.local ? 'text-cp-text' : item.percentTextClass"
class="font-mono text-z-xs leading-none font-heavy tabular-nums"
:class="item.local ? 'text-z-text' : item.percentTextClass"
>
{{ item.local ? t('accounts.quota.requestCount', { requestDisplay: item.requestDisplay }) : item.quotaValueDisplay }}
</strong>
@@ -100,7 +100,7 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
/>
<template v-else>
<div
class="mt-2 h-1 w-full overflow-hidden rounded-full bg-cp-border-secondary"
class="mt-2 h-1 w-full overflow-hidden rounded-full bg-z-border-secondary"
role="progressbar"
:aria-label="item.label"
aria-valuemin="0"
@@ -116,38 +116,38 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
</div>
</template>
<p v-if="item.observationDisplay" class="mt-2 text-[10px] leading-4 text-cp-text-tertiary">
<p v-if="item.observationDisplay" class="mt-2 text-[10px] leading-4 text-z-text-tertiary">
{{ item.observationDisplay }}
</p>
<dl class="mt-2.5 grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-[10px] leading-4">
<template v-if="item.local">
<dt class="font-emphasis text-cp-text-tertiary">
<dt class="font-emphasis text-z-text-tertiary">
{{ t('accounts.quota.statisticalMethod') }}
</dt>
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-cp-text">
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-z-text">
{{ t('accounts.quota.rollingWindow') }}
</dd>
<dt class="font-emphasis text-cp-text-tertiary">
<dt class="font-emphasis text-z-text-tertiary">
{{ t('accounts.quota.statisticalRange') }}
</dt>
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-cp-text-secondary">
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-z-text-secondary">
{{ item.durationDisplay }}
</dd>
</template>
<template v-else>
<template v-if="showConsumption && item.consumptionDisplay">
<dt class="font-emphasis text-cp-text-tertiary">
<dt class="font-emphasis text-z-text-tertiary">
{{ t('accounts.quota.windowConsumption') }}
</dt>
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-cp-text">
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-z-text">
{{ item.consumptionDisplay }}
</dd>
</template>
<dt class="font-emphasis text-cp-text-tertiary">
<dt class="font-emphasis text-z-text-tertiary">
{{ t('accounts.quota.resetTime') }}
</dt>
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-cp-text-secondary">
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-z-text-secondary">
{{ item.resetAtDisplay }}
</dd>
</template>
@@ -9,7 +9,7 @@ const { t } = useI18n({ useScope: 'global' })
<template>
<span
class="peer-badge relative isolate inline-flex max-w-full shrink-0 cursor-default items-center gap-0.5 overflow-hidden rounded-md bg-cp-cyan-container-strong py-0.5 pr-1.5 pl-0.5 align-middle text-cp-cyan-on-container"
class="peer-badge relative isolate inline-flex max-w-full shrink-0 cursor-default items-center gap-0.5 overflow-hidden rounded-md bg-z-cyan-container-strong py-0.5 pr-1.5 pl-0.5 align-middle text-z-cyan-on-container"
:class="size === 'xs' ? 'h-4.5' : 'h-5'"
:aria-label="t('accounts.upstream.link')"
:title="t('accounts.upstream.link')"
@@ -29,8 +29,8 @@ const { t } = useI18n({ useScope: 'global' })
background: linear-gradient(
110deg,
transparent 30%,
color-mix(in srgb, var(--cp-color-text-light-solid) 12%, transparent) 43%,
color-mix(in srgb, var(--cp-color-text-light-solid) 36%, transparent) 50%,
color-mix(in srgb, var(--z-color-text-light-solid) 12%, transparent) 43%,
color-mix(in srgb, var(--z-color-text-light-solid) 36%, transparent) 50%,
transparent 70%
);
transform: translateX(-120%);
@@ -28,8 +28,8 @@ const detail = computed(() => [
<template>
<span
class="min-w-0 truncate text-cp-xs font-emphasis"
:class="upstream.stale || upstream.lastError ? 'text-cp-warning-text' : 'text-cp-text-tertiary'"
class="min-w-0 truncate text-z-xs font-emphasis"
:class="upstream.stale || upstream.lastError ? 'text-z-warning-text' : 'text-z-text-tertiary'"
:title="detail"
role="status"
>
@@ -23,11 +23,11 @@ withDefaults(
<span
v-for="bar in bars"
:key="bar.key"
class="relative min-w-0 flex-1 bg-cp-border-secondary"
class="relative min-w-0 flex-1 bg-z-border-secondary"
:title="showNativeTooltip ? bar.title : undefined"
>
<span
class="absolute inset-x-0 bottom-0 bg-cp-green-solid"
class="absolute inset-x-0 bottom-0 bg-z-green-solid"
:style="{ height: bar.height }"
/>
</span>
@@ -49,7 +49,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
>
<span
v-if="view.quota.localUsageVisible"
class="text-right text-cp-text-quaternary"
class="text-right text-z-text-quaternary"
:class="view.classes.value"
:title="showNativeTooltip ? t('accounts.quota.usageInThisWindow', { localUsageDisplay: view.quota.localUsageDisplay }) : undefined"
>
@@ -70,7 +70,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
>
<span
v-if="view.quota.localUsageVisible"
class="text-cp-text-quaternary"
class="text-z-text-quaternary"
:class="view.classes.value"
>
{{ view.quota.localUsageDisplay }}
@@ -100,7 +100,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
</div>
<div
v-if="view.quota.resetVisible"
class="mt-3 text-cp-sm font-emphasis text-cp-text-secondary"
class="mt-3 text-z-sm font-emphasis text-z-text-secondary"
>
{{ t('accounts.quota.resets', { resetAtDisplay: window.resetAtDisplay }) }}
</div>
@@ -113,7 +113,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
</span>
<strong
v-if="view.local.requestValueVisible"
class="shrink-0 font-mono tabular-nums text-cp-text"
class="shrink-0 font-mono tabular-nums text-z-text"
:class="view.classes.value"
>
{{ view.local.requestDisplay }}
@@ -56,9 +56,9 @@ const variantDefinitions: Record<
> = {
metric: {
rootClass: 'grid min-w-0 grid-rows-[14px_14px] gap-1',
headerClass: 'flex items-center justify-between gap-2 text-cp-xs leading-[14px] font-bold',
labelClass: 'truncate text-cp-text-quaternary',
valueClass: 'text-cp-xs leading-none font-bold',
headerClass: 'flex items-center justify-between gap-2 text-z-xs leading-[14px] font-bold',
labelClass: 'truncate text-z-text-quaternary',
valueClass: 'text-z-xs leading-none font-bold',
trackOffsetClass: 'self-center',
trackShapeClass: 'h-1.5 w-full overflow-hidden rounded-full',
minimumBarWidth: '6px',
@@ -68,8 +68,8 @@ const variantDefinitions: Record<
},
compact: {
rootClass: 'flex min-w-0 flex-col',
headerClass: 'mb-1 flex items-baseline justify-between gap-2 text-cp-xs leading-[14px] font-bold',
labelClass: 'truncate text-cp-text-quaternary',
headerClass: 'mb-1 flex items-baseline justify-between gap-2 text-z-xs leading-[14px] font-bold',
labelClass: 'truncate text-z-text-quaternary',
valueClass: 'text-[10px] leading-[14px] font-heavy',
trackOffsetClass: 'mt-auto',
trackShapeClass: 'h-1 w-full overflow-hidden rounded-full',
@@ -79,10 +79,10 @@ const variantDefinitions: Record<
showResetTime: false,
},
detail: {
rootClass: 'rounded-lg bg-cp-fill-quaternary p-2',
headerClass: 'flex items-center justify-between gap-3 text-cp-sm font-bold',
labelClass: 'text-cp-text-secondary',
valueClass: 'text-cp-sm font-heavy',
rootClass: 'rounded-lg bg-z-fill-quaternary p-2',
headerClass: 'flex items-center justify-between gap-3 text-z-sm font-bold',
labelClass: 'text-z-text-secondary',
valueClass: 'text-z-sm font-heavy',
trackOffsetClass: 'mt-2',
trackShapeClass: 'h-2 overflow-hidden rounded-full',
minimumBarWidth: '8px',
@@ -97,20 +97,20 @@ const quotaToneClasses: Record<
{ bar: string, text: string }
> = {
unknown: {
bar: 'bg-cp-border',
text: 'text-cp-text-quaternary',
bar: 'bg-z-border',
text: 'text-z-text-quaternary',
},
danger: {
bar: 'bg-cp-error',
text: 'text-cp-error-text',
bar: 'bg-z-error',
text: 'text-z-error-text',
},
warning: {
bar: 'bg-cp-warning',
text: 'text-cp-warning-text',
bar: 'bg-z-warning',
text: 'text-z-warning-text',
},
success: {
bar: 'bg-cp-success',
text: 'text-cp-success-text',
bar: 'bg-z-success',
text: 'text-z-success-text',
},
}
@@ -146,7 +146,7 @@ export function resolveAccountUsageWindowPresentation(
value: definition.valueClass,
trackOffset: definition.trackOffsetClass,
trackShape: definition.trackShapeClass,
track: `${definition.trackShapeClass} bg-cp-border-secondary`,
track: `${definition.trackShapeClass} bg-z-border-secondary`,
},
quota: {
valueVisible: typeof input.window?.usedPercent === 'number'
+2 -1
View File
@@ -1,3 +1,4 @@
import { getThemeVarName } from '@codex-proxy/ui/theme'
import { escape } from 'es-toolkit'
import { isRecord } from '@/utils/data'
@@ -31,7 +32,7 @@ export function chartTooltipStyle(
fontFamily: options.fontFamily ?? (getComputedStyle(document.documentElement).fontFamily),
fontWeight: options.fontWeight ?? 650,
},
extraCssText: 'border-radius: 12px; box-shadow: var(--cp-box-shadow);',
extraCssText: `border-radius: 12px; box-shadow: var(${getThemeVarName('box-shadow')});`,
}
if (!options.axisPointer)
@@ -194,7 +194,7 @@ const userAgent = computed({
/>
</ZFormItem>
</div>
<p v-if="!custom && currentPreset?.reason" class="m-0 text-cp-sm text-cp-text-secondary">
<p v-if="!custom && currentPreset?.reason" class="m-0 text-z-sm text-z-text-secondary">
{{ currentPreset.reason }}
</p>
<div v-if="!custom" class="grid gap-4 sm:grid-cols-2">
@@ -14,26 +14,26 @@ const { t } = useI18n({ useScope: 'global' })
</script>
<template>
<p v-if="needsVersionInput" role="status" class="m-0 text-cp-sm text-cp-text-tertiary">
<p v-if="needsVersionInput" role="status" class="m-0 text-z-sm text-z-text-tertiary">
{{ t('settings.clientProfile.previewAfterFillingInVersion') }}
</p>
<div
v-else-if="previewing || preview"
class="grid min-h-20 min-w-0 content-center gap-2 rounded-cp bg-cp-fill-quaternary p-4"
class="grid min-h-20 min-w-0 content-center gap-2 rounded-z bg-z-fill-quaternary p-4"
aria-live="polite"
:aria-busy="previewing"
>
<template v-if="previewing">
<div class="flex h-lh items-center text-cp-sm" role="status" :aria-label="t('settings.clientProfile.resolvingClientIdentity')">
<div class="flex h-lh items-center text-z-sm" role="status" :aria-label="t('settings.clientProfile.resolvingClientIdentity')">
<ZSkeleton shape="text" class="w-4/5" aria-hidden="true" />
</div>
<div class="flex h-lh items-center text-cp-xs" aria-hidden="true">
<div class="flex h-lh items-center text-z-xs" aria-hidden="true">
<ZSkeleton shape="text" class="w-52 max-w-full" />
</div>
</template>
<template v-else-if="preview">
<code class="break-all text-cp-sm text-cp-text">{{ preview.userAgent }}</code>
<p class="m-0 text-cp-xs text-cp-text-tertiary">
<code class="break-all text-z-sm text-z-text">{{ preview.userAgent }}</code>
<p class="m-0 text-z-xs text-z-text-tertiary">
{{ preview.versionSource === 'custom' ? t('settings.clientProfile.fixedIdentity') : t('settings.clientProfile.automaticUpdates') }}
<template v-if="preview.versionSource === 'official'">
<template v-if="preview.versionLag">
@@ -42,7 +42,7 @@ const { t } = useI18n({ useScope: 'global' })
· {{ preview.checkedAtDisplay ? t('settings.clientProfile.checked', { checkedAtDisplay: preview.checkedAtDisplay }) : t('settings.clientProfile.toBeChecked') }}
</template>
</p>
<p v-if="preview.error && preview.versionSource !== 'custom'" :title="preview.error" class="m-0 text-cp-sm text-cp-warning">
<p v-if="preview.error && preview.versionSource !== 'custom'" :title="preview.error" class="m-0 text-z-sm text-z-warning">
{{ t('settings.clientProfile.updateFailedUseLastVersion') }}
</p>
</template>
@@ -150,7 +150,7 @@ defineExpose({ focusField })
<Info class="size-3.5" />
</ZIconButton>
</template>
<p class="m-0 max-w-72 p-3 text-cp-xs leading-relaxed text-cp-text-secondary">
<p class="m-0 max-w-72 p-3 text-z-xs leading-relaxed text-z-text-secondary">
{{ field.description }}
</p>
</ZPopover>
@@ -20,26 +20,26 @@ const outcomeMeta = computed(() => [
{
label: t('common.status.success'),
value: props.point.successRequests,
dotClass: 'bg-cp-success',
valueClass: 'text-cp-success-text',
dotClass: 'bg-z-success',
valueClass: 'text-z-success-text',
},
{
label: t('usage.health.point.serviceFailed'),
value: healthServiceFailureCount(props.point),
dotClass: 'bg-cp-error',
valueClass: 'text-cp-error-text',
dotClass: 'bg-z-error',
valueClass: 'text-z-error-text',
},
{
label: t('usage.health.point.clientCancel'),
value: props.point.cancelledRequests,
dotClass: 'bg-cp-orange-solid',
valueClass: 'text-cp-orange-text',
dotClass: 'bg-z-orange-solid',
valueClass: 'text-z-orange-text',
},
{
label: t('usage.health.point.callerError'),
value: props.point.callerErrorRequests,
dotClass: 'bg-cp-info',
valueClass: 'text-cp-info-text',
dotClass: 'bg-z-info',
valueClass: 'text-z-info-text',
},
])
const outcomeSegments = computed(() => {
@@ -62,12 +62,12 @@ const metricItems = outcomeMeta
<section
role="dialog"
:aria-label="t('usage.health.point.requestHealthDetailsFor', { time: point.time })"
class="overflow-hidden rounded-cp-lg bg-cp-bg-elevated"
class="overflow-hidden rounded-z-lg bg-z-bg-elevated"
>
<header class="flex items-center justify-between gap-3 bg-cp-popover-header-bg px-3.5 py-3">
<header class="flex items-center justify-between gap-3 bg-z-popover-header-bg px-3.5 py-3">
<div class="flex min-w-0 items-center gap-2">
<span class="size-2 shrink-0 rounded-full" :class="status.cellClass" />
<strong class="font-mono text-cp leading-none font-heavy text-cp-text">
<strong class="font-mono text-z leading-none font-heavy text-z-text">
{{ point.time }}
</strong>
<span
@@ -77,13 +77,13 @@ const metricItems = outcomeMeta
{{ status.label }}
</span>
</div>
<span class="shrink-0 text-[10px] leading-none font-emphasis text-cp-text-quaternary"> {{ t('usage.health.point.minutes') }} </span>
<span class="shrink-0 text-[10px] leading-none font-emphasis text-z-text-quaternary"> {{ t('usage.health.point.minutes') }} </span>
</header>
<div class="grid gap-3.5 px-3.5 py-3.5">
<div class="flex items-end justify-between gap-4">
<div>
<p class="m-0 text-cp-xs leading-none font-emphasis text-cp-text-quaternary">
<p class="m-0 text-z-xs leading-none font-emphasis text-z-text-quaternary">
{{ t('usage.health.point.validRequestAvailability') }}
</p>
<strong
@@ -94,14 +94,14 @@ const metricItems = outcomeMeta
</strong>
</div>
<div class="text-right">
<span class="block text-[10px] leading-none font-emphasis text-cp-text-quaternary"> {{ t('usage.health.point.validRequest') }} </span>
<strong class="mt-2 block font-mono text-cp-xl leading-none font-heavy tabular-nums text-cp-text">
<span class="block text-[10px] leading-none font-emphasis text-z-text-quaternary"> {{ t('usage.health.point.validRequest') }} </span>
<strong class="mt-2 block font-mono text-z-xl leading-none font-heavy tabular-nums text-z-text">
{{ formatHealthCount(eligibleRequests) }}
</strong>
</div>
</div>
<div class="flex h-1.5 w-full overflow-hidden rounded-full bg-cp-fill-tertiary">
<div class="flex h-1.5 w-full overflow-hidden rounded-full bg-z-fill-tertiary">
<span
v-for="segment in outcomeSegments"
:key="segment.label"
@@ -115,13 +115,13 @@ const metricItems = outcomeMeta
<div
v-for="item in metricItems"
:key="item.label"
class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-cp bg-cp-fill-tertiary px-2.5 py-2.5"
class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-z bg-z-fill-tertiary px-2.5 py-2.5"
>
<span class="size-1.5 rounded-full" :class="item.dotClass" />
<span class="truncate text-[10px] leading-none font-emphasis text-cp-text-secondary">
<span class="truncate text-[10px] leading-none font-emphasis text-z-text-secondary">
{{ item.label }}
</span>
<strong class="font-mono text-cp-sm leading-none font-heavy tabular-nums" :class="item.valueClass">
<strong class="font-mono text-z-sm leading-none font-heavy tabular-nums" :class="item.valueClass">
{{ formatHealthCount(item.value) }}
</strong>
</div>
@@ -17,8 +17,8 @@ const props = defineProps<{
const { t } = useI18n({ useScope: 'global' })
const healthPopoverArrowSurfaceClasses = {
top: 'bg-cp-popover-header-bg',
bottom: 'bg-cp-bg-elevated',
top: 'bg-z-popover-header-bg',
bottom: 'bg-z-bg-elevated',
}
const timelineGrid = useTemplateRef<HTMLElement>('timelineGrid')
@@ -26,6 +26,8 @@ const popoverAnchor = useTemplateRef<HTMLElement>('popoverAnchor')
const pointPopover = useTemplateRef<InstanceType<typeof ZPopover>>('pointPopover')
const preferredMotion = usePreferredReducedMotion()
const points = computed(() => props.timeline.points)
// 未取得时间桶时按常规日的 96 格保留非交互占位,不补造时间与请求数据
const pointCount = computed(() => points.value.length || 96)
const activePoint = shallowRef<DashboardHealthTimelinePoint>()
const popoverOpen = shallowRef(false)
@@ -240,7 +242,7 @@ onBeforeUnmount(() => {
<template #actions>
<div class="flex w-full flex-wrap items-center justify-between gap-x-4 gap-y-2">
<div
class="flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 text-cp-xs leading-none font-emphasis text-cp-text-quaternary"
class="flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 text-z-xs leading-none font-emphasis text-z-text-quaternary"
>
<span
v-for="item in healthLegend"
@@ -282,8 +284,15 @@ onBeforeUnmount(() => {
<span ref="popoverAnchor" aria-hidden="true" class="pointer-events-none absolute left-0 top-0 h-5 w-px opacity-0" />
<div
ref="timelineGrid" class="grid w-full grid-cols-[repeat(var(--timeline-columns),minmax(0,1fr))] items-end gap-x-0.5 gap-y-1 sm:grid-cols-[repeat(var(--timeline-columns-wide),minmax(0,1fr))]"
:style="{ '--timeline-columns': Math.max(1, Math.ceil(points.length / 2)), '--timeline-columns-wide': Math.max(1, points.length) }"
:style="{ '--timeline-columns': Math.ceil(pointCount / 2), '--timeline-columns-wide': pointCount }"
:role="points.length ? undefined : 'img'"
:aria-label="points.length ? undefined : healthStatusMeta.no_data.label"
>
<template v-if="!points.length">
<span v-for="index in pointCount" :key="index" aria-hidden="true" class="flex h-5 items-center">
<span class="block h-3.5 w-full rounded-xs" :class="healthStatusMeta.no_data.cellClass" />
</span>
</template>
<ZButton
v-for="(point, pointIndex) in points"
:key="point.bucketStart"
@@ -306,7 +315,7 @@ onBeforeUnmount(() => {
:class="[
healthStatusMeta[point.status].cellClass,
isActivePoint(point)
? 'brightness-105 shadow-[0_5px_10px_-5px_var(--cp-color-shadow)]'
? 'brightness-105 shadow-[0_5px_10px_-5px_var(--z-color-shadow)]'
: isInteractivePoint(point)
? 'group-hover:brightness-95'
: undefined,
@@ -316,7 +325,7 @@ onBeforeUnmount(() => {
</div>
</div>
</template>
<div data-health-timeline-popover class="w-72 overflow-hidden rounded-cp-lg">
<div data-health-timeline-popover class="w-72 overflow-hidden rounded-z-lg">
<HealthTimelinePointPopover v-if="activePoint" :point="activePoint" />
</div>
</ZPopover>
@@ -124,23 +124,23 @@ const amountItems = computed(() => {
function itemValueClass(tone?: string, accent?: boolean) {
if (tone === 'success')
return 'text-cp-success-text'
return 'text-z-success-text'
if (tone === 'info' || accent)
return 'text-cp-info-text'
return 'text-cp-text'
return 'text-z-info-text'
return 'text-z-text'
}
</script>
<template>
<div class="flex items-center justify-end gap-1.5">
<span class="font-mono text-cp font-heavy tabular-nums text-cp-green-text">
<span class="font-mono text-z font-heavy tabular-nums text-z-green-text">
{{ usageBillingText(record) }}
</span>
<div v-if="billing" class="flex items-center gap-1">
<span
v-if="billing.serviceTierDisplay === 'Fast'"
class="inline-flex size-4 items-center justify-center rounded-full bg-cp-warning-container text-cp-warning-on-container"
class="inline-flex size-4 items-center justify-center rounded-full bg-z-warning-container text-z-warning-on-container"
:title="t('usage.billing.fastMode')"
role="img"
:aria-label="t('usage.billing.fastMode')"
@@ -152,7 +152,7 @@ function itemValueClass(tone?: string, accent?: boolean) {
:trigger-label="billing.longContextBillingApplied ? t('usage.billing.viewLongContextBillingDetails') : t('usage.billing.viewCostDetails')"
:tone="billing.longContextBillingApplied ? 'warning' : 'primary'"
>
<div class="grid gap-1.5 text-cp-text-secondary">
<div class="grid gap-1.5 text-z-text-secondary">
<div v-for="item in amountItems" :key="item.label" class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-4">
<span class="whitespace-nowrap">{{ item.label }}</span>
<span class="justify-self-end whitespace-nowrap font-mono font-heavy" :class="itemValueClass(undefined, item.accent)">
@@ -160,7 +160,7 @@ function itemValueClass(tone?: string, accent?: boolean) {
</span>
</div>
</div>
<div class="mt-1 grid gap-1.5 rounded-cp bg-cp-fill-tertiary p-2 text-cp-text-secondary">
<div class="mt-1 grid gap-1.5 rounded-z bg-z-fill-tertiary p-2 text-z-text-secondary">
<div v-for="item in billingItems" :key="item.label" class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-4">
<span class="whitespace-nowrap">{{ item.label }}</span>
<span class="justify-self-end whitespace-nowrap font-mono font-heavy" :class="itemValueClass(item.tone)">
@@ -11,7 +11,7 @@ const clientIp = computed(() => usageClientIp(props.record))
<template>
<span
class="inline-flex h-6 max-w-full items-center rounded-full bg-cp-blue-container px-2.5 font-mono text-cp-sm leading-none font-bold text-cp-blue-on-container"
class="inline-flex h-6 max-w-full items-center rounded-full bg-z-blue-container px-2.5 font-mono text-z-sm leading-none font-bold text-z-blue-on-container"
:title="clientIp"
>
<span class="min-w-0 truncate">
@@ -19,18 +19,18 @@ defineProps<{
<template #reference>
<button
type="button"
class="inline-flex size-4 items-center justify-center rounded-full bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
class="inline-flex size-4 items-center justify-center rounded-full bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline"
:class="tone === 'warning'
? 'text-cp-warning-text'
: 'text-cp-primary-text'"
? 'text-z-warning-text'
: 'text-z-primary-text'"
:aria-label="triggerLabel"
>
<Info class="size-3" />
</button>
</template>
<div class="grid w-max min-w-60 max-w-[calc(100vw-2rem)] gap-2 p-3 text-cp-sm leading-none">
<p v-if="title" class="m-0 font-heavy text-cp-text">
<div class="grid w-max min-w-60 max-w-[calc(100vw-2rem)] gap-2 p-3 text-z-sm leading-none">
<p v-if="title" class="m-0 font-heavy text-z-text">
{{ title }}
</p>
<slot />
@@ -16,25 +16,25 @@ const { t } = useI18n({ useScope: 'global' })
const latencyDetails = computed(() => usageLatencyDetails(props.record))
const latencyTones = {
good: 'text-cp-success-text',
warn: 'text-cp-warning-text',
good: 'text-z-success-text',
warn: 'text-z-warning-text',
// 主题的 Orange 与 Warning 共用色种,混入 Error 让相邻两档可区分
slow: 'latency-slow',
critical: 'text-cp-error-text',
critical: 'text-z-error-text',
}
</script>
<template>
<div class="flex items-center justify-end gap-1.5">
<div
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1.5 whitespace-nowrap font-mono text-cp-sm leading-none font-heavy tabular-nums"
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1.5 whitespace-nowrap font-mono text-z-sm leading-none font-heavy tabular-nums"
>
<span class="text-cp-xs text-cp-text-quaternary">{{ t('usage.ttft') }}</span>
<span :class="latencyDetails.firstOutputSeverity === null ? 'text-cp-text-quaternary' : latencyTones[latencyDetails.firstOutputSeverity]">
<span class="text-z-xs text-z-text-quaternary">{{ t('usage.ttft') }}</span>
<span :class="latencyDetails.firstOutputSeverity === null ? 'text-z-text-quaternary' : latencyTones[latencyDetails.firstOutputSeverity]">
{{ latencyDetails.firstOutputDisplay }}
</span>
<span class="text-cp-xs text-cp-text-quaternary">{{ t('usage.latency.total') }}</span>
<span :class="latencyDetails.totalSeverity === null ? 'text-cp-text-quaternary' : latencyTones[latencyDetails.totalSeverity]">
<span class="text-z-xs text-z-text-quaternary">{{ t('usage.latency.total') }}</span>
<span :class="latencyDetails.totalSeverity === null ? 'text-z-text-quaternary' : latencyTones[latencyDetails.totalSeverity]">
{{ latencyDetails.totalDisplay }}
</span>
</div>
@@ -43,16 +43,16 @@ const latencyTones = {
<section
v-for="(section, index) in latencyDetails.sections"
:key="section.title"
class="grid gap-1.5 text-cp-text-secondary"
:class="{ 'border-t border-cp-split pt-2': index > 0 }"
class="grid gap-1.5 text-z-text-secondary"
:class="{ 'border-t border-z-split pt-2': index > 0 }"
>
<p class="m-0 flex items-center justify-between gap-3 text-cp-sm font-heavy text-cp-text">
<p class="m-0 flex items-center justify-between gap-3 text-z-sm font-heavy text-z-text">
<span>{{ section.title }}</span>
<span
class="rounded px-1 py-0.5 text-[10px] leading-none font-normal"
:class="section.source === 'official'
? 'bg-cp-primary-container text-cp-primary-on-container'
: 'bg-cp-fill-tertiary text-cp-text-tertiary'"
? 'bg-z-primary-container text-z-primary-on-container'
: 'bg-z-fill-tertiary text-z-text-tertiary'"
>
{{ section.source === 'official' ? t('usage.latency.official') : t('usage.latency.local') }}
</span>
@@ -60,12 +60,12 @@ const latencyTones = {
<div
v-for="item in section.items"
:key="item.label"
class="flex justify-between gap-4 text-cp-xs"
class="flex justify-between gap-4 text-z-xs"
>
<span class="whitespace-nowrap">{{ item.label }}</span>
<span
class="whitespace-nowrap font-mono font-heavy"
:class="item.emphasized ? 'text-cp-info-text' : 'text-cp-text'"
:class="item.emphasized ? 'text-z-info-text' : 'text-z-text'"
>
{{ item.value }}
</span>
@@ -77,6 +77,6 @@ const latencyTones = {
<style scoped>
.latency-slow {
color: color-mix(in oklab, var(--cp-color-warning-text) 65%, var(--cp-color-error-text));
color: color-mix(in oklab, var(--z-color-warning-text) 65%, var(--z-color-error-text));
}
</style>
@@ -18,22 +18,22 @@ const { t } = useI18n({ useScope: 'global' })
<template>
<div class="inline-grid max-w-full gap-1">
<code
class="block max-w-full truncate font-mono text-cp-sm leading-none font-heavy text-cp-text"
class="block max-w-full truncate font-mono text-z-sm leading-none font-heavy text-z-text"
:title="t('usage.status.requestedModel', { model: modelDisplay.primary })"
>
{{ modelDisplay.primary }}
</code>
<div
v-if="modelDisplay.secondary"
class="flex min-w-0 max-w-full items-center gap-1.25 text-cp-text-secondary"
class="flex min-w-0 max-w-full items-center gap-1.25 text-z-text-secondary"
:title="t('usage.status.mappedModel', { secondary: modelDisplay.secondary })"
>
<CornerDownRight
class="size-3.25 shrink-0 text-cp-blue-text"
class="size-3.25 shrink-0 text-z-blue-text"
stroke-width="2.4"
aria-hidden="true"
/>
<code class="block truncate font-mono text-cp-xs leading-none font-bold">
<code class="block truncate font-mono text-z-xs leading-none font-bold">
{{ modelDisplay.secondary }}
</code>
</div>
@@ -16,10 +16,10 @@ const performance = computed(() => usagePerformanceDetails(props.record))
</script>
<template>
<div class="grid justify-items-end gap-y-1.5 whitespace-nowrap font-mono text-cp-sm leading-none font-heavy tabular-nums">
<span class="text-cp-text">{{ performance.throughputDisplay }}</span>
<span class="text-cp-xs text-cp-text-secondary">
<span :title="t('usage.performance.ttftHelp')" class="text-cp-text-quaternary">TTFT</span>
<div class="grid justify-items-end gap-y-1.5 whitespace-nowrap font-mono text-z-sm leading-none font-heavy tabular-nums">
<span class="text-z-text">{{ performance.throughputDisplay }}</span>
<span class="text-z-xs text-z-text-secondary">
<span :title="t('usage.performance.ttftHelp')" class="text-z-text-quaternary">TTFT</span>
{{ performance.firstTokenDisplay }}
</span>
</div>
@@ -15,12 +15,12 @@ const { t } = useI18n({ useScope: 'global' })
<template>
<span
class="inline-flex items-center gap-1 whitespace-nowrap text-cp-sm font-bold text-cp-text"
class="inline-flex items-center gap-1 whitespace-nowrap text-z-sm font-bold text-z-text"
>
<span>{{ usageReasoningEffort(record) }}</span>
<span
v-if="record.subagentKind"
class="inline-flex shrink-0 text-cp-purple-text"
class="inline-flex shrink-0 text-z-purple-text"
:title="t('usage.reasoning.subagentRequest')"
:aria-label="t('usage.reasoning.subagentRequest')"
>
@@ -28,7 +28,7 @@ const { t } = useI18n({ useScope: 'global' })
</span>
<span
v-if="record.subagentKind === 'review'"
class="inline-flex shrink-0 text-cp-purple-text"
class="inline-flex shrink-0 text-z-purple-text"
:title="t('usage.reasoning.reviewSubagentRequest')"
:aria-label="t('usage.reasoning.reviewSubagentRequest')"
>
@@ -47,7 +47,7 @@ const { t } = useI18n({ useScope: 'global' })
>
<template #clientApiKeyName="{ displayValue }">
<span
class="block max-w-full truncate font-mono text-cp-sm leading-none font-bold text-cp-text"
class="block max-w-full truncate font-mono text-z-sm leading-none font-bold text-z-text"
:title="String(displayValue)"
>
{{ displayValue }}
@@ -63,14 +63,14 @@ const { t } = useI18n({ useScope: 'global' })
<template #accountEmail="{ row }">
<span
class="block max-w-full truncate font-mono text-cp-sm leading-none font-bold text-cp-text"
class="block max-w-full truncate font-mono text-z-sm leading-none font-bold text-z-text"
:title="usageAccountText(row)"
>
{{ usageAccountText(row) }}
</span>
<span
v-if="row.accountNotes?.trim()"
class="mt-1 block max-w-full truncate text-cp-xs font-emphasis text-cp-text-quaternary"
class="mt-1 block max-w-full truncate text-z-xs font-emphasis text-z-text-quaternary"
:title="row.accountNotes"
>
{{ row.accountNotes }}
@@ -85,7 +85,7 @@ const { t } = useI18n({ useScope: 'global' })
:plan-type-display="row.accountPlanTypeDisplay || row.accountPlanType || ''"
size="sm"
/>
<span v-else class="text-cp-text-quaternary">—</span>
<span v-else class="text-z-text-quaternary">—</span>
</template>
<template #createdAtDisplay="{ row }">
@@ -97,7 +97,7 @@ const { t } = useI18n({ useScope: 'global' })
</template>
<template #userAgent="{ row }">
<span class="block max-w-full wrap-break-word whitespace-normal font-mono text-cp-sm leading-[1.4] font-emphasis text-cp-text-secondary">
<span class="block max-w-full wrap-break-word whitespace-normal font-mono text-z-sm leading-[1.4] font-emphasis text-z-text-secondary">
{{ usageUserAgent(row) }}
</span>
</template>
@@ -116,10 +116,10 @@ const { t } = useI18n({ useScope: 'global' })
<template #errorMessage="{ row }">
<div class="min-w-0 py-0.5" :title="row.errorMessage || row.errorCode || undefined">
<code class="block truncate font-mono text-cp-sm font-bold text-cp-error-text">
<code class="block truncate font-mono text-z-sm font-bold text-z-error-text">
{{ row.errorCode || (row.logicalOutcome === 'incomplete' ? t('usage.error.responseIsNotComplete') : t('usage.error.requestFailed')) }}
</code>
<p v-if="row.errorMessage" class="mt-1 mb-0 truncate text-cp-xs leading-[1.45] font-emphasis text-cp-text-secondary">
<p v-if="row.errorMessage" class="mt-1 mb-0 truncate text-z-xs leading-[1.45] font-emphasis text-z-text-secondary">
{{ row.errorMessage }}
</p>
</div>
@@ -10,10 +10,10 @@ const { t } = useI18n({ useScope: 'global' })
<template>
<div class="inline-flex max-w-full items-center gap-1.5 whitespace-nowrap">
<code class="font-mono text-cp-sm font-emphasis">{{ record.route || '—' }}</code>
<code class="font-mono text-z-sm font-emphasis">{{ record.route || '—' }}</code>
<span
v-if="record.compact"
class="inline-flex shrink-0 text-cp-orange-text"
class="inline-flex shrink-0 text-z-orange-text"
:title="t('usage.route.compressionRequest')"
:aria-label="t('usage.route.compressionRequest')"
>
@@ -3,7 +3,7 @@ defineProps<{ value: string }>()
</script>
<template>
<span class="block truncate font-mono text-cp-sm leading-none font-bold text-cp-text-secondary" :title="value">
<span class="block truncate font-mono text-z-sm leading-none font-bold text-z-text-secondary" :title="value">
{{ value || '—' }}
</span>
</template>
@@ -43,34 +43,34 @@ const tokenItems = computed(() => [
<template>
<div class="flex items-center justify-end gap-1.5">
<div
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1 font-mono text-cp-sm leading-none font-bold tabular-nums"
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1 font-mono text-z-sm leading-none font-bold tabular-nums"
>
<span class="inline-flex items-center gap-1 text-cp-success-text">
<span class="inline-flex items-center gap-1 text-z-success-text">
<ArrowDown class="size-3" />
{{ tokenDetails.inputTokensDisplay }}
</span>
<span class="inline-flex items-center gap-1 text-cp-info-text">
<span class="inline-flex items-center gap-1 text-z-info-text">
<ArrowUp class="size-3" />
{{ tokenDetails.outputTokensDisplay }}
</span>
<span class="col-span-2 inline-flex items-center justify-end gap-1 text-cp-info-text">
<span class="col-span-2 inline-flex items-center justify-end gap-1 text-z-info-text">
<Archive class="size-3" />
{{ tokenDetails.cachedInputTokensDisplay }}
</span>
</div>
<UsageDetailPopover :title="t('usage.tokens.tokenDetails')" :trigger-label="t('usage.tokens.viewTokenDetails')">
<div class="grid gap-1.5 text-cp-text-secondary">
<div class="grid gap-1.5 text-z-text-secondary">
<div v-for="item in tokenItems" :key="item.label" class="flex justify-between gap-4">
<span class="whitespace-nowrap">{{ item.label }}</span>
<span class="whitespace-nowrap font-mono font-heavy text-cp-text">
<span class="whitespace-nowrap font-mono font-heavy text-z-text">
{{ item.value }}
</span>
</div>
</div>
<div class="mt-1 flex justify-between border-t border-cp-split pt-2">
<span class="whitespace-nowrap text-cp-text-secondary">{{ t('usage.tokens.totalTokens') }}</span>
<span class="whitespace-nowrap font-mono font-heavy text-cp-info-text">
<div class="mt-1 flex justify-between border-t border-z-split pt-2">
<span class="whitespace-nowrap text-z-text-secondary">{{ t('usage.tokens.totalTokens') }}</span>
<span class="whitespace-nowrap font-mono font-heavy text-z-info-text">
{{ tokenDetails.totalTokensDisplay }}
</span>
</div>
@@ -8,7 +8,7 @@ defineProps<{
<template>
<span
class="inline-flex h-6 min-w-12 items-center justify-center rounded-full px-2 text-cp leading-none font-bold"
class="inline-flex h-6 min-w-12 items-center justify-center rounded-full px-2 text-z leading-none font-bold"
:class="usageTransportTypeClass(transport)"
>
{{ usageTransportType(transport) }}
+18 -18
View File
@@ -36,39 +36,39 @@ export const healthLegend = computed(() => ([
export const healthStatusMeta = computed<Record<DashboardHealthStatus, DashboardHealthStatusMeta>>(() => ({
future: {
label: t('usage.future'),
cellClass: 'bg-cp-bg-container-disabled opacity-60',
badgeClass: 'bg-cp-fill-tertiary text-cp-text-quaternary',
valueClass: 'text-cp-text-quaternary',
cellClass: 'bg-z-bg-container-disabled opacity-60',
badgeClass: 'bg-z-fill-tertiary text-z-text-quaternary',
valueClass: 'text-z-text-quaternary',
},
no_data: {
label: t('usage.noValidSample'),
cellClass: 'bg-cp-border',
badgeClass: 'bg-cp-fill-tertiary text-cp-text-secondary',
valueClass: 'text-cp-text-quaternary',
cellClass: 'bg-z-border',
badgeClass: 'bg-z-fill-tertiary text-z-text-secondary',
valueClass: 'text-z-text-quaternary',
},
unavailable: {
label: t('usage.notReachable'),
cellClass: 'bg-cp-error',
badgeClass: 'bg-cp-error-container text-cp-error-on-container',
valueClass: 'text-cp-error-text',
cellClass: 'bg-z-error',
badgeClass: 'bg-z-error-container text-z-error-on-container',
valueClass: 'text-z-error-text',
},
unstable: {
label: t('usage.unstable'),
cellClass: 'bg-cp-warning',
badgeClass: 'bg-cp-warning-container text-cp-warning-on-container',
valueClass: 'text-cp-warning-text',
cellClass: 'bg-z-warning',
badgeClass: 'bg-z-warning-container text-z-warning-on-container',
valueClass: 'text-z-warning-text',
},
low_sample: {
label: t('usage.lowSample'),
cellClass: 'bg-cp-cyan-solid',
badgeClass: 'bg-cp-cyan-container text-cp-cyan-on-container',
valueClass: 'text-cp-cyan-text',
cellClass: 'bg-z-cyan-solid',
badgeClass: 'bg-z-cyan-container text-z-cyan-on-container',
valueClass: 'text-z-cyan-text',
},
stable: {
label: t('usage.stable'),
cellClass: 'bg-cp-success',
badgeClass: 'bg-cp-success-container text-cp-success-on-container',
valueClass: 'text-cp-success-text',
cellClass: 'bg-z-success',
badgeClass: 'bg-z-success-container text-z-success-on-container',
valueClass: 'text-z-success-text',
},
}))
@@ -58,10 +58,10 @@ export function usageTransportType(transport?: string | null) {
export function usageTransportTypeClass(transport?: string | null) {
const type = usageTransportType(transport)
if (type === 'WS')
return 'bg-cp-blue-container text-cp-blue-on-container'
return 'bg-z-blue-container text-z-blue-on-container'
if (type === 'SSE')
return 'bg-cp-green-container text-cp-green-on-container'
return 'bg-cp-fill-tertiary text-cp-text-secondary'
return 'bg-z-green-container text-z-green-on-container'
return 'bg-z-fill-tertiary text-z-text-secondary'
}
export function usageAccountText(record: Pick<UsageCommonRecord, 'accountEmail' | 'accountName' | 'accountId'>) {
+15 -15
View File
@@ -5,21 +5,21 @@ import { useThemeColor } from '@/composables/useThemeColor'
export function useChartPalette() {
const color = useThemeColor()
const palette = computed(() => ({
textPrimary: color('--cp-color-text', '#0E1726'),
textSecondary: color('--cp-color-text-secondary', '#64748B'),
textMuted: color('--cp-color-text-quaternary', '#94A3B8'),
surface: color('--cp-color-bg-container', '#FFFFFF'),
surfaceMuted: color('--cp-color-fill-tertiary', '#F1F5F9'),
grid: color('--cp-color-fill-tertiary', '#F1F5F9'),
divider: color('--cp-color-split', '#E2E8F0'),
border: color('--cp-color-border-secondary', '#E2E8F0'),
pointer: color('--cp-color-border', '#CBD5E1'),
info: color('--cp-color-blue-solid', '#5983F4'),
success: color('--cp-color-green-solid', '#12B981'),
warning: color('--cp-color-orange-solid', '#F59E0B'),
danger: color('--cp-color-red-solid', '#EF4444'),
normal: color('--cp-color-text-tertiary', '#94A3B8'),
reasoning: color('--cp-color-purple-solid', '#722ED1'),
textPrimary: color('color-text', '#0E1726'),
textSecondary: color('color-text-secondary', '#64748B'),
textMuted: color('color-text-quaternary', '#94A3B8'),
surface: color('color-bg-container', '#FFFFFF'),
surfaceMuted: color('color-fill-tertiary', '#F1F5F9'),
grid: color('color-fill-tertiary', '#F1F5F9'),
divider: color('color-split', '#E2E8F0'),
border: color('color-border-secondary', '#E2E8F0'),
pointer: color('color-border', '#CBD5E1'),
info: color('color-blue-solid', '#5983F4'),
success: color('color-green-solid', '#12B981'),
warning: color('color-orange-solid', '#F59E0B'),
danger: color('color-red-solid', '#EF4444'),
normal: color('color-text-tertiary', '#94A3B8'),
reasoning: color('color-purple-solid', '#722ED1'),
}))
return { color, palette }
+3 -2
View File
@@ -1,4 +1,5 @@
import type { InjectionKey, Ref } from 'vue'
import { getThemeVarName } from '@codex-proxy/ui/theme'
import { storeToRefs } from 'pinia'
import { inject, provide } from 'vue'
@@ -20,7 +21,7 @@ export function useThemeColor() {
const { themeRevision } = storeToRefs(useThemeStore())
return (name: string, fallback: string) => {
if (scopedTokens) {
const scopedValue = scopedTokens.value[name]
const scopedValue = scopedTokens.value[getThemeVarName(name)]
if (typeof scopedValue === 'string' && scopedValue.trim())
return scopedValue.trim()
if (typeof scopedValue === 'number')
@@ -36,5 +37,5 @@ export function useThemeColor() {
function readCssVariable(name: string, fallback: string) {
if (typeof document === 'undefined')
return fallback
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback
return getComputedStyle(document.documentElement).getPropertyValue(getThemeVarName(name)).trim() || fallback
}
+1 -2
View File
@@ -582,6 +582,7 @@
"fillInModelMappingCompletely": "Please fill in the model mapping completely",
"duplicateClientModel": "Duplicate client model: {requested}",
"requiredSettings": "Complete concurrency, queue, credential refresh, and scheduling settings",
"invalidRefreshSettings": "Credential refresh lead time and concurrency must be non-negative integers",
"invalidAffinityDuration": "Affinity duration must be an integer from 1 to 720 hours",
"invalidSwitchLimit": "Maximum account switches must be an integer from 0 to 31, set to 0 to disable switching",
"invalidConcurrency": "Default account concurrency must be an integer from 0 to 4,294,967,295, set to 0 for unlimited",
@@ -591,13 +592,11 @@
"invalidResetLeadTime": "Reset credit lead time must be an integer from 1 to 1,440 minutes",
"invalidRetention": "History retention must be an integer from 1 to 31 days",
"invalidQueueSettings": "Queue capacity must be an integer from 0 to 1,000 and timeout from 1 to 120 seconds",
"invalidClientVersion": "Enter a valid minimum client version",
"invalidProbeModel": "The probe model must be at most 128 characters without control characters",
"invalidWarmupTimes": "Use HH:MM for warmup times, for example 08:00 or 08:00,13:00",
"warmupModelRequired": "Select a model to enable warmup",
"invalidWarmupModel": "Warmup model name must not exceed 128 characters",
"settingsSaved": "Settings saved",
"protectionSettingsRequired": "Complete the error protection settings",
"invalidProtectionSettings": "Use a failure threshold of 2–1,000, count validity of 60–3,600 seconds, and a cooldown of 300–604,800 seconds"
},
"page": {
@@ -582,6 +582,7 @@
"fillInModelMappingCompletely": "请完整填写模型映射",
"duplicateClientModel": "存在重复的客户端模型:{requested}",
"requiredSettings": "请完整填写并发、队列、凭据刷新参数和调度策略",
"invalidRefreshSettings": "凭据刷新提前量和并发数须为非负整数",
"invalidAffinityDuration": "亲和时长应为 1~720 小时的整数",
"invalidSwitchLimit": "最大换号次数应为 0~31 的整数,0 表示不换号",
"invalidConcurrency": "默认账号并发上限应为 0~4294967295 的整数,0 表示不限制",
@@ -591,13 +592,11 @@
"invalidResetLeadTime": "重置卡提前使用时间应为 1~1440 分钟的整数",
"invalidRetention": "历史保留天数应为 1–31 的整数",
"invalidQueueSettings": "队列容量应为 0~1000 的整数,排队超时应为 1~120 秒的整数",
"invalidClientVersion": "请修正客户端最低版本格式",
"invalidProbeModel": "探测模型最多 128 个字符,不能包含控制字符",
"invalidWarmupTimes": "预激活时间格式无效,请输入 HH:MM 格式(如 08:00 或 08:00,13:00)",
"warmupModelRequired": "启用预激活时请选择模型",
"invalidWarmupModel": "预激活模型名称不能超过 128 个字符",
"settingsSaved": "设置已保存",
"protectionSettingsRequired": "请完整填写错误保护参数",
"invalidProtectionSettings": "失败阈值应为 2~1000,计数有效期为 60~3600 秒,冷却时长为 300~604800 秒"
},
"page": {
@@ -582,6 +582,7 @@
"fillInModelMappingCompletely": "請完整填寫模型映射",
"duplicateClientModel": "存在重複的客戶端模型:{requested}",
"requiredSettings": "請完整填寫併發、隊列、憑據刷新參數和調度策略",
"invalidRefreshSettings": "憑據刷新提前量和併發數須為非負整數",
"invalidAffinityDuration": "親和時長應爲 1~720 小時的整數",
"invalidSwitchLimit": "最大換號次數應爲 0~31 的整數,0 表示不換號",
"invalidConcurrency": "默認賬號併發上限應爲 0~4294967295 的整數,0 表示不限制",
@@ -591,13 +592,11 @@
"invalidResetLeadTime": "重置卡提前使用時間應爲 1~1440 分鐘的整數",
"invalidRetention": "歷史保留天數應爲 1–31 的整數",
"invalidQueueSettings": "隊列容量應爲 0~1000 的整數,排隊超時應爲 1~120 秒的整數",
"invalidClientVersion": "請修正客戶端最低版本格式",
"invalidProbeModel": "探測模型最多 128 個字符,不能包含控制字符",
"invalidWarmupTimes": "預激活時間格式無效,請輸入 HH:MM 格式(如 08:00 或 08:00,13:00)",
"warmupModelRequired": "啓用預激活時請選擇模型",
"invalidWarmupModel": "預激活模型名稱不能超過 128 個字符",
"settingsSaved": "設置已保存",
"protectionSettingsRequired": "請完整填寫錯誤保護參數",
"invalidProtectionSettings": "失敗閾值應爲 2~1000,計數有效期爲 60~3600 秒,冷卻時長爲 300~604800 秒"
},
"page": {
+47 -58
View File
@@ -141,23 +141,23 @@ const activeNavIndex = computed(() => {
const index = navItems.value.findIndex(item => isActive(item.path))
return Math.max(0, index)
})
const activeNavIndicatorStyle = computed(() => ({
transform: `translate3d(0, ${activeNavIndex.value * 58 + (activeNavIndex.value > pluginNavIndex
? pluginChildrenHeight.value
: 0)}px, 0)`,
opacity: isActive('/plugins') && pluginMenuVisible.value
? 0
: 1,
}))
const activePluginIndex = computed(() => route.name === 'plugins'
? 0
: pluginPageViews.value.findIndex(isPluginViewActive) + 1)
const activePluginIndicatorStyle = computed(() => ({
transform: `translate3d(0, ${activePluginIndex.value * 44}px, 0)`,
opacity: isActive('/plugins') && pluginMenuVisible.value
? 1
: 0,
}))
const activeNavIndicatorStyle = computed(() => {
const inPluginMenu = isActive('/plugins') && pluginMenuVisible.value
const offset = inPluginMenu
? 50 + activePluginIndex.value * 44
: activeNavIndex.value > pluginNavIndex
? pluginChildrenHeight.value
: 0
// 主菜单与子菜单共享选中框,位置和尺寸在同一次过渡中变化
return {
transform: `translate3d(${inPluginMenu ? 24 : 0}px, ${activeNavIndex.value * 58 + offset}px, 0)`,
width: inPluginMenu ? 'calc(100% - 24px)' : '100%',
height: inPluginMenu ? '40px' : '46px',
}
})
const navFeedbackMuted = shallowRef(false)
const { start: restoreNavFeedback, stop: stopNavFeedbackRestore } = useTimeoutFn(
() => {
@@ -225,7 +225,6 @@ async function handleLogout() {
const sidebarEl = ref<HTMLElement | null>(null)
const brandLabelEl = ref<HTMLElement | null>(null)
const navSignalEl = useTemplateRef<HTMLElement>('navSignal')
const pluginNavSignalEls = useTemplateRef<HTMLElement[]>('pluginNavSignal')
const collapsedSidebarWidth = 88
const expandedSidebarWidth = 251
const sidebarWidth = computed(() => (isCollapsed.value ? collapsedSidebarWidth : expandedSidebarWidth))
@@ -349,9 +348,7 @@ function animateSidebarWidth(collapsed: boolean) {
}
function animateNavSignal() {
const signal = isActive('/plugins') && pluginMenuVisible.value
? pluginNavSignalEls.value?.[0]
: navSignalEl.value
const signal = navSignalEl.value
if (!signal)
return
@@ -431,7 +428,6 @@ onBeforeUnmount(() => {
sidebarEl.value,
brandLabelEl.value,
navSignalEl.value,
...(pluginNavSignalEls.value ?? []),
].filter((target): target is HTMLElement =>
Boolean(target),
)
@@ -447,7 +443,7 @@ onBeforeUnmount(() => {
<template>
<aside
ref="sidebarEl"
class="z-20 h-dvh shrink-0 flex-col overflow-hidden bg-(--cp-layout-sider-bg) shadow-cp-layout-sider"
class="z-20 h-dvh shrink-0 flex-col overflow-hidden bg-(--z-layout-sider-bg) shadow-z-layout-sider"
:class="[
mobile ? 'flex' : 'hidden min-[961px]:flex',
isCollapsed ? 'w-22 basis-22 items-center' : 'w-62.75 basis-62.75',
@@ -459,28 +455,28 @@ onBeforeUnmount(() => {
>
<ZMotionIcon
variant="brand"
class="inline-flex size-11 items-center justify-center relative -top-0.5 rounded-cp"
class="inline-flex size-11 items-center justify-center relative -top-0.5 rounded-z"
>
<AppBrandMark class="block size-11 select-none" />
</ZMotionIcon>
<span v-show="brandLabelVisible" ref="brandLabelEl" class="grid min-w-33 content-center overflow-hidden">
<strong class="text-base leading-[1.1] font-heavy text-cp-text"> Codex Proxy </strong>
<strong class="text-base leading-[1.1] font-heavy text-z-text"> Codex Proxy </strong>
<span class="mt-1.5 flex h-4.5 min-w-0 items-center gap-2">
<span class="shrink-0 text-xs leading-none font-emphasis text-cp-text-secondary"> Rust build </span>
<span class="shrink-0 text-xs leading-none font-emphasis text-z-text-secondary"> Rust build </span>
<button
v-if="hasVersionLabel"
type="button"
class="inline-flex h-4.5 min-w-0 cursor-pointer items-center gap-1 rounded-cp-sm border-0 px-1.5 font-mono text-[10px] leading-none font-bold transition-colors outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-container"
class="inline-flex h-4.5 min-w-0 cursor-pointer items-center gap-1 rounded-z-sm border-0 px-1.5 font-mono text-[10px] leading-none font-bold transition-colors outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-z-bg-container"
:class="[
hasUpdate
? 'bg-cp-success-container text-cp-success-on-container hover:bg-cp-success-container-hover'
: 'bg-cp-fill-quaternary text-cp-text-quaternary hover:bg-cp-fill-tertiary hover:text-cp-text-secondary',
? 'bg-z-success-container text-z-success-on-container hover:bg-z-success-container-hover'
: 'bg-z-fill-quaternary text-z-text-quaternary hover:bg-z-fill-tertiary hover:text-z-text-secondary',
]"
:title="updateButtonLabel"
@click="openSystemUpdate"
>
<span>{{ versionLabel }}</span>
<ArrowUpCircle v-if="hasUpdate" class="size-3 shrink-0 text-cp-success" />
<ArrowUpCircle v-if="hasUpdate" class="size-3 shrink-0 text-z-success" />
</button>
</span>
</span>
@@ -491,12 +487,12 @@ onBeforeUnmount(() => {
<!-- 焦点环绘制在按钮内侧,避免被滚动区和折叠菜单裁切 -->
<nav class="relative grid gap-3" :class="isCollapsed ? 'mx-auto w-11.5' : 'w-full'" :aria-label="t('navigation.mainLabel')">
<span
class="pointer-events-none absolute inset-x-0 top-0 h-11.5 overflow-hidden rounded-cp bg-cp-menu-item-selected-bg transition-[transform,opacity] duration-260 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
class="pointer-events-none absolute left-0 top-0 overflow-hidden rounded-z bg-z-menu-item-selected-bg transition-[transform,width,height] duration-260 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
:style="activeNavIndicatorStyle"
>
<span
ref="navSignal"
class="absolute inset-y-0 left-0 w-2/3 [background:linear-gradient(90deg,transparent,color-mix(in_srgb,var(--cp-color-info)_9%,transparent),transparent)]"
class="absolute inset-y-0 left-0 w-2/3 [background:linear-gradient(90deg,transparent,color-mix(in_srgb,var(--z-color-info)_9%,transparent),transparent)]"
/>
</span>
<template v-for="item in navItems" :key="item.label">
@@ -505,16 +501,16 @@ onBeforeUnmount(() => {
<template #reference="{ close }">
<button
type="button"
class="inline-flex h-11.5 cursor-pointer items-center rounded-cp border-0 bg-transparent text-sm leading-[1.15] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cp-control-outline"
class="inline-flex h-11.5 cursor-pointer items-center rounded-z border-0 bg-transparent text-sm leading-[1.15] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-z-control-outline"
:class="[
isCollapsed ? 'w-11.5 justify-center' : 'w-full gap-3 px-4',
isActive(item.path)
? navFeedbackMuted
? 'font-bold text-cp-text transition-none'
: 'font-bold text-cp-text transition-colors duration-200'
? 'font-bold text-z-text transition-none'
: 'font-bold text-z-text transition-colors duration-200'
: navFeedbackMuted
? 'font-semibold text-cp-text-secondary transition-none'
: 'font-semibold text-cp-text-secondary transition-colors duration-200 hover:bg-cp-fill-quaternary hover:text-cp-text',
? 'font-semibold text-z-text-secondary transition-none'
: 'font-semibold text-z-text-secondary transition-colors duration-200 hover:bg-z-fill-quaternary hover:text-z-text',
]"
:aria-expanded="isCollapsed ? undefined : pluginGroupExpanded"
:aria-controls="isCollapsed ? undefined : pluginMenuId"
@@ -540,7 +536,7 @@ onBeforeUnmount(() => {
</span>
</button>
</template>
<span class="block px-3 py-2 text-cp-sm whitespace-nowrap">{{ item.label }}</span>
<span class="block px-3 py-2 text-z-sm whitespace-nowrap">{{ item.label }}</span>
</ZPopover>
<div
@@ -552,19 +548,12 @@ onBeforeUnmount(() => {
>
<div class="min-h-0 overflow-hidden">
<div class="relative ml-3 grid gap-1 pt-1 pl-3">
<span
aria-hidden="true"
class="pointer-events-none absolute top-1 right-0 left-3 h-10 overflow-hidden rounded-cp bg-cp-menu-item-selected-bg transition-[transform,opacity] duration-260 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
:style="activePluginIndicatorStyle"
>
<span ref="pluginNavSignal" class="absolute inset-y-0 left-0 w-2/3 opacity-0 [background:linear-gradient(90deg,transparent,color-mix(in_srgb,var(--cp-color-info)_9%,transparent),transparent)]" />
</span>
<button
type="button"
class="relative inline-flex h-10 min-w-0 cursor-pointer items-center gap-2 rounded-cp border-0 bg-transparent px-3 text-left text-cp-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cp-control-outline motion-reduce:transition-none"
class="relative inline-flex h-10 min-w-0 cursor-pointer items-center gap-2 rounded-z border-0 bg-transparent px-3 text-left text-z-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-z-control-outline motion-reduce:transition-none"
:class="route.name === 'plugins'
? 'font-emphasis text-cp-text'
: navFeedbackMuted ? 'font-semibold text-cp-text-secondary' : 'font-semibold text-cp-text-secondary hover:bg-cp-fill-quaternary hover:text-cp-text'"
? 'font-emphasis text-z-text'
: navFeedbackMuted ? 'font-semibold text-z-text-secondary' : 'font-semibold text-z-text-secondary hover:bg-z-fill-quaternary hover:text-z-text'"
:aria-current="route.name === 'plugins' ? 'page' : undefined"
@click="navigate('/plugins')"
>
@@ -575,10 +564,10 @@ onBeforeUnmount(() => {
v-for="view in pluginPageViews"
:key="view.target.instanceId"
type="button"
class="relative inline-flex h-10 min-w-0 cursor-pointer items-center gap-2 rounded-cp border-0 bg-transparent px-3 text-left text-cp-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cp-control-outline motion-reduce:transition-none"
class="relative inline-flex h-10 min-w-0 cursor-pointer items-center gap-2 rounded-z border-0 bg-transparent px-3 text-left text-z-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-z-control-outline motion-reduce:transition-none"
:class="isPluginViewActive(view)
? 'font-emphasis text-cp-text'
: navFeedbackMuted ? 'font-semibold text-cp-text-secondary' : 'font-semibold text-cp-text-secondary hover:bg-cp-fill-quaternary hover:text-cp-text'"
? 'font-emphasis text-z-text'
: navFeedbackMuted ? 'font-semibold text-z-text-secondary' : 'font-semibold text-z-text-secondary hover:bg-z-fill-quaternary hover:text-z-text'"
:aria-current="isPluginViewActive(view) ? 'page' : undefined"
@click="navigatePluginView(view)"
>
@@ -594,16 +583,16 @@ onBeforeUnmount(() => {
<template #reference="{ close }">
<button
type="button"
class="relative z-10 inline-flex h-11.5 cursor-pointer items-center rounded-cp border-0 text-sm leading-[1.15] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cp-control-outline"
class="relative z-10 inline-flex h-11.5 cursor-pointer items-center rounded-z border-0 text-sm leading-[1.15] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-z-control-outline"
:class="[
isCollapsed ? 'w-11.5 justify-center' : 'w-full gap-3 px-4',
isActive(item.path)
? navFeedbackMuted
? 'bg-transparent font-bold text-cp-text transition-none'
: 'bg-transparent font-bold text-cp-text transition-colors duration-200'
? 'bg-transparent font-bold text-z-text transition-none'
: 'bg-transparent font-bold text-z-text transition-colors duration-200'
: navFeedbackMuted
? 'bg-transparent font-semibold text-cp-text-secondary transition-none'
: 'bg-transparent font-semibold text-cp-text-secondary transition-colors duration-200 hover:bg-cp-fill-quaternary hover:text-cp-text',
? 'bg-transparent font-semibold text-z-text-secondary transition-none'
: 'bg-transparent font-semibold text-z-text-secondary transition-colors duration-200 hover:bg-z-fill-quaternary hover:text-z-text',
]"
:aria-label="isCollapsed ? item.label : undefined"
@click="close(); navigate(item.path)"
@@ -617,7 +606,7 @@ onBeforeUnmount(() => {
</span>
</button>
</template>
<span class="block px-3 py-2 text-cp-sm whitespace-nowrap">{{ item.label }}</span>
<span class="block px-3 py-2 text-z-sm whitespace-nowrap">{{ item.label }}</span>
</ZPopover>
</template>
</nav>
@@ -626,14 +615,14 @@ onBeforeUnmount(() => {
<div class="mx-4 mb-6 shrink-0" :class="isCollapsed ? 'w-11' : 'self-stretch'">
<div
class="bg-cp-fill-quaternary"
:class="isCollapsed ? 'grid gap-1 rounded-cp p-1' : 'grid h-11 grid-cols-[4.25rem_auto] items-center justify-between rounded-cp-lg px-2'"
class="bg-z-fill-quaternary"
:class="isCollapsed ? 'grid gap-1 rounded-z p-1' : 'grid h-11 grid-cols-[4.25rem_auto] items-center justify-between rounded-z-lg px-2'"
>
<span
v-if="!isCollapsed"
class="inline-flex h-7 items-center justify-self-start gap-1.5 whitespace-nowrap rounded-lg bg-cp-success-container px-2 text-xs leading-none font-emphasis text-cp-success-on-container"
class="inline-flex h-7 items-center justify-self-start gap-1.5 whitespace-nowrap rounded-lg bg-z-success-container px-2 text-xs leading-none font-emphasis text-z-success-on-container"
>
<i class="size-1.5 rounded-full bg-cp-success" />
<i class="size-1.5 rounded-full bg-z-success" />
{{ t('navigation.online') }}
</span>
@@ -233,10 +233,10 @@ onScopeDispose(() => {
>
<button
type="button"
class="inline-flex size-12 touch-none select-none items-center justify-center rounded-cp border-0 bg-cp-fill-secondary text-cp-text shadow-cp-tertiary outline-none transition-[background-color,box-shadow,color,opacity,transform] duration-150 [-webkit-tap-highlight-color:transparent] hover:bg-cp-bg-text-active active:scale-95 active:bg-cp-control-item-bg-active focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-layout"
class="inline-flex size-12 touch-none select-none items-center justify-center rounded-z border-0 bg-z-fill-secondary text-z-text shadow-z-tertiary outline-none transition-[background-color,box-shadow,color,opacity,transform] duration-150 [-webkit-tap-highlight-color:transparent] hover:bg-z-bg-text-active active:scale-95 active:bg-z-control-item-bg-active focus-visible:ring-2 focus-visible:ring-z-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-z-bg-layout"
:class="
isDragging
? 'scale-95 cursor-grabbing shadow-[0_14px_28px_-20px_var(--cp-color-shadow)]'
? 'scale-95 cursor-grabbing shadow-[0_14px_28px_-20px_var(--z-color-shadow)]'
: 'cursor-grab'
"
:aria-label="t('navigation.sidebar.open')"
@@ -292,22 +292,22 @@ watch(
:show-close="!restarting" :close-on-click-modal="!restarting" :close-on-press-escape="!restarting"
>
<template #icon>
<ArrowUpCircle class="size-4.5 text-cp-success" />
<ArrowUpCircle class="size-4.5 text-z-success" />
</template>
<div class="grid gap-3.5">
<section class="grid gap-4 rounded-cp-card bg-cp-fill-quaternary px-4 py-4">
<section class="grid gap-4 rounded-z-card bg-z-fill-quaternary px-4 py-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="min-w-0">
<p class="m-0 text-cp-xs leading-none font-heavy text-cp-text-quaternary">
<p class="m-0 text-z-xs leading-none font-heavy text-z-text-quaternary">
Codex Proxy RS
</p>
<p class="mt-2 mb-0 text-lg leading-none font-heavy text-cp-text">
<p class="mt-2 mb-0 text-lg leading-none font-heavy text-z-text">
{{ t('systemUpdate.application') }}
</p>
</div>
<span
class="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-cp-sm font-heavy"
class="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-z-sm font-heavy"
:class="presentation.status.badge"
>
<component
@@ -323,10 +323,10 @@ watch(
<div
v-for="item in presentation.summaryItems"
:key="item.key"
class="min-w-0 rounded-cp bg-cp-bg-container px-3 py-2.5"
class="min-w-0 rounded-z bg-z-bg-container px-3 py-2.5"
>
<div class="flex min-w-0 items-center justify-between gap-2">
<p class="m-0 truncate text-cp-xs leading-none font-heavy text-cp-text-quaternary">
<p class="m-0 truncate text-z-xs leading-none font-heavy text-z-text-quaternary">
{{ item.label }}
</p>
<a
@@ -335,14 +335,14 @@ watch(
target="_blank"
rel="noreferrer"
:class="{ invisible: checking || changingChannel }"
class="inline-flex shrink-0 items-center gap-1 text-cp-xs leading-none font-bold text-cp-link transition-colors hover:text-cp-link-hover"
class="inline-flex shrink-0 items-center gap-1 text-z-xs leading-none font-bold text-z-link transition-colors hover:text-z-link-hover"
>
{{ t('systemUpdate.releasePage') }}
<ExternalLink class="size-3" />
</a>
</div>
<p
class="mt-2 mb-0 h-[1em] truncate font-mono text-cp leading-none font-bold text-cp-text"
class="mt-2 mb-0 h-[1em] truncate font-mono text-z leading-none font-bold text-z-text"
:title="item.title || item.value"
>
<ZSkeleton
@@ -357,12 +357,12 @@ watch(
</div>
</div>
<p v-if="updateInfo?.unsupportedReason" class="m-0 text-cp-sm text-cp-text-secondary">
<p v-if="updateInfo?.unsupportedReason" class="m-0 text-z-sm text-z-text-secondary">
{{ updateInfo.unsupportedReason }}
</p>
<p
v-if="updateError || updateInfo?.warning"
class="m-0 rounded-cp bg-cp-error-container px-3 py-2 text-cp-sm leading-normal font-bold text-cp-error-on-container"
class="m-0 rounded-z bg-z-error-container px-3 py-2 text-z-sm leading-normal font-bold text-z-error-on-container"
>
{{ updateError || updateInfo?.warning }}
</p>
@@ -375,12 +375,12 @@ watch(
{{ t('systemUpdate.status.lastFailed') }}
</ZButton>
</template>
<div class="grid w-80 max-w-[calc(100vw-2rem)] gap-2 p-3 text-cp-sm">
<div v-if="lastFailedOperation.targetVersion || lastFailedOperation.finishedAt" class="flex flex-wrap gap-x-3 gap-y-1 text-cp-xs text-cp-text-quaternary">
<div class="grid w-80 max-w-[calc(100vw-2rem)] gap-2 p-3 text-z-sm">
<div v-if="lastFailedOperation.targetVersion || lastFailedOperation.finishedAt" class="flex flex-wrap gap-x-3 gap-y-1 text-z-xs text-z-text-quaternary">
<span v-if="lastFailedOperation.targetVersion">{{ t('systemUpdate.targetVersion', { version: lastFailedOperation.targetVersion }) }}</span>
<span v-if="lastFailedOperation.finishedAt">{{ lastFailedOperation.finishedAtDisplay }}</span>
</div>
<p class="m-0 wrap-anywhere text-cp-text-secondary">
<p class="m-0 wrap-anywhere text-z-text-secondary">
{{ lastFailedOperation.error || lastFailedOperation.message || t('common.errors.operation') }}
</p>
</div>
@@ -389,13 +389,13 @@ watch(
<section
v-if="hasReleaseNotes || releaseNotesLoading"
class="grid gap-2 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5"
class="grid gap-2 rounded-z-card bg-z-fill-quaternary px-4 py-3.5"
>
<div class="flex items-center justify-between gap-3">
<p class="m-0 text-cp font-heavy text-cp-text">
<p class="m-0 text-z font-heavy text-z-text">
{{ t('systemUpdate.releaseNotes') }}
</p>
<span class="font-mono text-cp-xs font-emphasis text-cp-text-quaternary">
<span class="font-mono text-z-xs font-emphasis text-z-text-quaternary">
<ZSkeleton v-if="releaseNotesLoading" shape="text" class="w-14" aria-hidden="true" />
<template v-else>{{ presentation.releaseVersion }}</template>
</span>
@@ -420,22 +420,22 @@ watch(
<section
v-if="showUpdateProgress"
class="overflow-hidden rounded-cp-card bg-cp-fill-quaternary"
class="overflow-hidden rounded-z-card bg-z-fill-quaternary"
>
<header class="flex items-center justify-between gap-3 px-4 pt-3.5 pb-2.5">
<div class="flex min-w-0 items-center gap-2">
<Terminal class="size-4 shrink-0 text-cp-success" />
<p class="m-0 text-cp leading-none font-heavy text-cp-text">
<Terminal class="size-4 shrink-0 text-z-success" />
<p class="m-0 text-z leading-none font-heavy text-z-text">
{{ t('systemUpdate.progress') }}
</p>
</div>
<span
class="inline-flex h-6 items-center gap-1.5 rounded-full bg-cp-fill-quaternary px-2 text-cp-xs leading-none font-bold text-cp-text-secondary"
class="inline-flex h-6 items-center gap-1.5 rounded-full bg-z-fill-quaternary px-2 text-z-xs leading-none font-bold text-z-text-secondary"
:title="updateStreamError || presentation.streamStatusLabel"
>
<i
class="size-1.5 rounded-full"
:class="updateStreaming ? 'bg-cp-success' : 'bg-cp-text-quaternary'"
:class="updateStreaming ? 'bg-z-success' : 'bg-z-text-quaternary'"
/>
{{ presentation.streamStatusLabel }}
</span>
@@ -449,16 +449,16 @@ watch(
<div
v-for="log in updateLogRows"
:key="log.id"
class="grid grid-cols-[68px_14px_minmax(0,1fr)] items-start gap-2 rounded-cp bg-cp-bg-container px-3 py-2 font-mono text-cp-xs leading-[1.55]"
class="grid grid-cols-[68px_14px_minmax(0,1fr)] items-start gap-2 rounded-z bg-z-bg-container px-3 py-2 font-mono text-z-xs leading-[1.55]"
>
<span class="tabular-nums text-cp-text-quaternary">{{ log.time }}</span>
<span class="tabular-nums text-z-text-quaternary">{{ log.time }}</span>
<Circle
class="mt-1 size-2.5"
:class="log.classes.marker"
fill="currentColor"
/>
<p class="m-0 min-w-0 wrap-break-word" :class="log.classes.text">
<span v-if="log.step" class="mr-1 text-cp-text-quaternary">[{{ log.step }}]</span>
<span v-if="log.step" class="mr-1 text-z-text-quaternary">[{{ log.step }}]</span>
{{ log.message }}
</p>
</div>
@@ -485,12 +485,12 @@ watch(
<CircleHelp class="size-4" />
</ZButton>
</template>
<div class="grid w-72 gap-2 p-3 text-cp-sm text-cp-text-secondary">
<div class="grid w-72 gap-2 p-3 text-z-sm text-z-text-secondary">
<p v-for="channel in availableChannels" :key="channel" class="m-0">
<strong class="font-heavy text-cp-text">{{ channelLabels[channel].label }}</strong>
<strong class="font-heavy text-z-text">{{ channelLabels[channel].label }}</strong>
· {{ channelLabels[channel].description }}
</p>
<p class="m-0 text-cp-text-quaternary">
<p class="m-0 text-z-text-quaternary">
{{ t('systemUpdate.channels.description') }}
</p>
</div>
@@ -545,20 +545,20 @@ watch(
@confirm="handleConfirmRestart"
>
<div class="grid gap-4">
<div v-if="restartPlan?.targetVersion" class="flex items-center justify-between gap-3 text-cp-sm">
<span class="text-cp-text-secondary">{{ t('systemUpdate.versionAfterRestart') }}</span>
<span class="font-mono font-emphasis text-cp-text">v{{ restartPlan.targetVersion }}</span>
<div v-if="restartPlan?.targetVersion" class="flex items-center justify-between gap-3 text-z-sm">
<span class="text-z-text-secondary">{{ t('systemUpdate.versionAfterRestart') }}</span>
<span class="font-mono font-emphasis text-z-text">v{{ restartPlan.targetVersion }}</span>
</div>
<ul class="m-0 max-h-64 list-none overflow-y-auto p-0" :aria-label="t('systemUpdate.plugins.incompatible')">
<li v-for="plugin in restartPlan?.incompatiblePlugins" :key="plugin.instanceId" class="flex items-start gap-3 py-3 first:pt-0 last:pb-0">
<TriangleAlert class="mt-0.5 size-4 shrink-0 text-cp-warning" aria-hidden="true" />
<TriangleAlert class="mt-0.5 size-4 shrink-0 text-z-warning" aria-hidden="true" />
<div class="grid min-w-0 gap-1">
<strong class="wrap-anywhere text-cp-sm font-heavy text-cp-text">{{ plugin.name }}</strong>
<span class="wrap-anywhere text-cp-xs leading-relaxed font-normal text-cp-text-secondary">{{ plugin.reason }}</span>
<strong class="wrap-anywhere text-z-sm font-heavy text-z-text">{{ plugin.name }}</strong>
<span class="wrap-anywhere text-z-xs leading-relaxed font-normal text-z-text-secondary">{{ plugin.reason }}</span>
</div>
</li>
</ul>
<p class="m-0 rounded-cp bg-cp-fill-quaternary px-3 py-2.5 text-cp-sm leading-relaxed font-normal text-cp-text-secondary">
<p class="m-0 rounded-z bg-z-fill-quaternary px-3 py-2.5 text-z-sm leading-relaxed font-normal text-z-text-secondary">
{{ t('systemUpdate.plugins.requestErrorHelp') }}
</p>
</div>
@@ -574,23 +574,23 @@ watch(
@confirm="handleConfirmUpdate"
>
<div class="grid gap-3">
<div class="grid gap-2 rounded-cp bg-cp-fill-quaternary p-3">
<div class="grid gap-2 rounded-z bg-z-fill-quaternary p-3">
<div
v-for="item in presentation.confirmRows"
:key="item.key"
class="flex min-w-0 items-center justify-between gap-3"
>
<span class="text-cp-sm leading-none font-bold text-cp-text-quaternary">
<span class="text-z-sm leading-none font-bold text-z-text-quaternary">
{{ item.label }}
</span>
<span
class="truncate font-mono text-cp leading-none font-heavy text-cp-text"
class="truncate font-mono text-z leading-none font-heavy text-z-text"
>
{{ item.value }}
</span>
</div>
</div>
<p class="m-0 text-cp-sm leading-relaxed font-emphasis text-cp-text-quaternary">
<p class="m-0 text-z-sm leading-relaxed font-emphasis text-z-text-quaternary">
{{ t('systemUpdate.confirmation.description') }}
</p>
</div>
@@ -38,13 +38,13 @@ export function resolveSystemUpdatePresentation(input: SystemUpdatePresentationI
export function resolveSystemUpdateLogClasses(level: string) {
switch (level) {
case 'success':
return { marker: 'text-cp-success', text: 'text-cp-success' }
return { marker: 'text-z-success', text: 'text-z-success' }
case 'warning':
return { marker: 'text-cp-warning', text: 'text-cp-warning' }
return { marker: 'text-z-warning', text: 'text-z-warning' }
case 'error':
return { marker: 'text-cp-error', text: 'text-cp-error' }
return { marker: 'text-z-error', text: 'text-z-error' }
default:
return { marker: 'text-cp-info', text: 'text-cp-text' }
return { marker: 'text-z-info', text: 'text-z-text' }
}
}
@@ -57,16 +57,16 @@ function resolveStatus(input: SystemUpdatePresentationInput) {
? t('systemUpdate.status.updating')
: t('systemUpdate.status.checking'),
icon: RefreshCw,
badge: 'bg-cp-info-container text-cp-info-on-container',
iconClass: 'text-cp-info',
badge: 'bg-z-info-container text-z-info-on-container',
iconClass: 'text-z-info',
}
}
if (input.updateError || input.updateInfo?.warning) {
return {
label: t('common.status.error'),
icon: XCircle,
badge: 'bg-cp-error-container text-cp-error-on-container',
iconClass: 'text-cp-error',
badge: 'bg-z-error-container text-z-error-on-container',
iconClass: 'text-z-error',
}
}
if (input.needRestart || input.hasUpdate || input.updateInfo) {
@@ -77,15 +77,15 @@ function resolveStatus(input: SystemUpdatePresentationInput) {
? t('systemUpdate.status.available')
: t('systemUpdate.status.upToDate'),
icon: input.hasUpdate ? ArrowUpCircle : CheckCircle2,
badge: 'bg-cp-success-container text-cp-success-on-container',
iconClass: 'text-cp-success',
badge: 'bg-z-success-container text-z-success-on-container',
iconClass: 'text-z-success',
}
}
return {
label: t('systemUpdate.status.notChecked'),
icon: CheckCircle2,
badge: 'bg-cp-fill-tertiary text-cp-text-secondary',
iconClass: 'text-cp-text-quaternary',
badge: 'bg-z-fill-tertiary text-z-text-secondary',
iconClass: 'text-z-text-quaternary',
}
}
+1 -1
View File
@@ -67,7 +67,7 @@ watch(
</script>
<template>
<div class="relative flex h-dvh overflow-hidden bg-cp-bg-layout" :inert="mobileSidebarOpen">
<div class="relative flex h-dvh overflow-hidden bg-z-bg-layout" :inert="mobileSidebarOpen">
<AppSidebar
:collapsed="sidebarCollapsed"
@toggle="toggleSidebar"
+36 -36
View File
@@ -1,7 +1,7 @@
// @env browser
import type {
ResolvedTheme,
Theme,
ThemeColorId,
ThemeCustomization,
ThemeMode,
@@ -9,15 +9,15 @@ import type {
import { normalizeHexColor } from '@codex-proxy/ui'
import {
applyResolvedTheme,
DEFAULT_CUSTOM_THEME_COLOR,
DEFAULT_THEME_COLOR,
DEFAULT_THEME_MODE,
applyTheme,
createTheme,
defaultThemeColorId,
defaultThemeMode,
defaultThemePrimaryColor,
isThemeColorId,
isThemeMode,
normalizeThemeCustomization,
resolveTheme,
resolveThemeName,
resolveThemeScheme,
} from '@codex-proxy/ui/theme'
import { usePreferredDark, usePreferredReducedMotion, useTimeoutFn } from '@vueuse/core'
@@ -48,9 +48,9 @@ type ViewTransitionDocument = Document & {
export const useThemeStore = defineStore(
'theme',
() => {
const themeMode = shallowRef<ThemeMode>(DEFAULT_THEME_MODE)
const themeColor = shallowRef<ThemeColorId>(DEFAULT_THEME_COLOR)
const customThemeColor = shallowRef(DEFAULT_CUSTOM_THEME_COLOR)
const themeMode = shallowRef<ThemeMode>(defaultThemeMode)
const themeColor = shallowRef<ThemeColorId>(defaultThemeColorId)
const customThemeColor = shallowRef(defaultThemePrimaryColor)
const themeCustomization = ref<ThemeCustomization>({})
const themeRevision = shallowRef(0)
const themeTransitioning = shallowRef(false)
@@ -60,14 +60,14 @@ export const useThemeStore = defineStore(
let themeTransitionOrigin: ThemeTransitionOrigin | undefined
let themeTransitionRequested = false
const effectiveTheme = computed(() => resolveThemeName(themeMode.value, preferredDark.value))
const effectiveTheme = computed(() => resolveThemeScheme(themeMode.value, preferredDark.value))
const resolvedTheme = computed(() =>
resolveTheme(
effectiveTheme.value,
themeColor.value,
customThemeColor.value,
themeCustomization.value,
),
createTheme({
scheme: effectiveTheme.value,
colorId: themeColor.value,
primaryColor: customThemeColor.value,
customization: themeCustomization.value,
}),
)
const { start: startFallbackTransitionTimer, stop: stopFallbackTransitionTimer } = useTimeoutFn(
@@ -79,23 +79,23 @@ export const useThemeStore = defineStore(
{ immediate: false },
)
watch(resolvedTheme, theme => applyTheme(theme))
watch(resolvedTheme, theme => syncTheme(theme))
function initializeTheme(): void {
normalizePersistedTheme()
applyTheme(resolvedTheme.value)
syncTheme(resolvedTheme.value)
}
function normalizePersistedTheme(): void {
if (!isThemeMode(themeMode.value))
themeMode.value = DEFAULT_THEME_MODE
themeMode.value = defaultThemeMode
if (!isThemeColorId(themeColor.value))
themeColor.value = DEFAULT_THEME_COLOR
customThemeColor.value = normalizeHexColor(customThemeColor.value) ?? DEFAULT_CUSTOM_THEME_COLOR
themeColor.value = defaultThemeColorId
customThemeColor.value = normalizeHexColor(customThemeColor.value) ?? defaultThemePrimaryColor
themeCustomization.value = normalizeThemeCustomization(themeCustomization.value)
}
function applyTheme(theme: ResolvedTheme): void {
function syncTheme(theme: Theme): void {
if (typeof document === 'undefined')
return
@@ -120,16 +120,16 @@ export const useThemeStore = defineStore(
clearThemeTransitionRequest()
}
function commitTheme(theme: ResolvedTheme): void {
applyResolvedTheme(document.documentElement, theme)
function commitTheme(theme: Theme): void {
applyTheme(document.documentElement, theme)
appliedThemeSignature = themeSignature(theme)
themeRevision.value += 1
}
function runThemeTransition(theme: ResolvedTheme): void {
function runThemeTransition(theme: Theme): void {
const transitionDocument = document as ViewTransitionDocument
const shrinkingDarkLayer
= document.documentElement.dataset.theme === 'dark' && theme.name === 'light'
= document.documentElement.dataset.theme === 'dark' && theme.scheme === 'light'
if (!transitionDocument.startViewTransition) {
runFallbackThemeTransition(theme)
@@ -192,7 +192,7 @@ export const useThemeStore = defineStore(
.catch(() => undefined)
}
function runFallbackThemeTransition(theme: ResolvedTheme): void {
function runFallbackThemeTransition(theme: Theme): void {
themeTransitioning.value = true
stopFallbackTransitionTimer()
document.documentElement.classList.add('theme-fallback-transition')
@@ -250,18 +250,18 @@ export const useThemeStore = defineStore(
function resetTheme(event?: MouseEvent): void {
const unchanged
= themeMode.value === DEFAULT_THEME_MODE
&& themeColor.value === DEFAULT_THEME_COLOR
&& customThemeColor.value === DEFAULT_CUSTOM_THEME_COLOR
= themeMode.value === defaultThemeMode
&& themeColor.value === defaultThemeColorId
&& customThemeColor.value === defaultThemePrimaryColor
&& Object.keys(themeCustomization.value).length === 0
if (unchanged) {
clearThemeTransitionRequest()
return
}
requestThemeTransition(event)
themeMode.value = DEFAULT_THEME_MODE
themeColor.value = DEFAULT_THEME_COLOR
customThemeColor.value = DEFAULT_CUSTOM_THEME_COLOR
themeMode.value = defaultThemeMode
themeColor.value = defaultThemeColorId
customThemeColor.value = defaultThemePrimaryColor
themeCustomization.value = {}
}
@@ -316,6 +316,6 @@ export const useThemeStore = defineStore(
},
)
function themeSignature(theme: ResolvedTheme): string {
return `${theme.name}:${theme.colorId}:${theme.seed}:${JSON.stringify(theme.tokens)}`
function themeSignature(theme: Theme): string {
return `${theme.scheme}:${theme.colorId}:${theme.seed}:${JSON.stringify(theme.tokens)}`
}
+4 -4
View File
@@ -2,10 +2,10 @@ type PresetVisualTone = 'blue' | 'cyan' | 'green' | 'orange'
const presetVisualTones: readonly PresetVisualTone[] = ['blue', 'green', 'orange', 'cyan']
const presetVisualToneClasses = {
blue: 'bg-cp-blue-container-strong text-cp-blue-on-container',
cyan: 'bg-cp-cyan-container-strong text-cp-cyan-on-container',
green: 'bg-cp-green-container-strong text-cp-green-on-container',
orange: 'bg-cp-orange-container-strong text-cp-orange-on-container',
blue: 'bg-z-blue-container-strong text-z-blue-on-container',
cyan: 'bg-z-cyan-container-strong text-z-cyan-on-container',
green: 'bg-z-green-container-strong text-z-green-on-container',
orange: 'bg-z-orange-container-strong text-z-orange-on-container',
} as const satisfies Record<PresetVisualTone, string>
export function stableVisualIndex(value: unknown, length: number): number {
-7
View File
@@ -1,5 +1,4 @@
import type { RequestLocation } from '@/api'
import { t } from '@/i18n'
export function normalizeRequestLocation(location: RequestLocation): RequestLocation {
return {
@@ -9,9 +8,3 @@ export function normalizeRequestLocation(location: RequestLocation): RequestLoca
timezone: location.timezone.trim(),
}
}
export function requestLocationError(location: RequestLocation): string {
if (!/^[A-Z]{2}$/.test(location.country) || !location.region || !location.city || !location.timezone)
return t('common.location.required')
return ''
}
@@ -17,21 +17,19 @@ const transportOptions = [
</script>
<template>
<div class="grid gap-4">
<ZFormItem v-if="!editing" :label="t('accounts.credentials.accountName')" required>
<ZInput v-model="form.name" :placeholder="t('accounts.credentials.enterUpstreamName')" :disabled="disabled" />
</ZFormItem>
<ZFormItem :label="t('common.fields.baseUrl')" required>
<template #extra>
<span class="text-cp-xs text-cp-text-quaternary" :title="t('accounts.credentials.baseUrlHelp')">{{ t('accounts.credentials.apiPrefix') }}</span>
</template>
<ZInput v-model="form.base_url" type="url" placeholder="https://api.example.com/v1" :disabled="disabled" autocomplete="off" />
</ZFormItem>
<ZFormItem :label="t('common.fields.apiKey')" :required="!editing">
<ZInput v-model="form.apiKey" type="password" :placeholder="editing ? t('accounts.credentials.apiKeyPlaceholder') : t('accounts.credentials.enterUpstreamKey')" :disabled="disabled" autocomplete="new-password" />
</ZFormItem>
<ZFormItem :label="t('accounts.credentials.transportLabel')">
<ZSegmented v-model="form.transport" :aria-label="t('accounts.credentials.transportLabel')" :options="transportOptions" :disabled="disabled" :title="t('accounts.credentials.webSocketHelp')" />
</ZFormItem>
</div>
<ZFormItem v-if="!editing" prop="apiKey.name" :show-message="false" :label="t('accounts.credentials.accountName')" required>
<ZInput v-model="form.name" :placeholder="t('accounts.credentials.enterUpstreamName')" :disabled="disabled" />
</ZFormItem>
<ZFormItem prop="apiKey.base_url" :show-message="Boolean(form.base_url.trim())" :label="t('common.fields.baseUrl')" required>
<template #extra>
<span class="text-z-xs text-z-text-quaternary" :title="t('accounts.credentials.baseUrlHelp')">{{ t('accounts.credentials.apiPrefix') }}</span>
</template>
<ZInput v-model="form.base_url" type="url" placeholder="https://api.example.com/v1" :disabled="disabled" autocomplete="off" />
</ZFormItem>
<ZFormItem prop="apiKey.apiKey" :show-message="Boolean(form.apiKey)" :label="t('common.fields.apiKey')" :required="!editing">
<ZInput v-model="form.apiKey" type="password" :placeholder="editing ? t('accounts.credentials.apiKeyPlaceholder') : t('accounts.credentials.enterUpstreamKey')" :disabled="disabled" autocomplete="new-password" />
</ZFormItem>
<ZFormItem prop="apiKey.transport" :show-message="false" :label="t('accounts.credentials.transportLabel')">
<ZSegmented v-model="form.transport" :aria-label="t('accounts.credentials.transportLabel')" :options="transportOptions" :disabled="disabled" :title="t('accounts.credentials.webSocketHelp')" />
</ZFormItem>
</template>
@@ -1,18 +1,20 @@
<script setup lang="ts">
import type { AccountGroupRef, AccountModelAccess } from '@/api'
import { ZButton, ZDialog, ZFormItem, ZSegmented } from '@codex-proxy/ui'
import { ZButton, ZDialog, ZForm, ZFormItem, ZSegmented } from '@codex-proxy/ui'
import { computed } from 'vue'
import { computed, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAccountSettingsFormRules } from '../composables/useAccountSettingsFormRules'
import AccountSettingsFields from './AccountSettingsFields.vue'
defineProps<{
const props = defineProps<{
selectedCount: number
catalogAccountId?: string
groups: AccountGroupRef[]
groupsLoading: boolean
saving: boolean
editedFields: ReadonlySet<string>
}>()
const emit = defineEmits<{
@@ -29,6 +31,24 @@ const weight = defineModel<string>('weight', { required: true })
const proxyMode = defineModel<string>('proxyMode', { required: true })
const proxyId = defineModel<string>('proxyId', { required: true })
const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: true })
const formRef = useTemplateRef<InstanceType<typeof ZForm>>('formElement')
const form = computed(() => ({
concurrencyLimit: concurrencyLimit.value,
weight: weight.value,
modelAccess: modelAccess.value,
proxyMode: proxyMode.value,
proxyId: proxyId.value,
}))
const rules = useAccountSettingsFormRules(() => form.value, () => props.editedFields)
watch(open, () => formRef.value?.clearValidate())
async function requestSave() {
if (props.saving || !props.selectedCount || props.groupsLoading || !await formRef.value?.validate().catch(() => false))
return
emit('save')
}
const schedulingModes = computed(() => [
{
value: 'preserve',
@@ -63,7 +83,7 @@ const schedulingMode = computed({
width="48rem"
:show-close="!saving" :close-on-click-modal="!saving" :close-on-press-escape="!saving"
>
<div class="grid gap-5">
<ZForm ref="formElement" :model="form" :rules="rules" scroll-to-error focus-on-error novalidate @submit="requestSave">
<AccountSettingsFields
v-model:concurrency-limit="concurrencyLimit"
v-model:weight="weight"
@@ -72,6 +92,7 @@ const schedulingMode = computed({
v-model:proxy-mode="proxyMode"
v-model:proxy-id="proxyId"
preserve-model-access
:account-id="catalogAccountId"
:groups="groups"
:groups-loading="groupsLoading"
@@ -89,7 +110,7 @@ const schedulingMode = computed({
</ZFormItem>
</template>
</AccountSettingsFields>
</div>
</ZForm>
<template #footer>
<ZButton :disabled="saving" @click="open = false">
@@ -99,7 +120,7 @@ const schedulingMode = computed({
type="primary"
:loading="saving"
:disabled="selectedCount === 0 || groupsLoading"
@click="emit('save')"
@click="requestSave"
>
{{ t('common.actions.save') }}
</ZButton>
@@ -25,7 +25,7 @@ const label = computed(() => t('accounts.capacity.concurrentRequestsLimit', { va
variant="text" size="small" :type="full ? 'warning' : 'default'"
:aria-label="t('accounts.capacity.viewConcurrencyDetails', { label })"
:title="t('accounts.capacity.viewDetails', { label })"
class="relative top-0.75 -m-0.5 h-auto! px-0.5! py-0.5! whitespace-nowrap font-mono text-[9px]! font-normal! tabular-nums hover:bg-cp-bg-text-hover!"
class="relative top-0.75 -m-0.5 h-auto! px-0.5! py-0.5! whitespace-nowrap font-mono text-[9px]! font-normal! tabular-nums hover:bg-z-bg-text-hover!"
@click.stop="$emit('showDetails')"
>
<span aria-hidden="true" class="inline-flex items-center gap-1">
@@ -53,27 +53,27 @@ const columns = computed(() => (defineTableColumns<AccountConcurrencyEntry>([
<template>
<ZTable
class="h-[min(60dvh,20rem)]! min-h-0 [--cp-table-row-height:3rem]"
class="h-[min(60dvh,20rem)]! min-h-0 [--z-table-row-height:3rem]"
:columns="columns" :data="entries" row-key="leaseId"
:loading="loading" :empty-text="emptyText"
>
<template #key="{ row }">
<span
class="block truncate font-emphasis text-cp-text"
class="block truncate font-emphasis text-z-text"
:title="[row.clientKeyName, row.clientKeyId].filter(Boolean).join('\n') || undefined"
>
{{ row.clientKeyName ?? row.clientKeyId ?? '—' }}
</span>
</template>
<template #model="{ displayValue }">
<span class="text-cp font-normal text-cp-text-secondary" :title="String(displayValue)">{{ displayValue }}</span>
<span class="text-z font-normal text-z-text-secondary" :title="String(displayValue)">{{ displayValue }}</span>
</template>
<template #startedAtDisplay="{ displayValue }">
<span class="text-cp">{{ displayValue }}</span>
<span class="text-z">{{ displayValue }}</span>
</template>
<template #clientIp="{ row }">
<UsageClientIpCell v-if="row.clientIp" :record="row" class="text-cp!" />
<span v-else class="text-cp-text-quaternary">—</span>
<UsageClientIpCell v-if="row.clientIp" :record="row" class="text-z!" />
<span v-else class="text-z-text-quaternary">—</span>
</template>
</ZTable>
</template>
@@ -37,12 +37,12 @@ const selectedModel = defineModel<string>('selectedModel', { required: true })
function connectionLogClass(tone: string) {
if (tone === 'success')
return 'text-cp-success-text'
return 'text-z-success-text'
if (tone === 'danger')
return 'text-cp-error-text'
return 'text-z-error-text'
if (tone === 'info')
return 'text-cp-info-text'
return 'text-cp-text-secondary'
return 'text-z-info-text'
return 'text-z-text-secondary'
}
</script>
@@ -56,7 +56,7 @@ function connectionLogClass(tone: string) {
>
<div v-if="account" class="flex flex-col gap-4">
<section
class="flex items-center justify-between gap-4 rounded-cp-card bg-cp-fill-quaternary px-4 py-3"
class="flex items-center justify-between gap-4 rounded-z-card bg-z-fill-quaternary px-4 py-3"
>
<AccountIdentityCell :account="account" size="lg" show-plan show-notes />
<AccountStatusBadge
@@ -73,10 +73,10 @@ function connectionLogClass(tone: string) {
/>
</section>
<section class="rounded-cp-card bg-cp-fill-quaternary px-4 py-3">
<section class="rounded-z-card bg-z-fill-quaternary px-4 py-3">
<div class="grid gap-2">
<div class="flex min-h-8 items-center justify-between gap-3">
<span class="text-cp-sm font-heavy text-cp-text-quaternary">
<span class="text-z-sm font-heavy text-z-text-quaternary">
{{ t('accounts.connectionTest.model') }}
</span>
<ZIconButton
@@ -103,7 +103,7 @@ function connectionLogClass(tone: string) {
</div>
</section>
<section class="rounded-cp-card bg-cp-fill-quaternary p-4">
<section class="rounded-z-card bg-z-fill-quaternary p-4">
<div class="flex min-w-0 items-start gap-3">
<span
class="inline-flex size-10 shrink-0 items-center justify-center rounded-lg"
@@ -116,11 +116,11 @@ function connectionLogClass(tone: string) {
/>
</span>
<div class="min-w-0">
<p class="m-0 text-[16px] font-heavy text-cp-text">
<p class="m-0 text-[16px] font-heavy text-z-text">
{{ statusView.label }}
</p>
<p
class="mt-1.5 mb-0 text-cp leading-normal font-emphasis text-cp-text-secondary"
class="mt-1.5 mb-0 text-z leading-normal font-emphasis text-z-text-secondary"
>
{{ statusView.description }}
</p>
@@ -128,59 +128,59 @@ function connectionLogClass(tone: string) {
</div>
<div class="mt-4 grid gap-3 sm:grid-cols-3">
<div class="rounded-lg bg-cp-bg-container px-3 py-2.5">
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
<div class="rounded-lg bg-z-bg-container px-3 py-2.5">
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
{{ t('accounts.concurrency.startTime') }}
</p>
<p class="mt-1.5 mb-0 font-mono text-cp-sm font-emphasis text-cp-text">
<p class="mt-1.5 mb-0 font-mono text-z-sm font-emphasis text-z-text">
{{ startedAt || '-' }}
</p>
</div>
<div class="rounded-lg bg-cp-bg-container px-3 py-2.5">
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
<div class="rounded-lg bg-z-bg-container px-3 py-2.5">
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
{{ t('accounts.connectionTest.completedAt') }}
</p>
<p class="mt-1.5 mb-0 font-mono text-cp-sm font-emphasis text-cp-text">
<p class="mt-1.5 mb-0 font-mono text-z-sm font-emphasis text-z-text">
{{ finishedAt || '-' }}
</p>
</div>
<div class="rounded-lg bg-cp-bg-container px-3 py-2.5">
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
<div class="rounded-lg bg-z-bg-container px-3 py-2.5">
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
{{ t('accounts.connectionTest.duration') }}
</p>
<p class="mt-1.5 mb-0 font-mono text-cp-sm font-emphasis whitespace-nowrap text-cp-text">
<p class="mt-1.5 mb-0 font-mono text-z-sm font-emphasis whitespace-nowrap text-z-text">
{{ durationMs !== null ? `${durationMs} ms` : '-' }}
</p>
</div>
</div>
<div class="mt-3 rounded-lg bg-cp-bg-container px-3 py-2.5">
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
<div class="mt-3 rounded-lg bg-z-bg-container px-3 py-2.5">
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
{{ t('accounts.connectionTest.model') }}
</p>
<p
class="mt-1.5 mb-0 truncate font-mono text-cp-sm font-emphasis text-cp-text"
class="mt-1.5 mb-0 truncate font-mono text-z-sm font-emphasis text-z-text"
:title="model || '-'"
>
{{ model || '-' }}
</p>
</div>
<div class="mt-3 rounded-lg bg-cp-bg-container px-3 py-2.5">
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
<div class="mt-3 rounded-lg bg-z-bg-container px-3 py-2.5">
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
{{ t('accounts.connectionTest.events.title') }}
</p>
<div class="pt-2">
<div v-if="logs.length === 0" class="text-cp-sm font-emphasis text-cp-text-quaternary">
<div v-if="logs.length === 0" class="text-z-sm font-emphasis text-z-text-quaternary">
-
</div>
<div v-else class="flex flex-col gap-1.5">
<div
v-for="item in logs"
:key="item.key"
class="grid grid-cols-[54px_minmax(0,1fr)] gap-2 text-cp-sm leading-[1.45] font-emphasis"
class="grid grid-cols-[54px_minmax(0,1fr)] gap-2 text-z-sm leading-[1.45] font-emphasis"
>
<span class="font-mono text-cp-text-quaternary">{{ item.time }}</span>
<span class="font-mono text-z-text-quaternary">{{ item.time }}</span>
<div class="min-w-0">
<p
class="m-0 wrap-break-word"
@@ -188,15 +188,15 @@ function connectionLogClass(tone: string) {
>
{{ item.text }}
</p>
<div v-if="item.detail" class="mt-2 rounded-lg bg-cp-fill-quaternary px-3 py-2">
<div v-if="item.detail" class="mt-2 rounded-lg bg-z-fill-quaternary px-3 py-2">
<p
v-if="item.tone === 'danger'"
class="mt-0 mb-2 text-cp-xs font-heavy text-cp-text-quaternary"
class="mt-0 mb-2 text-z-xs font-heavy text-z-text-quaternary"
>
{{ t('accounts.connectionTest.diagnostics') }}
</p>
<pre
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-cp-xs leading-[1.6] font-emphasis text-cp-text"
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-z-xs leading-[1.6] font-emphasis text-z-text"
v-text="item.detail"
/>
</div>
@@ -1,19 +1,24 @@
<script setup lang="ts">
import type { FormRules } from '@codex-proxy/ui'
import type { ApiKeyAccountForm } from '../utils/upstreamApiKey'
import type { Account, AccountGroupRef, AccountModelAccess } from '@/api'
import { ZButton, ZDialog, ZFormItem, ZInput, ZSegmented, ZTextarea } from '@codex-proxy/ui'
import { ZButton, ZDialog, ZForm, ZFormItem, ZInput, ZSegmented, ZTextarea } from '@codex-proxy/ui'
import { computed, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import AccountPlanBadge from '@/components/account/AccountPlanBadge.vue'
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
import { useAccountApiKeyFormRules } from '../composables/useAccountApiKeyFormRules'
import { useAccountSettingsFormRules } from '../composables/useAccountSettingsFormRules'
import { accountNameUpdate } from '../utils/accountName'
import { isOpenAiApiKeyAccount, isOpenAiOAuthAccount } from '../utils/upstreamApiKey'
import AccountApiKeyFields from './AccountApiKeyFields.vue'
import AccountIdentityCell from './AccountIdentityCell.vue'
import AccountSettingsFields from './AccountSettingsFields.vue'
defineProps<{
const props = defineProps<{
account: Account | null
groups: AccountGroupRef[]
groupsLoading: boolean
@@ -44,6 +49,41 @@ const weight = defineModel<string>('weight', { required: true })
const proxyMode = defineModel<string>('proxyMode', { required: true })
const proxyId = defineModel<string>('proxyId', { required: true })
const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: true })
const formRef = useTemplateRef<InstanceType<typeof ZForm>>('formElement')
const form = computed(() => ({
name: name.value,
apiKey: apiKey.value,
concurrencyLimit: concurrencyLimit.value,
weight: weight.value,
modelAccess: modelAccess.value,
proxyMode: proxyMode.value,
proxyId: proxyId.value,
}))
const settingsRules = useAccountSettingsFormRules(() => form.value)
const apiKeyRules = useAccountApiKeyFormRules(true)
const rules = computed<FormRules>(() => ({
...settingsRules.value,
...apiKeyRules.value,
name: {
trigger: ['blur', 'change'],
validator: () => {
const updatedName = accountNameUpdate(name.value, props.account?.name ?? '')
return updatedName === undefined
|| (new TextEncoder().encode(updatedName).length <= 512 && !/\p{Cc}/u.test(updatedName))
},
message: t('accounts.editor.nameInvalid'),
},
}))
const saveDisabled = computed(() => !props.account || props.groupsLoading
|| (isOpenAiApiKeyAccount(props.account) && !props.configurationReady))
watch(open, () => formRef.value?.clearValidate())
async function requestSave() {
if (props.saving || saveDisabled.value || !await formRef.value?.validate().catch(() => false))
return
emit('save')
}
</script>
<template>
@@ -54,9 +94,9 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
:show-close="!saving" :close-on-click-modal="!saving" :close-on-press-escape="!saving"
@closed="emit('afterLeave')"
>
<div v-if="account" class="grid min-w-0 grid-cols-1 gap-5">
<ZForm v-if="account" ref="formElement" :model="form" :rules="rules" scroll-to-error focus-on-error novalidate @submit="requestSave">
<div
class="flex flex-wrap items-center justify-between gap-4 rounded-cp bg-cp-fill-quaternary px-4 py-3.5"
class="flex flex-wrap items-center justify-between gap-4 rounded-z bg-z-fill-quaternary px-4 py-3.5"
>
<AccountIdentityCell
class="min-w-0 flex-1"
@@ -96,7 +136,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
:account-id="account.id"
/>
<ZFormItem :label="t('accounts.credentials.accountName')">
<ZFormItem prop="name" :label="t('accounts.credentials.accountName')">
<ZInput
v-model="name"
:aria-label="t('accounts.credentials.accountName')"
@@ -114,7 +154,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
:disabled="saving"
/>
</ZFormItem>
</div>
</ZForm>
<template #footer>
<ZButton :disabled="saving" @click="open = false">
@@ -123,8 +163,8 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
<ZButton
type="primary"
:loading="saving"
:disabled="!account || groupsLoading || (isOpenAiApiKeyAccount(account) && !configurationReady)"
@click="emit('save')"
:disabled="saveDisabled"
@click="requestSave"
>
{{ t('common.actions.save') }}
</ZButton>
@@ -68,7 +68,7 @@ const groupOptions = computed(() => [
class="col-span-2 min-w-0 sm:col-span-3 xl:w-80 xl:flex-none"
>
<template #prefix>
<Search class="size-4.5 text-cp-text-tertiary" />
<Search class="size-4.5 text-z-text-tertiary" />
</template>
</ZInput>
@@ -112,7 +112,7 @@ const groupOptions = computed(() => [
<div class="w-32 p-1.5">
<ZMenuItem @click="(close(), emit('editSelected'))">
<template #icon>
<Pencil class="size-3.5 text-cp-text-quaternary" />
<Pencil class="size-3.5 text-z-text-quaternary" />
</template>
{{ t('accounts.filters.editSelected', { count: selectedCount }) }}
</ZMenuItem>
@@ -124,7 +124,7 @@ const groupOptions = computed(() => [
@click="(close(), emit('exportSelected'))"
>
<template #icon>
<Download class="size-3.5 text-cp-text-quaternary" />
<Download class="size-3.5 text-z-text-quaternary" />
</template>
{{ t('accounts.filters.exportSelected', { count: selectedCount }) }}
</ZMenuItem>
@@ -143,7 +143,7 @@ const groupOptions = computed(() => [
</ZPopover>
<ZButton v-if="hasImportTasks" class="whitespace-nowrap" @click="emit('importTasks')">
<ListTodo class="size-4" />
{{ t('accounts.filters.importTasks') }} <span v-if="activeImportCount" class="font-mono text-cp-link">{{ activeImportCount }}</span>
{{ t('accounts.filters.importTasks') }} <span v-if="activeImportCount" class="font-mono text-z-link">{{ activeImportCount }}</span>
</ZButton>
<slot name="actions" />
<ZButton type="primary" class="whitespace-nowrap" @click="emit('create')">
@@ -44,8 +44,8 @@ const initial = computed(() => displayTitle.value.slice(0, 1).toUpperCase())
const avatarSizeClass = computed(() =>
props.size === 'lg'
? 'size-10 text-cp-xl'
: 'size-9 text-cp',
? 'size-10 text-z-xl'
: 'size-9 text-z',
)
const metaGapClass = computed(() => props.metaSize === 'xs'
@@ -68,7 +68,7 @@ const avatarToneClass = computed(() => {
</span>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<span class="min-w-0 truncate text-cp font-heavy text-cp-text" :title="identityTitle">
<span class="min-w-0 truncate text-z font-heavy text-z-text" :title="identityTitle">
{{ displayTitle }}
</span>
<span
@@ -4,7 +4,7 @@ import type { AccountModelAccess } from '@/api'
import { ZCheckbox, ZEmpty, ZFormItem, ZIconButton, ZInput, ZPopover, ZScrollbar, ZSegmented } from '@codex-proxy/ui'
import { CircleAlert, RefreshCw, Search, TriangleAlert } from '@lucide/vue'
import { computed, ref, watch } from 'vue'
import { computed, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { getAccountModels, refreshAccountModels } from '@/api'
import { useRequestState } from '@/composables/useRequestState'
@@ -14,11 +14,16 @@ const props = withDefaults(defineProps<{
accountId?: string
disabled?: boolean
allowPreserve?: boolean
formProp: string
}>(), { disabled: false, allowPreserve: false })
const { t } = useI18n({ useScope: 'global' })
const model = defineModel<AccountModelAccess | undefined>({ required: true })
const formItem = useTemplateRef<InstanceType<typeof ZFormItem>>('modelAccessItem')
watch(model, () => {
void formItem.value?.validate('change').catch(() => undefined)
})
// 未取得目录时不能判断缺失,成功返回空目录则可以
const catalog = ref<Array<{ id: string, label: string }>>()
const catalogObservedAtDisplay = ref<string>()
@@ -165,12 +170,12 @@ watch(search, () => {
</script>
<template>
<div class="grid gap-3">
<ZFormItem>
<div class="grid gap-4.5">
<ZFormItem ref="modelAccessItem" :prop="formProp">
<template #label>
<span class="inline-flex flex-wrap items-end gap-x-2 gap-y-1">
<span>{{ t('accounts.modelAccess.modelCatalog') }}</span>
<span v-if="restricted && catalogObservedAtDisplay" class="text-cp-xs leading-none font-normal text-cp-text-tertiary tabular-nums">
<span v-if="restricted && catalogObservedAtDisplay" class="text-z-xs leading-none font-normal text-z-text-tertiary tabular-nums">
{{ catalogObservedAtDisplay }}
</span>
</span>
@@ -187,10 +192,10 @@ watch(search, () => {
:title="undefined"
@keydown="handleCatalogHelpKeydown($event, open, close)"
>
<CircleAlert class="size-[1em] text-cp-xs" aria-hidden="true" />
<CircleAlert class="size-[1em] text-z-xs" aria-hidden="true" />
</ZIconButton>
</template>
<div class="max-w-72 space-y-2 px-3 py-2 text-cp-sm leading-relaxed text-cp-text-secondary">
<div class="max-w-72 space-y-2 px-3 py-2 text-z-sm leading-relaxed text-z-text-secondary">
<p class="m-0">
{{ t('accounts.modelAccess.catalogObservedAtHelp') }}
</p>
@@ -216,7 +221,7 @@ watch(search, () => {
<Search class="size-4" aria-hidden="true" />
</template>
</ZInput>
<p v-if="inputError" class="m-0 text-cp-xs text-cp-error-text" role="alert">
<p v-if="inputError" class="m-0 text-z-xs text-z-error-text" role="alert">
{{ inputError }}
</p>
<ZScrollbar v-if="models.length" max-height="15rem" class="min-w-0 -m-1">
@@ -224,8 +229,8 @@ watch(search, () => {
<div
v-for="item in models"
:key="item.id"
class="flex min-w-0 items-center rounded-cp transition-colors duration-150 motion-reduce:transition-none"
:class="model?.models.includes(item.id) ? 'bg-cp-primary-container text-cp-primary-on-container' : 'bg-cp-fill-quaternary text-cp-text-secondary hover:bg-cp-fill-tertiary hover:text-cp-text'"
class="flex min-w-0 items-center rounded-z transition-colors duration-150 motion-reduce:transition-none"
:class="model?.models.includes(item.id) ? 'bg-z-primary-container text-z-primary-on-container' : 'bg-z-fill-quaternary text-z-text-secondary hover:bg-z-fill-tertiary hover:text-z-text'"
>
<ZCheckbox
class="min-h-11 min-w-0 flex-1 px-3 py-2.5"
@@ -249,14 +254,14 @@ watch(search, () => {
:aria-expanded="open"
:title="undefined"
>
<TriangleAlert class="size-3.5 text-cp-warning-text" aria-hidden="true" />
<TriangleAlert class="size-3.5 text-z-warning-text" aria-hidden="true" />
</ZIconButton>
</template>
<div class="max-w-64 space-y-1 px-3 py-2 text-cp-sm wrap-anywhere">
<p class="m-0 font-emphasis text-cp-text">
<div class="max-w-64 space-y-1 px-3 py-2 text-z-sm wrap-anywhere">
<p class="m-0 font-emphasis text-z-text">
{{ item.id }}
</p>
<p class="m-0 text-cp-text-secondary">
<p class="m-0 text-z-text-secondary">
{{ t('accounts.modelAccess.modelCatalogMissingHint') }}
</p>
</div>
@@ -264,7 +269,7 @@ watch(search, () => {
</div>
</div>
</ZScrollbar>
<p v-else-if="loading" class="m-0 py-4 text-center text-cp-sm text-cp-text-tertiary" role="status">
<p v-else-if="loading" class="m-0 py-4 text-center text-z-sm text-z-text-tertiary" role="status">
{{ t('accounts.modelAccess.loadingModel') }}
</p>
<ZEmpty v-else-if="!error" :title="search ? t('accounts.modelAccess.noMatchingModels') : t('accounts.modelAccess.noModels')" :icon="search ? Search : undefined" size="small" surface="none" />
@@ -45,7 +45,7 @@ watch(() => props.notes, recheckOpen, { flush: 'post' })
<template #reference>
<button
type="button"
class="inline-flex min-w-0 max-w-full cursor-default touch-manipulation items-center rounded-cp-sm border-0 bg-transparent p-0 text-left font-mono text-cp-xs leading-4 font-emphasis text-cp-text-quaternary outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
class="inline-flex min-w-0 max-w-full cursor-default touch-manipulation items-center rounded-z-sm border-0 bg-transparent p-0 text-left font-mono text-z-xs leading-4 font-emphasis text-z-text-quaternary outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline"
:aria-label="t('accounts.notes.viewAccountNotes')"
aria-haspopup="dialog"
:aria-expanded="open"
@@ -55,9 +55,9 @@ watch(() => props.notes, recheckOpen, { flush: 'post' })
</button>
</template>
<section :id="detailId" class="w-max max-w-[min(20rem,calc(100vw-1rem))] overflow-hidden rounded-cp-lg" role="dialog" :aria-label="t('accounts.notes.title')">
<section :id="detailId" class="w-max max-w-[min(20rem,calc(100vw-1rem))] overflow-hidden rounded-z-lg" role="dialog" :aria-label="t('accounts.notes.title')">
<ZScrollbar max-height="min(240px, calc(100dvh - 2rem))">
<div class="px-3 py-2.5 text-cp-sm leading-5 whitespace-pre-wrap text-cp-text select-text wrap-anywhere">
<div class="px-3 py-2.5 text-z-sm leading-5 whitespace-pre-wrap text-z-text select-text wrap-anywhere">
{{ notes }}
</div>
</ZScrollbar>
@@ -47,15 +47,15 @@ const overviewItems = computed(() => [
function overviewIconClass(tone: string) {
if (tone === 'success') {
return 'bg-cp-green-container text-cp-green-on-container'
return 'bg-z-green-container text-z-green-on-container'
}
if (tone === 'warning') {
return 'bg-cp-orange-container text-cp-orange-on-container'
return 'bg-z-orange-container text-z-orange-on-container'
}
if (tone === 'danger') {
return 'bg-cp-error-container text-cp-error-on-container'
return 'bg-z-error-container text-z-error-on-container'
}
return 'bg-cp-blue-container text-cp-blue-on-container'
return 'bg-z-blue-container text-z-blue-on-container'
}
</script>
@@ -64,13 +64,13 @@ function overviewIconClass(tone: string) {
<ZCard v-for="item in overviewItems" :key="item.label" as="article" padding="compact">
<div class="flex items-stretch justify-between gap-3">
<div class="flex min-w-0 flex-col">
<p class="m-0 text-cp-sm leading-none font-heavy text-cp-text-secondary">
<p class="m-0 text-z-sm leading-none font-heavy text-z-text-secondary">
{{ item.label }}
</p>
<strong class="my-2 block font-mono text-[26px] leading-none font-extrabold text-cp-text">
<strong class="my-2 block font-mono text-[26px] leading-none font-extrabold text-z-text">
{{ item.value }}
</strong>
<p class="m-0 truncate text-cp-sm leading-none font-emphasis text-cp-text-quaternary">
<p class="m-0 truncate text-z-sm leading-none font-emphasis text-z-text-quaternary">
{{ item.caption }}
</p>
</div>
@@ -5,7 +5,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useProxyCatalog } from '@/composables/useProxyCatalog'
const props = withDefaults(defineProps<{ accountId?: string, currentProxyId?: string | null, endpoint?: string | null, disabled?: boolean, preserve?: boolean, error?: string }>(), { preserve: true })
const props = withDefaults(defineProps<{ accountId?: string, currentProxyId?: string | null, endpoint?: string | null, disabled?: boolean, preserve?: boolean, formProp: string }>(), { preserve: true })
const { t } = useI18n({ useScope: 'global' })
@@ -71,7 +71,7 @@ const selection = computed({
</script>
<template>
<ZFormItem :label="t('accounts.proxy.outboundTunnel')" :error="error">
<ZFormItem :prop="formProp" :show-message="false" :label="t('accounts.proxy.outboundTunnel')">
<ZSelect v-model="selection" class="w-full" :options="options" :disabled="disabled || loading" :aria-label="t('accounts.proxy.outboundTunnel')" />
</ZFormItem>
</template>
@@ -17,7 +17,6 @@ withDefaults(defineProps<{
preserveProxy?: boolean
preserveModelAccess?: boolean
showScheduling?: boolean
proxyError?: string
}>(), { preserveProxy: true, showScheduling: true })
const { t } = useI18n({ useScope: 'global' })
@@ -32,11 +31,11 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
</script>
<template>
<div class="grid gap-5">
<AccountModelAccessField v-model="modelAccess" :account-id="accountId" :disabled="disabled" :allow-preserve="preserveModelAccess" />
<div class="grid gap-4.5">
<AccountModelAccessField v-model="modelAccess" form-prop="modelAccess" :account-id="accountId" :disabled="disabled" :allow-preserve="preserveModelAccess" />
<slot name="scheduling">
<div v-if="showScheduling" class="flex min-h-6 items-center justify-between gap-3">
<span class="text-cp leading-none font-medium text-cp-text-secondary">{{ t('accounts.scheduling.scheduling') }}</span>
<span class="text-z leading-none font-medium text-z-text-secondary">{{ t('accounts.scheduling.scheduling') }}</span>
<ZSwitch
v-model="enabled"
:aria-label="t('accounts.scheduling.switchAccountSchedule')"
@@ -45,8 +44,8 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
</div>
</slot>
<div class="grid gap-4 sm:grid-cols-2">
<ZFormItem :label="t('accounts.scheduling.concurrencyLimit')">
<div class="grid gap-4.5 sm:grid-cols-2">
<ZFormItem prop="concurrencyLimit" :label="t('accounts.scheduling.concurrencyLimit')">
<ZInput
v-model="concurrencyLimit"
:aria-label="t('accounts.scheduling.accountConcurrencyLimit')"
@@ -57,7 +56,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
:disabled="disabled"
/>
</ZFormItem>
<ZFormItem :label="t('accounts.scheduling.weight')">
<ZFormItem prop="weight" :show-message="Boolean(weight.trim())" :label="t('accounts.scheduling.weight')">
<ZInput
v-model="weight"
:aria-label="t('accounts.scheduling.accountSchedulingWeight')"
@@ -78,6 +77,6 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
:disabled="disabled"
/>
</ZFormItem>
<AccountProxyField v-model:mode="proxyMode" v-model:proxy-id="proxyId" :preserve="preserveProxy" :error="proxyError" :current-proxy-id="currentProxyId" :endpoint="endpoint" :account-id="accountId" :disabled="disabled" />
<AccountProxyField v-model:mode="proxyMode" v-model:proxy-id="proxyId" form-prop="proxyId" :preserve="preserveProxy" :current-proxy-id="currentProxyId" :endpoint="endpoint" :account-id="accountId" :disabled="disabled" />
</div>
</template>
@@ -41,7 +41,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
:aria-label="t('accounts.editor.title')"
@click.stop="emit('edit', account)"
>
<Pencil class="size-3.5 text-cp-link" />
<Pencil class="size-3.5 text-z-link" />
</ZIconButton>
<ZIconButton
@@ -50,7 +50,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
:disabled="deleting"
@click.stop="emit('delete', account)"
>
<Trash2 class="size-3.5 text-cp-error" />
<Trash2 class="size-3.5 text-z-error" />
</ZIconButton>
<ZPopover placement="bottom-end">
@@ -68,7 +68,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
@click.stop="(close(), emit('test', account))"
>
<template #icon>
<Wifi class="size-3.5 text-cp-text-quaternary" />
<Wifi class="size-3.5 text-z-text-quaternary" />
</template>
{{ t('accounts.connectionTest.title') }}
</ZMenuItem>
@@ -78,7 +78,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
@click.stop="(close(), emit('toggleScheduling', account))"
>
<template #icon>
<Power class="size-3.5 text-cp-text-quaternary" />
<Power class="size-3.5 text-z-text-quaternary" />
</template>
{{ account.enabled ? t('accounts.actions.disableScheduling') : t('accounts.actions.enableScheduling') }}
</ZMenuItem>
@@ -89,16 +89,16 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
@click.stop="(close(), emit('refresh', account.id))"
>
<template #loading>
<RefreshCw class="size-3.5 animate-spin text-cp-text-quaternary motion-reduce:animate-none" />
<RefreshCw class="size-3.5 animate-spin text-z-text-quaternary motion-reduce:animate-none" />
</template>
<template #icon>
<RefreshCw class="size-3.5 text-cp-text-quaternary" />
<RefreshCw class="size-3.5 text-z-text-quaternary" />
</template>
{{ t('accounts.actions.refreshToken') }}
</ZMenuItem>
<ZMenuItem v-if="credentialEligible" @click.stop="(close(), emit('reauthorize', account))">
<template #icon>
<KeyRound class="size-3.5 text-cp-text-quaternary" />
<KeyRound class="size-3.5 text-z-text-quaternary" />
</template>
{{ t('accounts.actions.reauthorize') }}
</ZMenuItem>
@@ -109,10 +109,10 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
@click.stop="(close(), emit('downloadModelCatalog', account))"
>
<template #loading>
<RefreshCw class="size-3.5 animate-spin text-cp-text-quaternary motion-reduce:animate-none" />
<RefreshCw class="size-3.5 animate-spin text-z-text-quaternary motion-reduce:animate-none" />
</template>
<template #icon>
<Download class="size-3.5 text-cp-text-quaternary" />
<Download class="size-3.5 text-z-text-quaternary" />
</template>
{{ t('accounts.actions.downloadModelCatalog') }}
</ZMenuItem>
@@ -122,7 +122,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
@click.stop="(close(), emit('recover', account.id))"
>
<template #icon>
<RotateCcw class="size-3.5 text-cp-text-quaternary" />
<RotateCcw class="size-3.5 text-z-text-quaternary" />
</template>
{{ t('accounts.actions.restoreState') }}
</ZMenuItem>
@@ -81,42 +81,42 @@ const modelUsageColumns = computed(() => (defineTableColumns<AccountModelUsage>(
</script>
<template>
<section class="grid gap-4 rounded-lg bg-cp-bg-container p-4 shadow-cp-tertiary xl:min-h-0 xl:grid-cols-[0.52fr_1.48fr]">
<section class="grid gap-4 rounded-lg bg-z-bg-container p-4 shadow-z-tertiary xl:min-h-0 xl:grid-cols-[0.52fr_1.48fr]">
<div class="xl:flex xl:min-h-0 xl:flex-col">
<div class="mb-3 flex shrink-0 items-center justify-between gap-3">
<h3 class="m-0 text-cp-lg font-heavy text-cp-text">
<h3 class="m-0 text-z-lg font-heavy text-z-text">
{{ t('accounts.usage.tokensStructure') }}
</h3>
<span class="text-cp-xs font-emphasis text-cp-text-quaternary">{{ usageWindowLabels[account.usage.windowKind] }}</span>
<span class="text-z-xs font-emphasis text-z-text-quaternary">{{ usageWindowLabels[account.usage.windowKind] }}</span>
</div>
<div class="grid gap-2 xl:min-h-0 xl:flex-1 xl:grid-rows-5">
<div class="flex items-center justify-between rounded-lg bg-cp-green-container px-3 py-2">
<span class="text-cp-sm font-bold text-cp-green-on-container">{{ t('accounts.usage.input') }}</span>
<strong class="font-mono text-cp text-cp-text">
<div class="flex items-center justify-between rounded-lg bg-z-green-container px-3 py-2">
<span class="text-z-sm font-bold text-z-green-on-container">{{ t('accounts.usage.input') }}</span>
<strong class="font-mono text-z text-z-text">
{{ account.usage.inputTokensDisplay }}
</strong>
</div>
<div class="flex items-center justify-between rounded-lg bg-cp-orange-container px-3 py-2">
<span class="text-cp-sm font-bold text-cp-orange-on-container">{{ t('accounts.usage.output') }}</span>
<strong class="font-mono text-cp text-cp-text">
<div class="flex items-center justify-between rounded-lg bg-z-orange-container px-3 py-2">
<span class="text-z-sm font-bold text-z-orange-on-container">{{ t('accounts.usage.output') }}</span>
<strong class="font-mono text-z text-z-text">
{{ account.usage.outputTokensDisplay }}
</strong>
</div>
<div class="flex items-center justify-between rounded-lg bg-cp-cyan-container px-3 py-2">
<span class="text-cp-sm font-bold text-cp-cyan-on-container">{{ t('accounts.usage.cacheRead') }}</span>
<strong class="font-mono text-cp text-cp-text">
<div class="flex items-center justify-between rounded-lg bg-z-cyan-container px-3 py-2">
<span class="text-z-sm font-bold text-z-cyan-on-container">{{ t('accounts.usage.cacheRead') }}</span>
<strong class="font-mono text-z text-z-text">
{{ account.usage.cachedInputTokensDisplay }}
</strong>
</div>
<div class="flex items-center justify-between rounded-lg bg-cp-blue-container px-3 py-2">
<span class="text-cp-sm font-bold text-cp-blue-on-container">{{ t('accounts.usage.reasoning') }}</span>
<strong class="font-mono text-cp text-cp-text">
<div class="flex items-center justify-between rounded-lg bg-z-blue-container px-3 py-2">
<span class="text-z-sm font-bold text-z-blue-on-container">{{ t('accounts.usage.reasoning') }}</span>
<strong class="font-mono text-z text-z-text">
{{ account.usage.reasoningOutputTokensDisplay }}
</strong>
</div>
<div class="flex items-center justify-between rounded-lg bg-cp-blue-container px-3 py-2">
<span class="text-cp-sm font-bold text-cp-blue-on-container">{{ t('accounts.usage.cacheWrites') }}</span>
<strong class="font-mono text-cp text-cp-text">
<div class="flex items-center justify-between rounded-lg bg-z-blue-container px-3 py-2">
<span class="text-z-sm font-bold text-z-blue-on-container">{{ t('accounts.usage.cacheWrites') }}</span>
<strong class="font-mono text-z text-z-text">
{{ account.usage.cacheWriteInputTokensDisplay }}
</strong>
</div>
@@ -125,28 +125,28 @@ const modelUsageColumns = computed(() => (defineTableColumns<AccountModelUsage>(
<div class="min-w-0 pt-4 xl:flex xl:min-h-0 xl:flex-col xl:pt-0 xl:pl-4">
<div class="mb-3 flex shrink-0 flex-wrap items-center gap-x-4 gap-y-2">
<h3 class="m-0 shrink-0 text-cp-lg font-heavy text-cp-text">
<h3 class="m-0 shrink-0 text-z-lg font-heavy text-z-text">
{{ t('accounts.usage.modelUsageRanking') }}
</h3>
<div class="ml-auto flex items-baseline gap-4">
<div v-if="hasUsageSummary" class="flex items-baseline gap-1.5 whitespace-nowrap">
<Sigma class="size-3.5 self-center text-cp-text-tertiary" :stroke-width="1.75" />
<Sigma class="size-3.5 self-center text-z-text-tertiary" :stroke-width="1.75" />
<span :title="t('accounts.usage.totalTokens')">
<span class="sr-only">{{ t('accounts.usage.totalTokens') }}</span>
<span class="font-mono text-cp-sm font-emphasis tabular-nums text-cp-text">
<span class="font-mono text-z-sm font-emphasis tabular-nums text-z-text">
{{ account.usage.totalTokensDisplay }}
</span>
</span>
<span class="mx-0.5 text-[10px] leading-none font-emphasis text-cp-text-quaternary"> / </span>
<span class="mx-0.5 text-[10px] leading-none font-emphasis text-z-text-quaternary"> / </span>
<span :title="t('accounts.usage.totalCharge')">
<span class="sr-only">{{ t('accounts.usage.totalCharges') }}</span>
<span class="font-mono text-cp-sm font-heavy tabular-nums text-cp-green-text">
<span class="font-mono text-z-sm font-heavy tabular-nums text-z-green-text">
{{ totalBillingDisplay }}
</span>
</span>
</div>
<span class="whitespace-nowrap text-cp-xs font-emphasis text-cp-text-quaternary">{{ usageWindowLabels[account.usage.windowKind] }}</span>
<span class="whitespace-nowrap text-z-xs font-emphasis text-z-text-quaternary">{{ usageWindowLabels[account.usage.windowKind] }}</span>
</div>
</div>
@@ -36,18 +36,18 @@ const statusText = computed(() => {
<template>
<div class="flex flex-col gap-4">
<div class="rounded-cp bg-cp-fill-quaternary px-4 py-3">
<div class="rounded-z bg-z-fill-quaternary px-4 py-3">
<div class="flex items-start gap-3">
<div
class="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-cp bg-cp-bg-container text-cp-primary-text"
class="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-z bg-z-bg-container text-z-primary-text"
>
<KeyRound class="size-4" />
</div>
<div class="min-w-0 flex-1">
<p class="m-0 text-cp font-bold text-cp-text">
<p class="m-0 text-z font-bold text-z-text">
{{ panelTitle }}
</p>
<p class="m-0 mt-1 text-cp-sm leading-[1.55] font-medium text-cp-text-secondary">
<p class="m-0 mt-1 text-z-sm leading-[1.55] font-medium text-z-text-secondary">
{{ panelDescription }}
</p>
</div>
@@ -79,9 +79,9 @@ const statusText = computed(() => {
</ZIconButton>
</template>
<ZScrollbar max-height="92px">
<div class="rounded-cp bg-(--cp-input-bg) px-3.5 py-3 shadow-cp-tertiary">
<div class="rounded-z bg-(--z-input-bg) px-3.5 py-3 shadow-z-tertiary">
<pre
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-cp-sm leading-[1.6] font-emphasis text-cp-text-secondary"
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-z-sm leading-[1.6] font-emphasis text-z-text-secondary"
v-text="authUrl"
/>
</div>
@@ -89,10 +89,10 @@ const statusText = computed(() => {
</ZFormItem>
</ZForm>
<p v-if="statusText" class="m-0 text-cp-sm text-cp-text-secondary" role="status">
<p v-if="statusText" class="m-0 text-z-sm text-z-text-secondary" role="status">
{{ statusText }}
</p>
<p v-if="authorization.error" class="m-0 text-cp-sm text-cp-error" role="alert">
<p v-if="authorization.error" class="m-0 text-z-sm text-z-error" role="alert">
{{ authorization.error }}
</p>
@@ -11,7 +11,6 @@ defineProps<{
groups: AccountGroupRef[]
groupsLoading: boolean
disabled: boolean
proxyError?: string
}>()
const { t } = useI18n({ useScope: 'global' })
@@ -22,7 +21,7 @@ const form = defineModel<AccountCreateForm>({ required: true })
<template>
<div class="grid gap-6">
<fieldset class="m-0 min-w-0 border-0 p-0">
<legend class="mb-3 p-0 text-cp font-medium text-cp-text-secondary">
<legend class="mb-3 p-0 text-z font-medium text-z-text-secondary">
{{ t('accounts.onboarding.accountPlatform') }}
</legend>
<AccountProviderChooser v-model="form.source" :disabled="disabled" />
@@ -36,11 +35,11 @@ const form = defineModel<AccountCreateForm>({ required: true })
v-model:proxy-mode="form.proxyMode"
v-model:proxy-id="form.proxyId"
preserve-model-access
:groups="groups"
:groups-loading="groupsLoading"
:preserve-proxy="false"
:disabled="disabled"
:proxy-error="proxyError"
/>
<ZFormItem :label="t('common.fields.notes')">
<ZTextarea
@@ -5,16 +5,16 @@ import type { AccountCreateForm, AccountImportMode } from '../../utils/accountCr
import type { Account, AccountGroupRef } from '@/api'
import { Openai, Xai } from '@boxicons/vue'
import { ZButton, ZDialog, ZIconButton, ZSegmented } from '@codex-proxy/ui'
import { ZButton, ZDialog, ZForm, ZIconButton, ZSegmented } from '@codex-proxy/ui'
import { Copy, LayoutGrid, Settings2 } from '@lucide/vue'
import { computed } from 'vue'
import { computed, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import AccountPlanBadge from '@/components/account/AccountPlanBadge.vue'
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
import { useCopyText } from '@/composables/useCopyText'
import { accountCreateProvider, accountProxyError } from '../../utils/accountCreate'
import { accountModelAccessError } from '../../utils/modelAccess'
import { parseAccountSchedulingForm } from '../../utils/schedulingForm'
import { useAccountApiKeyFormRules } from '../../composables/useAccountApiKeyFormRules'
import { useAccountSettingsFormRules } from '../../composables/useAccountSettingsFormRules'
import { accountCreateProvider } from '../../utils/accountCreate'
import AccountApiKeyFields from '../AccountApiKeyFields.vue'
import AccountIdentityCell from '../AccountIdentityCell.vue'
import AccountImportFields from './AccountImportFields.vue'
@@ -26,13 +26,11 @@ const props = withDefaults(defineProps<{
groups: AccountGroupRef[]
groupsLoading: boolean
saving?: boolean
oauthLoading?: boolean
reauthorizing?: boolean
account?: Account | null
authorization: AccountAuthorizationView
}>(), {
saving: false,
oauthLoading: false,
reauthorizing: false,
account: null,
})
@@ -51,11 +49,13 @@ const accountCopyValue = computed(() =>
|| props.account?.id
|| '',
)
const busy = computed(() => props.saving || props.oauthLoading)
const busy = computed(() => props.saving
|| props.authorization.status === 'starting'
|| props.authorization.status === 'completing')
const canSelect = computed(() => form.value.source?.kind === 'bundle' || Boolean(accountCreateProvider(form.value)))
const proxyError = computed(() => accountProxyError(form.value))
const modelError = computed(() => accountModelAccessError(form.value.modelAccess))
const scheduling = computed(() => parseAccountSchedulingForm(form.value.concurrencyLimit, form.value.weight))
const formRef = useTemplateRef<InstanceType<typeof ZForm>>('formElement')
const settingsRules = useAccountSettingsFormRules(() => form.value)
const apiKeyRules = useAccountApiKeyFormRules()
const view = computed(() => resolveAccountCreatePresentation({
form: form.value,
provider: accountCreateProvider(form.value),
@@ -81,9 +81,24 @@ const importText = computed({
},
})
function continueToImport() {
if (canSelect.value && !modelError.value && scheduling.value.valid && !props.groupsLoading && !proxyError.value && !busy.value)
form.value.step = 'import'
watch([
open,
() => form.value.step,
mode,
], () => formRef.value?.clearValidate())
async function continueToImport() {
if (!canSelect.value || props.groupsLoading || busy.value || !await formRef.value?.validate().catch(() => false))
return
form.value.step = 'import'
}
async function requestCreate() {
if (!view.value.canSubmit)
return
if (mode.value === 'api_key' && !await formRef.value?.validate().catch(() => false))
return
emit('create')
}
</script>
@@ -98,16 +113,16 @@ function continueToImport() {
@closed="emit('afterLeave')"
>
<template #icon>
<Settings2 v-if="view.configuring" class="text-cp-text" :size="20" aria-hidden="true" />
<LayoutGrid v-else-if="view.isBatch" class="text-cp-text" :size="20" aria-hidden="true" />
<Xai v-else-if="view.provider === 'xai'" class="text-cp-text" :width="20" :height="20" aria-hidden="true" />
<Openai v-else-if="view.provider === 'openai'" class="text-cp-text" :width="20" :height="20" aria-hidden="true" />
<Settings2 v-if="view.configuring" class="text-z-text" :size="20" aria-hidden="true" />
<LayoutGrid v-else-if="view.isBatch" class="text-z-text" :size="20" aria-hidden="true" />
<Xai v-else-if="view.provider === 'xai'" class="text-z-text" :width="20" :height="20" aria-hidden="true" />
<Openai v-else-if="view.provider === 'openai'" class="text-z-text" :width="20" :height="20" aria-hidden="true" />
</template>
<div class="grid gap-4">
<div
v-if="reauthorizing && account"
class="flex flex-wrap items-center justify-between gap-4 rounded-cp bg-cp-fill-quaternary px-4 py-3.5"
class="flex flex-wrap items-center justify-between gap-4 rounded-z bg-z-fill-quaternary px-4 py-3.5"
>
<AccountIdentityCell
class="min-w-0 flex-1"
@@ -138,17 +153,9 @@ function continueToImport() {
</div>
</div>
<AccountSetupFields
v-if="view.configuring"
v-model="form"
:disabled="busy"
:groups="groups"
:groups-loading="groupsLoading"
:proxy-error="form.proxyId.trim() ? proxyError : undefined"
/>
<p v-if="view.configuring && !scheduling.valid" class="m-0 text-xs text-cp-error" role="alert">
{{ scheduling.message }}
</p>
<ZForm v-if="view.configuring" ref="formElement" :model="form" :rules="settingsRules" scroll-to-error focus-on-error novalidate @submit="continueToImport">
<AccountSetupFields v-model="form" :disabled="busy" :groups="groups" :groups-loading="groupsLoading" />
</ZForm>
<template v-if="!view.configuring">
<ZSegmented
v-if="!reauthorizing && !view.isBatch"
@@ -166,13 +173,15 @@ function continueToImport() {
:can-start="true"
:panel-title="`${view.label} ${reauthorizing ? t('accounts.actions.reauthorize') : t('accounts.onboarding.authorize')}`"
:panel-description="view.oauth.description"
:loading="oauthLoading"
:loading="authorization.status === 'starting'"
:callback-label="view.oauth.callbackLabel"
:callback-placeholder="view.oauth.callbackPlaceholder"
:disabled="busy"
@regenerate="emit('generateOauth')"
/>
<AccountApiKeyFields v-else-if="mode === 'api_key'" v-model="form.apiKey" :disabled="busy" />
<ZForm v-else-if="mode === 'api_key'" ref="formElement" :model="form" :rules="apiKeyRules" scroll-to-error focus-on-error novalidate @submit="requestCreate">
<AccountApiKeyFields v-model="form.apiKey" :disabled="busy" />
</ZForm>
<AccountImportFields
v-else
:key="mode"
@@ -192,10 +201,10 @@ function continueToImport() {
<ZButton v-else class="mr-auto" :disabled="busy" @click="form.step = 'settings'">
{{ t('common.actions.previous') }}
</ZButton>
<ZButton v-if="view.configuring" type="primary" :disabled="!canSelect || !scheduling.valid || Boolean(modelError) || groupsLoading || Boolean(proxyError) || busy" @click="continueToImport">
<ZButton v-if="view.configuring" type="primary" :disabled="!canSelect || groupsLoading || busy" @click="continueToImport">
{{ t('accounts.onboarding.continueImporting') }}
</ZButton>
<ZButton v-else type="primary" :loading="saving || oauthLoading" :disabled="!view.canSubmit" @click="emit('create')">
<ZButton v-else type="primary" :loading="saving || authorization.status === 'completing'" :disabled="!view.canSubmit" @click="requestCreate">
{{ view.submitLabel }}
</ZButton>
</template>
@@ -3,7 +3,6 @@ import type { AccountCreateForm } from '../../utils/accountCreate'
import { t } from '@/i18n'
import { formatProviderLabel } from '@/utils/providers'
import { accountCreateProvider, accountImportModes } from '../../utils/accountCreate'
import { apiKeyAccountError } from '../../utils/upstreamApiKey'
export function resolveAccountCreatePresentation(input: {
form: AccountCreateForm
@@ -35,9 +34,7 @@ export function resolveAccountCreatePresentation(input: {
let canSubmit = available && !input.busy
if (form.mode === 'oauth')
canSubmit &&= Boolean(authorization.flow && input.callback.trim())
else if (form.mode === 'api_key')
canSubmit &&= !apiKeyAccountError(form.apiKey)
else
else if (form.mode !== 'api_key')
canSubmit &&= Boolean(form.importTexts[form.mode].trim())
return {
@@ -45,10 +45,10 @@ watch(hasAttention, (value) => {
<section class="min-w-0 space-y-5" :aria-label="t('accounts.importTasks.importTaskDetails')">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 class="text-base font-bold text-cp-text">
<h3 class="text-base font-bold text-z-text">
{{ taskLabel(task) }}
</h3>
<p class="mt-1 text-xs text-cp-text-secondary">
<p class="mt-1 text-xs text-z-text-secondary">
{{ t('accounts.importTasks.created', { createdAtDisplay: task.createdAtDisplay }) }}
</p>
</div>
@@ -60,11 +60,11 @@ watch(hasAttention, (value) => {
@click="emit('stop')"
>
<template #icon>
<Square class="size-3 text-cp-error" />
<Square class="size-3 text-z-error" />
</template>
{{ task.stopRequested || task.counts.pending === 0 ? t('accounts.importTasks.waitForCurrentEntryToEnd') : t('accounts.importTasks.stopUnstartedEntries') }}
</ZButton>
<p v-if="task.counts.pending > 0 || task.stopRequested" class="m-0 max-w-64 text-right text-cp-xs text-cp-text-secondary">
<p v-if="task.counts.pending > 0 || task.stopRequested" class="m-0 max-w-64 text-right text-z-xs text-z-text-secondary">
{{ t('accounts.importTasks.stopHelp') }}
</p>
</div>
@@ -75,11 +75,11 @@ watch(hasAttention, (value) => {
<div>
<div class="mb-2 flex items-baseline justify-between gap-3 text-xs">
<span class="text-cp-text-secondary">{{ t('accounts.importTasks.entryProgress') }} <strong class="font-mono font-medium text-cp-text">{{ processed(task) }} / {{ task.total }}</strong></span>
<span class="font-semibold text-cp-text">{{ t('accounts.importTasks.accountsImported', { importedAccounts: task.counts.importedAccounts }) }}</span>
<span class="text-z-text-secondary">{{ t('accounts.importTasks.entryProgress') }} <strong class="font-mono font-medium text-z-text">{{ processed(task) }} / {{ task.total }}</strong></span>
<span class="font-semibold text-z-text">{{ t('accounts.importTasks.accountsImported', { importedAccounts: task.counts.importedAccounts }) }}</span>
</div>
<div
class="flex h-2 overflow-hidden rounded-full bg-cp-fill-alter"
class="flex h-2 overflow-hidden rounded-full bg-z-fill-alter"
role="progressbar"
:aria-label="t('accounts.importTasks.itemProcessingProgress')"
:aria-valuenow="processed(task)"
@@ -88,17 +88,17 @@ watch(hasAttention, (value) => {
>
<div v-for="status in outcomeOrder" :key="status" :class="itemStates[status].fill" :style="{ width: `${task.counts[status] / task.total * 100}%` }" />
</div>
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-xs text-cp-text-secondary">
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-xs text-z-text-secondary">
<span v-for="status in outcomeOrder" :key="status" class="inline-flex items-center gap-1.5">
<span class="size-1.5 rounded-full" :class="itemStates[status].fill" aria-hidden="true" />
{{ itemStates[status].label }} <span class="font-mono text-cp-text">{{ task.counts[status] }}</span>
{{ itemStates[status].label }} <span class="font-mono text-z-text">{{ task.counts[status] }}</span>
</span>
</div>
</div>
<div>
<div v-if="hasAttention" class="mb-3 flex flex-wrap items-center gap-x-4 gap-y-3">
<p v-if="task.counts.unknown" class="flex min-w-0 items-start gap-2 text-xs leading-relaxed text-cp-warning-text">
<p v-if="task.counts.unknown" class="flex min-w-0 items-start gap-2 text-xs leading-relaxed text-z-warning-text">
<CircleAlert class="mt-0.5 size-3.5 shrink-0" />
{{ t('accounts.importTasks.itemsToBeCheckedMayHaveBeen') }}
</p>
@@ -117,20 +117,20 @@ watch(hasAttention, (value) => {
<li
v-for="item in visibleItems"
:key="item.index"
class="grid grid-cols-[1.75rem_minmax(0,1fr)_4.5rem] items-center gap-x-3 gap-y-1 rounded-cp-sm px-3 py-2.5 odd:bg-cp-fill-quaternary sm:grid-cols-[2rem_2rem_minmax(0,1fr)_4.5rem]"
class="grid grid-cols-[1.75rem_minmax(0,1fr)_4.5rem] items-center gap-x-3 gap-y-1 rounded-z-sm px-3 py-2.5 odd:bg-z-fill-quaternary sm:grid-cols-[2rem_2rem_minmax(0,1fr)_4.5rem]"
>
<span class="font-mono text-xs text-cp-text-secondary">{{ String(item.index).padStart(2, '0') }}</span>
<span class="font-mono text-xs text-z-text-secondary">{{ String(item.index).padStart(2, '0') }}</span>
<ProviderIconGroup :provider="item.provider" size="sm" />
<p class="col-span-2 col-start-2 row-start-2 min-w-0 text-xs leading-relaxed wrap-anywhere text-cp-text-secondary sm:col-span-1 sm:col-start-3 sm:row-start-1">
<p class="col-span-2 col-start-2 row-start-2 min-w-0 text-xs leading-relaxed wrap-anywhere text-z-text-secondary sm:col-span-1 sm:col-start-3 sm:row-start-1">
{{ itemDescription(item) }}
</p>
<span class="col-start-3 row-start-1 inline-flex items-center justify-self-end sm:col-start-4" :title="itemStates[item.status].label">
<template v-if="item.status === 'succeeded' || item.status === 'failed'">
<Check v-if="item.status === 'succeeded'" class="size-4 text-cp-success-text" aria-hidden="true" />
<X v-else class="size-4 text-cp-error-text" aria-hidden="true" />
<Check v-if="item.status === 'succeeded'" class="size-4 text-z-success-text" aria-hidden="true" />
<X v-else class="size-4 text-z-error-text" aria-hidden="true" />
<span class="sr-only">{{ itemStates[item.status].label }}</span>
</template>
<span v-else class="rounded-cp-sm px-2 py-0.5 text-[11px] font-semibold" :class="itemStates[item.status].color">
<span v-else class="rounded-z-sm px-2 py-0.5 text-[11px] font-semibold" :class="itemStates[item.status].color">
{{ itemStates[item.status].label }}
</span>
</span>
@@ -80,7 +80,7 @@ function handleRetentionKeydown(event: KeyboardEvent) {
<CircleAlert class="size-4" />
</ZIconButton>
</template>
<div :id="retentionId" role="tooltip" class="grid w-72 max-w-[calc(100vw-2rem)] gap-2 p-4 text-cp-xs leading-relaxed text-cp-text-secondary">
<div :id="retentionId" role="tooltip" class="grid w-72 max-w-[calc(100vw-2rem)] gap-2 p-4 text-z-xs leading-relaxed text-z-text-secondary">
<p class="m-0">
{{ t('accounts.importTasks.resultsRetainedForHour') }}
</p>
@@ -5,33 +5,33 @@ import { t } from '@/i18n'
export const itemStates = computed<Record<ImportItemStatus, { label: string, color: string, fill: string }>>(() => ({
pending: {
label: t('common.status.pending'),
color: 'bg-cp-fill-alter text-cp-text-secondary',
fill: 'bg-cp-fill-secondary',
color: 'bg-z-fill-alter text-z-text-secondary',
fill: 'bg-z-fill-secondary',
},
running: {
label: t('accounts.importTasks.processing'),
color: 'bg-cp-primary-container text-cp-primary-on-container',
fill: 'bg-cp-primary',
color: 'bg-z-primary-container text-z-primary-on-container',
fill: 'bg-z-primary',
},
succeeded: {
label: t('accounts.importTasks.alreadyInStock'),
color: 'bg-cp-success-container text-cp-success-on-container',
fill: 'bg-cp-success',
color: 'bg-z-success-container text-z-success-on-container',
fill: 'bg-z-success',
},
failed: {
label: t('common.status.failed'),
color: 'bg-cp-error-container text-cp-error-on-container',
fill: 'bg-cp-error',
color: 'bg-z-error-container text-z-error-on-container',
fill: 'bg-z-error',
},
unknown: {
label: t('accounts.importTasks.toBeVerified'),
color: 'bg-cp-warning-container text-cp-warning-on-container',
fill: 'bg-cp-warning',
color: 'bg-z-warning-container text-z-warning-on-container',
fill: 'bg-z-warning',
},
skipped: {
label: t('accounts.importTasks.notExecuted'),
color: 'bg-cp-fill-tertiary text-cp-text-secondary',
fill: 'bg-cp-text-quaternary',
color: 'bg-z-fill-tertiary text-z-text-secondary',
fill: 'bg-z-text-quaternary',
},
}))
@@ -77,7 +77,7 @@ function invocationView(invocation: AccountProfileInvocation) {
<template>
<section class="grid gap-8 sm:grid-cols-2 sm:gap-12" aria-labelledby="profile-activity-insights-title">
<div>
<h3 id="profile-activity-insights-title" class="m-0 text-cp-lg font-heavy text-cp-text">
<h3 id="profile-activity-insights-title" class="m-0 text-z-lg font-heavy text-z-text">
{{ t('accounts.profile.activity.activityInsights') }}
</h3>
<dl class="mt-4 mb-0 grid gap-3.5">
@@ -86,10 +86,10 @@ function invocationView(invocation: AccountProfileInvocation) {
:key="row.label"
class="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-4"
>
<dt class="min-w-0 text-cp font-semibold text-cp-text-secondary">
<dt class="min-w-0 text-z font-semibold text-z-text-secondary">
{{ row.label }}
</dt>
<dd class="m-0 font-mono font-heavy tabular-nums text-cp-text">
<dd class="m-0 font-mono font-heavy tabular-nums text-z-text">
{{ row.value }}
</dd>
</div>
@@ -97,7 +97,7 @@ function invocationView(invocation: AccountProfileInvocation) {
</div>
<div>
<h3 class="m-0 text-cp-lg font-heavy text-cp-text">
<h3 class="m-0 text-z-lg font-heavy text-z-text">
{{ t('accounts.profile.activity.pluginsAndSkills') }}
</h3>
<ol v-if="invocations.length > 0" class="mt-3 mb-0 grid list-none gap-1.5 p-0">
@@ -106,16 +106,16 @@ function invocationView(invocation: AccountProfileInvocation) {
:key="invocation.key"
class="grid grid-cols-[24px_minmax(0,1fr)_auto] items-center gap-2.5 py-1.5"
>
<span class="grid size-6 place-items-center rounded-cp bg-cp-fill-quaternary text-cp-text-secondary">
<span class="grid size-6 place-items-center rounded-z bg-z-fill-quaternary text-z-text-secondary">
<Package class="size-3.5" :stroke-width="2" />
</span>
<span class="truncate text-cp font-bold text-cp-text">{{ invocation.name }}</span>
<span class="font-mono text-cp-sm font-semibold tabular-nums text-cp-text-secondary">
<span class="truncate text-z font-bold text-z-text">{{ invocation.name }}</span>
<span class="font-mono text-z-sm font-semibold tabular-nums text-z-text-secondary">
{{ t('accounts.profile.activity.runs', { count: formatInteger(invocation.count) }) }}
</span>
</li>
</ol>
<p v-else class="mt-4 mb-0 text-cp-sm font-semibold text-cp-text-quaternary">
<p v-else class="mt-4 mb-0 text-z-sm font-semibold text-z-text-quaternary">
{{ t('accounts.profile.activity.noToolCalls') }}
</p>
</div>
@@ -87,13 +87,13 @@ function formatDuration(value: number | null | undefined) {
<section class="@container min-w-0 py-1" :aria-label="t('accounts.profile.metrics.cumulativeActivitySummary')" :aria-busy="loading">
<dl class="grid grid-cols-2 gap-y-4 @[24rem]:grid-cols-3 @[34rem]:grid-cols-5">
<div v-for="metric in metrics" :key="metric.label" class="min-w-0 px-2 text-center sm:px-3">
<dd class="m-0 font-mono text-cp-lg leading-tight font-heavy whitespace-nowrap tabular-nums text-cp-text" :title="metric.title">
<dd class="m-0 font-mono text-z-lg leading-tight font-heavy whitespace-nowrap tabular-nums text-z-text" :title="metric.title">
<ZSkeleton v-if="loading && !profile" shape="text" class="mx-auto h-4 w-14" />
<template v-else>
{{ metric.value }}
</template>
</dd>
<dt class="mt-1.5 text-cp-xs font-semibold text-cp-text-secondary">
<dt class="mt-1.5 text-z-xs font-semibold text-z-text-secondary">
{{ metric.label }}
</dt>
</div>
@@ -67,14 +67,14 @@ watch(
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<h3 id="profile-display-name" class="m-0 min-w-0 truncate text-cp-lg leading-tight font-heavy text-cp-text">
<h3 id="profile-display-name" class="m-0 min-w-0 truncate text-z-lg leading-tight font-heavy text-z-text">
{{ displayName }}
</h3>
<AccountPlanBadge :upstream="account.upstream" :authentication-kind="account.authenticationKind" :plan-type="account.planType" :plan-type-display="account.planTypeDisplay" size="sm" />
</div>
<div
v-if="username || accountIdentity"
class="mt-1.5 grid min-w-0 gap-0.5 text-cp-xs font-semibold text-cp-text-secondary"
class="mt-1.5 grid min-w-0 gap-0.5 text-z-xs font-semibold text-z-text-secondary"
>
<span v-if="username" class="truncate">{{ username }}</span>
<span v-if="accountIdentity" class="truncate" :title="accountIdentity">{{ accountIdentity }}</span>
@@ -13,7 +13,7 @@ const { t } = useI18n({ useScope: 'global' })
<section class="grid min-w-0 gap-4 overflow-hidden">
<div class="flex items-center justify-between">
<ZSkeleton shape="text" class="h-4 w-20" />
<ZSkeleton class="h-7 w-32 rounded-cp opacity-70" />
<ZSkeleton class="h-7 w-32 rounded-z opacity-70" />
</div>
<div class="grid min-w-190 gap-1" style="grid-template-columns: repeat(52, minmax(0, 1fr))">
<div v-for="week in 52" :key="week" class="grid gap-1">
@@ -46,30 +46,30 @@ const dates = computed(() => [
</script>
<template>
<section class="min-w-0 rounded-cp-lg bg-cp-fill-alter p-4 sm:p-5" :aria-label="t('accounts.profile.subscription.label')">
<section class="min-w-0 rounded-z-lg bg-z-fill-alter p-4 sm:p-5" :aria-label="t('accounts.profile.subscription.label')">
<dl class="m-0 grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-[1.3fr_1.3fr_1fr_1fr]">
<div v-for="date in dates" :key="date.label" class="min-w-0">
<dt class="text-cp-xs text-cp-text-secondary">
<dt class="text-z-xs text-z-text-secondary">
{{ date.label }}
</dt>
<dd class="m-0 mt-1.5 font-mono text-cp-sm leading-relaxed tabular-nums text-cp-text">
<dd class="m-0 mt-1.5 font-mono text-z-sm leading-relaxed tabular-nums text-z-text">
<time v-if="date.value" :datetime="date.value">{{ date.display ?? t('common.values.unknown') }}</time>
<span v-else>{{ t('common.values.unknown') }}</span>
</dd>
</div>
<div class="min-w-0">
<dt class="text-cp-xs text-cp-text-secondary">
<dt class="text-z-xs text-z-text-secondary">
{{ t('accounts.profile.subscription.renewalMethod') }}
</dt>
<dd class="m-0 mt-1.5 text-cp-sm leading-relaxed" :class="subscription.willRenew === true ? 'text-cp-success' : 'text-cp-text'">
<dd class="m-0 mt-1.5 text-z-sm leading-relaxed" :class="subscription.willRenew === true ? 'text-z-success' : 'text-z-text'">
{{ renewal }}
</dd>
</div>
<div class="min-w-0">
<dt class="text-cp-xs text-cp-text-secondary">
<dt class="text-z-xs text-z-text-secondary">
{{ t('accounts.usage.billing') }}
</dt>
<dd class="m-0 mt-1.5 text-cp-sm leading-relaxed wrap-break-word text-cp-text">
<dd class="m-0 mt-1.5 text-z-sm leading-relaxed wrap-break-word text-z-text">
{{ billing }}
</dd>
</div>
@@ -30,11 +30,11 @@ const modeOptions = computed(() => ([
},
]))
const levelClasses: Record<ProfileActivityLevel, string> = {
0: 'bg-cp-activity-level-0',
1: 'bg-cp-activity-level-1',
2: 'bg-cp-activity-level-2',
3: 'bg-cp-activity-level-3',
4: 'bg-cp-activity-level-4',
0: 'bg-z-activity-level-0',
1: 'bg-z-activity-level-1',
2: 'bg-z-activity-level-2',
3: 'bg-z-activity-level-3',
4: 'bg-z-activity-level-4',
}
const grid = computed(() => buildProfileActivityGrid(props.calendar, mode.value))
const rangeLabel = computed(() => grid.value.rangeLabel)
@@ -53,7 +53,7 @@ const monthLabels = computed(() =>
<section class="min-w-0" aria-labelledby="profile-token-activity-title">
<div class="mb-4 flex flex-wrap items-end justify-between gap-3">
<div>
<h3 id="profile-token-activity-title" class="m-0 text-cp-lg font-heavy text-cp-text">
<h3 id="profile-token-activity-title" class="m-0 text-z-lg font-heavy text-z-text">
{{ t('accounts.profile.activity.tokenActivity') }}
</h3>
</div>
@@ -75,7 +75,7 @@ const monthLabels = computed(() =>
<span
v-for="(month, monthIndex) in monthLabels"
:key="`${month.weekIndex}-${month.label}`"
class="row-start-1 h-4 whitespace-nowrap text-[10px] leading-4 font-semibold text-cp-text-quaternary"
class="row-start-1 h-4 whitespace-nowrap text-[10px] leading-4 font-semibold text-z-text-quaternary"
:class="monthIndex === monthLabels.length - 1 ? 'text-right' : undefined"
:style="{
gridColumn: `${month.weekIndex + (monthIndex === monthLabels.length - 1 ? 0 : 1)} / span 2`,
@@ -31,13 +31,13 @@ const { profile, subscription, loading, error, load } = useAccountPersonalInfo({
<template>
<ZDialog v-model="open" :title="t('accounts.profile.personalInformation')" width="64rem">
<div class="flex min-h-0 min-w-0 flex-col gap-6 pb-2">
<div class="grid min-w-0 grid-cols-1 gap-4 rounded-cp-lg bg-cp-fill-alter p-4 sm:p-5 lg:items-center" :class="account.capabilities.profile ? 'lg:grid-cols-[minmax(310px,1.05fr)_minmax(0,1.95fr)]' : undefined">
<div class="grid min-w-0 grid-cols-1 gap-4 rounded-z-lg bg-z-fill-alter p-4 sm:p-5 lg:items-center" :class="account.capabilities.profile ? 'lg:grid-cols-[minmax(310px,1.05fr)_minmax(0,1.95fr)]' : undefined">
<AccountProfileHero :account="account" :profile="profile" />
<AccountProfileMetrics v-if="account.capabilities.profile" :profile="profile" :loading="loading" />
</div>
<section v-if="subscription" class="grid min-w-0 grid-cols-1 gap-4" aria-labelledby="profile-subscription-title">
<h3 id="profile-subscription-title" class="m-0 text-cp font-heavy text-cp-text">
<h3 id="profile-subscription-title" class="m-0 text-z font-heavy text-z-text">
{{ t('accounts.profile.subscriptionInformation') }}
</h3>
<AccountSubscription :subscription="subscription" />
@@ -47,7 +47,7 @@ const { profile, subscription, loading, error, load } = useAccountPersonalInfo({
<AccountProfileSkeleton v-if="account.capabilities.profile" />
<div v-else role="status" aria-busy="true">
<span class="sr-only">{{ t('accounts.profile.loadingSubscription') }}</span>
<ZSkeleton class="h-24 w-full rounded-cp-lg" />
<ZSkeleton class="h-24 w-full rounded-z-lg" />
</div>
</template>
<template v-else-if="profile">
@@ -15,60 +15,60 @@ const metrics = computed(() => [
label: t('accounts.quotaForecast.tokenCapacity'),
observed: source.value?.tokensDisplay ?? '—',
estimated: props.forecast.estimatedTokensDisplay,
tone: 'text-cp-cyan-text',
tone: 'text-z-cyan-text',
},
{
label: t('accounts.quotaForecast.equivalentFeeUsd'),
observed: source.value?.usdDisplay ?? '—',
estimated: props.forecast.estimatedUsdDisplay,
tone: 'text-cp-green-text',
tone: 'text-z-green-text',
},
])
</script>
<template>
<section v-if="source" :aria-label="t('accounts.quotaForecast.capacityForecast')" class="flex flex-col gap-5 rounded-cp-card bg-cp-fill-tertiary/70 p-4 [html[data-theme=light]_&]:bg-cp-fill-quaternary/70">
<section v-if="source" :aria-label="t('accounts.quotaForecast.capacityForecast')" class="flex flex-col gap-5 rounded-z-card bg-z-fill-tertiary/70 p-4 [html[data-theme=light]_&]:bg-z-fill-quaternary/70">
<div>
<div class="flex flex-wrap items-center justify-between gap-2">
<span class="text-cp-xs text-cp-text-secondary">{{ t('accounts.quotaForecast.usedThisCycle') }}</span>
<span class="inline-flex items-center rounded-cp-sm bg-cp-info-container px-2 py-1 text-cp-xs font-emphasis text-cp-info-on-container">
<span class="text-z-xs text-z-text-secondary">{{ t('accounts.quotaForecast.usedThisCycle') }}</span>
<span class="inline-flex items-center rounded-z-sm bg-z-info-container px-2 py-1 text-z-xs font-emphasis text-z-info-on-container">
{{ forecast.lowSample ? t('accounts.quotaForecast.preliminary') : t('accounts.quotaForecast.estimateLabel') }}
</span>
</div>
<div class="mt-2 flex items-end justify-between gap-4">
<strong class="font-mono text-3xl font-heavy tabular-nums text-cp-text [html[data-theme=light]_&]:font-emphasis">{{ source.usedPercentDisplay }}</strong>
<span class="pb-1 text-cp-xs text-cp-text-secondary">{{ t('accounts.quotaForecast.thisCycle') }} <span class="font-mono">100%</span></span>
<strong class="font-mono text-3xl font-heavy tabular-nums text-z-text [html[data-theme=light]_&]:font-emphasis">{{ source.usedPercentDisplay }}</strong>
<span class="pb-1 text-z-xs text-z-text-secondary">{{ t('accounts.quotaForecast.thisCycle') }} <span class="font-mono">100%</span></span>
</div>
<!-- 实心为已观测比例,虚线为待推算容量,不伪造历史趋势或置信区间。 -->
<div
class="relative mt-3 h-3 overflow-hidden rounded-full bg-cp-bg-container"
class="relative mt-3 h-3 overflow-hidden rounded-full bg-z-bg-container"
role="img"
:aria-label="t('accounts.quotaForecast.usedDescription', { rate: source.usedPercentDisplay })"
>
<span class="absolute inset-x-0 top-1/2 border-t-2 border-dashed border-cp-primary/50" />
<span class="absolute inset-y-0 left-0 rounded-full bg-cp-primary" :style="{ width: `${source.usedPercent ?? 0}%` }" />
<span class="absolute inset-x-0 top-1/2 border-t-2 border-dashed border-z-primary/50" />
<span class="absolute inset-y-0 left-0 rounded-full bg-z-primary" :style="{ width: `${source.usedPercent ?? 0}%` }" />
</div>
<div class="mt-2 flex justify-between font-mono text-[10px] text-cp-text-tertiary" aria-hidden="true">
<div class="mt-2 flex justify-between font-mono text-[10px] text-z-text-tertiary" aria-hidden="true">
<span>0</span><span>25</span><span>50</span><span>75</span><span>100%</span>
</div>
</div>
<div class="grid gap-2">
<div class="flex flex-wrap justify-between gap-x-4 gap-y-1 text-cp-xs text-cp-text-secondary">
<div class="flex flex-wrap justify-between gap-x-4 gap-y-1 text-z-xs text-z-text-secondary">
<span>{{ t('accounts.quotaForecast.alreadyRecordedThisCycle') }}</span>
<span>{{ forecast.extrapolated ? t('accounts.quotaForecast.equivalentCapacityOverDays', { targetDays: forecast.targetDays }) : t('accounts.quotaForecast.projectedTotal') }}</span>
</div>
<div class="grid gap-2">
<div v-for="metric in metrics" :key="metric.label" class="rounded-cp bg-cp-bg-container px-3 py-3">
<p class="m-0 text-cp-xs text-cp-text-secondary">
<div v-for="metric in metrics" :key="metric.label" class="rounded-z bg-z-bg-container px-3 py-3">
<p class="m-0 text-z-xs text-z-text-secondary">
{{ metric.label }}
</p>
<div class="mt-2 grid grid-cols-[1fr_20px_1.2fr] items-center gap-2">
<span class="font-mono text-cp-sm font-emphasis tabular-nums text-cp-text [html[data-theme=light]_&]:font-medium">{{ metric.observed }}</span>
<ArrowRight class="size-3.5 text-cp-text-tertiary" />
<span class="font-mono text-z-sm font-emphasis tabular-nums text-z-text [html[data-theme=light]_&]:font-medium">{{ metric.observed }}</span>
<ArrowRight class="size-3.5 text-z-text-tertiary" />
<strong class="text-right font-mono text-xl font-heavy tabular-nums" :class="metric.tone">
<span v-if="metric.estimated !== '—'" class="mr-1 text-cp-sm font-normal text-cp-text-tertiary">≈</span>{{ metric.estimated }}
<span v-if="metric.estimated !== '—'" class="mr-1 text-z-sm font-normal text-z-text-tertiary">≈</span>{{ metric.estimated }}
</strong>
</div>
</div>
@@ -10,7 +10,7 @@ const { t } = useI18n({ useScope: 'global' })
<div role="status" aria-busy="true">
<span class="sr-only">{{ t('accounts.quotaForecast.loadingQuotaForecast') }}</span>
<div aria-hidden="true">
<div class="flex flex-col gap-5 rounded-cp-card bg-cp-fill-tertiary/70 p-4 [html[data-theme=light]_&]:bg-cp-fill-quaternary/70">
<div class="flex flex-col gap-5 rounded-z-card bg-z-fill-tertiary/70 p-4 [html[data-theme=light]_&]:bg-z-fill-quaternary/70">
<div>
<div class="flex items-center justify-between gap-3">
<ZSkeleton shape="text" class="w-16" />
@@ -28,7 +28,7 @@ const { t } = useI18n({ useScope: 'global' })
<ZSkeleton shape="text" class="w-20" />
<ZSkeleton shape="text" class="w-20" />
</div>
<div v-for="metric in 2" :key="metric" class="rounded-cp bg-cp-bg-container px-3 py-3">
<div v-for="metric in 2" :key="metric" class="rounded-z bg-z-bg-container px-3 py-3">
<ZSkeleton shape="text" class="w-20" />
<div class="mt-3 grid grid-cols-[1fr_20px_1.2fr] items-center gap-2">
<ZSkeleton shape="text" class="w-14" />
@@ -84,7 +84,7 @@ function handleExplanationKeydown(event: KeyboardEvent) {
type="info"
>
<template #icon>
<ChartNoAxesCombined class="size-5 text-cp-primary-text" :stroke-width="1.75" />
<ChartNoAxesCombined class="size-5 text-z-primary-text" :stroke-width="1.75" />
</template>
<div class="grid grid-cols-1 gap-4">
@@ -100,7 +100,7 @@ function handleExplanationKeydown(event: KeyboardEvent) {
<div aria-live="polite" :aria-busy="loading || refreshing">
<ForecastSkeleton v-if="loading || refreshing" />
<template v-else-if="forecast">
<div v-if="unavailableReason" class="grid rounded-cp-card bg-cp-fill-tertiary/70 [html[data-theme=light]_&]:bg-cp-fill-quaternary/70">
<div v-if="unavailableReason" class="grid rounded-z-card bg-z-fill-tertiary/70 [html[data-theme=light]_&]:bg-z-fill-quaternary/70">
<ZEmpty :title="t('accounts.quotaForecast.unpredictableAtMoment')" :description="unavailableReason" :icon="ChartNoAxesCombined" surface="none" class="min-h-72 content-center" />
</div>
<ForecastCapacity v-else :forecast="forecast" />
@@ -110,7 +110,7 @@ function handleExplanationKeydown(event: KeyboardEvent) {
<template #footer>
<div class="mr-auto flex min-w-0 items-center gap-1">
<span v-if="forecast?.source?.observedAt" class="hidden text-cp-xs text-cp-text-tertiary sm:block">
<span v-if="forecast?.source?.observedAt" class="hidden text-z-xs text-z-text-tertiary sm:block">
{{ t('accounts.quotaForecast.updated', { observedAtDisplay: forecast.source.observedAtDisplay }) }}
</span>
<ZPopover v-model="explanationOpen" trigger="hover-click" placement="top-start" :hover-delay="240">
@@ -127,8 +127,8 @@ function handleExplanationKeydown(event: KeyboardEvent) {
<CircleAlert class="size-3.5" aria-hidden="true" />
</ZIconButton>
</template>
<section :id="explanationId" role="tooltip" class="grid w-96 max-w-[calc(100vw-2rem)] gap-3 p-4 text-cp-xs leading-relaxed text-cp-text-secondary">
<h4 class="m-0 font-heavy text-cp-text">
<section :id="explanationId" role="tooltip" class="grid w-96 max-w-[calc(100vw-2rem)] gap-3 p-4 text-z-xs leading-relaxed text-z-text-secondary">
<h4 class="m-0 font-heavy text-z-text">
{{ t('accounts.quotaForecast.forReferenceOnly') }}
</h4>
<p class="m-0">
@@ -37,15 +37,15 @@ const hasBalance = computed(() => !props.credits?.unlimited && balanceDisplay.va
<template>
<div class="flex min-w-0 flex-wrap items-baseline justify-between gap-x-4 gap-y-1 px-4 pb-4" :aria-label="t('accounts.quota.quotaPoints')">
<span class="text-cp-sm font-heavy text-cp-text">{{ t('accounts.quota.quotaPoints') }}</span>
<span class="text-z-sm font-heavy text-z-text">{{ t('accounts.quota.quotaPoints') }}</span>
<span class="flex min-w-0 items-baseline gap-1.5">
<strong
class="min-w-0 text-right text-cp-sm font-heavy wrap-anywhere"
:class="hasBalance ? 'font-mono tabular-nums text-cp-text' : 'text-cp-text-secondary'"
class="min-w-0 text-right text-z-sm font-heavy wrap-anywhere"
:class="hasBalance ? 'font-mono tabular-nums text-z-text' : 'text-z-text-secondary'"
>
{{ value }}
</strong>
<span v-if="hasBalance" class="shrink-0 text-cp-xs text-cp-text-tertiary">{{ t('accounts.quota.point') }}</span>
<span v-if="hasBalance" class="shrink-0 text-z-xs text-z-text-tertiary">{{ t('accounts.quota.point') }}</span>
</span>
</div>
</template>
@@ -53,7 +53,7 @@ const items = computed(() => props.windows.map((window) => {
<section class="grid min-w-0 gap-2">
<h4
v-if="grouped && label && windows[0]?.limitId !== 'codex'"
class="m-0 truncate text-cp-xs leading-4 font-heavy text-cp-text-secondary"
class="m-0 truncate text-z-xs leading-4 font-heavy text-z-text-secondary"
:title="label"
>
{{ label }}
@@ -65,12 +65,12 @@ const items = computed(() => props.windows.map((window) => {
>
<div v-for="item in items" :key="item.key" class="grid min-w-0 gap-1.5">
<template v-if="item.local">
<div class="flex min-w-0 items-baseline justify-between gap-2 text-cp-xs leading-3.5">
<span class="min-w-0 truncate font-bold text-cp-text-quaternary">
<div class="flex min-w-0 items-baseline justify-between gap-2 text-z-xs leading-3.5">
<span class="min-w-0 truncate font-bold text-z-text-quaternary">
{{ item.localLabel }}
</span>
<strong
class="shrink-0 font-mono font-heavy tabular-nums text-cp-text"
class="shrink-0 font-mono font-heavy tabular-nums text-z-text"
:title="t('accounts.quota.requestsInThisWindow', { requestDisplay: item.requestDisplay })"
>
{{ t('accounts.quota.requestCount', { requestDisplay: item.requestDisplay }) }}
@@ -83,7 +83,7 @@ const items = computed(() => props.windows.map((window) => {
:label="item.timelineTitle"
/>
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-cp-text-tertiary">
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-z-text-tertiary">
<span class="shrink-0 font-emphasis">{{ t('accounts.quota.statisticalRange') }}</span>
<span
class="min-w-0 truncate font-mono font-emphasis tabular-nums"
@@ -94,20 +94,20 @@ const items = computed(() => props.windows.map((window) => {
</p>
</template>
<template v-else>
<div class="flex min-w-0 items-baseline justify-between gap-2 text-cp-xs leading-3.5">
<span class="min-w-0 truncate font-bold text-cp-text-quaternary" :title="item.observationDisplay || undefined">
<div class="flex min-w-0 items-baseline justify-between gap-2 text-z-xs leading-3.5">
<span class="min-w-0 truncate font-bold text-z-text-quaternary" :title="item.observationDisplay || undefined">
{{ item.label }}
</span>
<span class="flex shrink-0 items-baseline gap-1.5 font-mono font-heavy tabular-nums">
<template v-if="item.amountUsedDisplay">
<strong class="text-cp-text-quaternary">
<strong class="text-z-text-quaternary">
{{ item.amountUsedDisplay }}<template v-if="item.usedPercent === null"> / {{ item.quotaValueDisplay }}</template>
</strong>
<span v-if="item.localUsageDisplay" class="font-normal text-cp-text-quaternary" aria-hidden="true">·</span>
<span v-if="item.localUsageDisplay" class="font-normal text-z-text-quaternary" aria-hidden="true">·</span>
</template>
<strong
v-if="item.localUsageDisplay"
class="text-cp-text-quaternary"
class="text-z-text-quaternary"
:title="t('accounts.quota.usageInThisWindow', { localUsageDisplay: item.localUsageDisplay })"
>
{{ item.localUsageDisplay }}
@@ -123,7 +123,7 @@ const items = computed(() => props.windows.map((window) => {
</div>
<div
class="h-1.5 w-full overflow-hidden rounded-full bg-cp-border-secondary"
class="h-1.5 w-full overflow-hidden rounded-full bg-z-border-secondary"
role="progressbar"
:aria-label="item.ariaLabel"
aria-valuemin="0"
@@ -138,7 +138,7 @@ const items = computed(() => props.windows.map((window) => {
/>
</div>
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-cp-text-tertiary">
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-z-text-tertiary">
<span class="shrink-0 font-emphasis">{{ t('common.actions.reset') }}</span>
<span class="min-w-0 truncate font-mono font-emphasis tabular-nums" :title="item.resetAtDisplay">
{{ item.resetAtDisplay }}
@@ -43,10 +43,10 @@ const { start: scheduleOpen, stop: cancelOpen } = useTimeoutFn(() => {
const option = computed<EChartsOption>(() => {
const colors = {
success: color('--cp-color-success', ''),
warning: color('--cp-color-warning', ''),
danger: color('--cp-color-error', ''),
unknown: color('--cp-color-border', ''),
success: color('color-success', ''),
warning: color('color-warning', ''),
danger: color('color-error', ''),
unknown: color('color-border', ''),
}
const nodes = props.nodes
return {
@@ -264,7 +264,7 @@ onBeforeUnmount(() => chart?.off('datazoom', onDataZoom))
<ZPopover ref="popover" v-model="open" class="w-full" placement="top" trigger="hover-click" :reference-element="anchor" :disabled="!activeNode?.windows.length" :hover-delay="180" animate-position>
<template #reference>
<div
class="relative w-full rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
class="relative w-full rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline"
role="slider" tabindex="0" :aria-valuemin="1" :aria-valuemax="nodes.length" :aria-valuenow="(activeIndex ?? 0) + 1" :aria-valuetext="t('accounts.upstream.account', { number: (activeIndex ?? 0) + 1 })" :aria-label="t('accounts.upstream.poolChart')" :aria-description="t('accounts.upstream.poolKeyboard')"
@pointermove="onPointerMove" @pointerleave="onPointerLeave" @click="onChartClick" @dblclick="resetZoom" @keydown="onKeydown"
>
@@ -65,10 +65,10 @@ watch(() => props.accountId, () => {
</script>
<template>
<section class="grid gap-2 rounded-cp bg-cp-fill-quaternary px-4 py-3" :aria-label="t('accounts.quotaReset.automaticallyUseResetCard')">
<section class="grid gap-2 rounded-z bg-z-fill-quaternary px-4 py-3" :aria-label="t('accounts.quotaReset.automaticallyUseResetCard')">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex items-center gap-1">
<h3 class="m-0 text-cp-sm font-heavy text-cp-text">
<h3 class="m-0 text-z-sm font-heavy text-z-text">
{{ t('accounts.quotaReset.automaticallyUsed') }}
</h3>
<ZPopover trigger="click" placement="bottom-start" role="dialog">
@@ -77,7 +77,7 @@ watch(() => props.accountId, () => {
<CircleHelp class="size-3.5" />
</ZIconButton>
</template>
<div class="grid max-w-64 gap-2 p-3 text-cp-xs leading-relaxed text-cp-text-secondary">
<div class="grid max-w-64 gap-2 p-3 text-z-xs leading-relaxed text-z-text-secondary">
<p>{{ t('accounts.quotaReset.automation.scheduleHelp') }}</p>
<p>{{ t('accounts.quotaReset.automation.verifyHelp') }}</p>
</div>
@@ -99,10 +99,10 @@ watch(() => props.accountId, () => {
/>
</div>
</div>
<p v-if="status?.enabled && status.nextConsumeAtDisplay" class="text-cp-xs text-cp-text-secondary">
<p v-if="status?.enabled && status.nextConsumeAtDisplay" class="text-z-xs text-z-text-secondary">
{{ t('accounts.quotaReset.nextUse', { nextConsumeAtDisplay: status.nextConsumeAtDisplay }) }}
</p>
<p v-if="resultText" class="text-cp-xs text-cp-warning" role="status">
<p v-if="resultText" class="text-z-xs text-z-warning" role="status">
{{ resultText }}
</p>
</section>
@@ -110,6 +110,6 @@ watch(() => props.accountId, () => {
<style scoped>
.reset-credit-switch :deep(input:not(:checked) + span) {
background-color: var(--cp-switch-unchecked-bg);
background-color: var(--z-switch-unchecked-bg);
}
</style>
@@ -125,8 +125,8 @@ function handleRequestConsume(creditId: string) {
<template>
<button
type="button"
class="inline-flex shrink-0 touch-manipulation items-center justify-center rounded-cp border-0 bg-transparent text-cp-text-secondary outline-none transition-[background-color,color,opacity,transform] duration-150 hover:bg-cp-fill-quaternary hover:text-cp-text active:bg-cp-fill-tertiary focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-container motion-safe:active:scale-[0.96] motion-reduce:transition-none"
:class="showTriggerCount ? 'h-cp-control-sm gap-1 px-2' : 'size-cp-control-sm'"
class="inline-flex shrink-0 touch-manipulation items-center justify-center rounded-z border-0 bg-transparent text-z-text-secondary outline-none transition-[background-color,color,opacity,transform] duration-150 hover:bg-z-fill-quaternary hover:text-z-text active:bg-z-fill-tertiary focus-visible:ring-2 focus-visible:ring-z-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-z-bg-container motion-safe:active:scale-[0.96] motion-reduce:transition-none"
:class="showTriggerCount ? 'h-z-control-sm gap-1 px-2' : 'size-z-control-sm'"
:aria-label="triggerLabel"
:aria-pressed="panelOpen || undefined"
:title="triggerLabel"
@@ -146,16 +146,16 @@ function handleRequestConsume(creditId: string) {
:show-close="!consuming" :close-on-click-modal="!consuming" :close-on-press-escape="!consuming"
>
<div v-if="confirmation" class="grid gap-3">
<section class="rounded-cp bg-cp-fill-quaternary px-4 py-3.5">
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
<section class="rounded-z bg-z-fill-quaternary px-4 py-3.5">
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
{{ t('accounts.quotaReset.thisTimeUse') }}
</p>
<p class="mt-1.5 mb-0 text-cp-lg leading-snug font-heavy text-cp-text">
<p class="mt-1.5 mb-0 text-z-lg leading-snug font-heavy text-z-text">
{{ confirmCreditTitle }}
</p>
<p
v-if="confirmation.credit"
class="mt-1 mb-0 font-mono text-[10px] leading-normal font-emphasis text-cp-text-quaternary"
class="mt-1 mb-0 font-mono text-[10px] leading-normal font-emphasis text-z-text-quaternary"
>
{{ confirmation.credit.expiresAtDisplay ? t('accounts.quotaReset.expires', { expiresAtDisplay: confirmation.credit.expiresAtDisplay }) : t('accounts.quotaReset.expiryHelp') }}
</p>
@@ -163,20 +163,20 @@ function handleRequestConsume(creditId: string) {
</div>
<div v-else class="grid gap-4">
<section class="overflow-hidden rounded-cp bg-cp-fill-quaternary" :aria-label="t('accounts.quotaReset.currentQuota')">
<section class="overflow-hidden rounded-z bg-z-fill-quaternary" :aria-label="t('accounts.quotaReset.currentQuota')">
<UsageLimits :windows="account.quota.windows" />
<AccountQuotaCredits :credits="account.quota.credits" />
</section>
<ResetCreditAutomation v-if="account.capabilities.consumeResetCredit" :key="account.id" :account-id="account.id" />
<section v-if="ambiguous" class="flex items-start gap-3 rounded-cp bg-cp-warning-container px-4 py-3.5" role="status">
<AlertTriangle class="mt-0.5 size-4 shrink-0 text-cp-warning-on-container" />
<section v-if="ambiguous" class="flex items-start gap-3 rounded-z bg-z-warning-container px-4 py-3.5" role="status">
<AlertTriangle class="mt-0.5 size-4 shrink-0 text-z-warning-on-container" />
<div class="min-w-0 flex-1">
<p class="m-0 text-cp-sm font-heavy text-cp-warning-on-container">
<p class="m-0 text-z-sm font-heavy text-z-warning-on-container">
{{ t('accounts.quotaReset.operationUnconfirmed') }}
</p>
<p class="mt-1 mb-0 text-cp-xs leading-normal font-emphasis text-cp-text-secondary">
<p class="mt-1 mb-0 text-z-xs leading-normal font-emphasis text-z-text-secondary">
{{ t('accounts.quotaReset.confirmPrevious') }}
</p>
</div>
@@ -187,7 +187,7 @@ function handleRequestConsume(creditId: string) {
<section class="grid gap-3" :aria-label="t('accounts.quotaReset.availableResetCards')">
<div class="flex items-center gap-2">
<h3 class="m-0 min-w-0 flex-1 text-cp-sm font-heavy text-cp-text">
<h3 class="m-0 min-w-0 flex-1 text-z-sm font-heavy text-z-text">
{{ t('accounts.quotaReset.availableResetCards') }}
</h3>
<ZTag size="small" :type="hasSnapshot && availableCount > 0 ? 'success' : 'default'" role="status">
@@ -210,7 +210,7 @@ function handleRequestConsume(creditId: string) {
<div v-if="!loadError" :aria-busy="loading || consuming">
<p
v-if="loading && !hasSnapshot"
class="m-0 px-4 py-4 text-cp-xs font-emphasis text-cp-text-secondary"
class="m-0 px-4 py-4 text-z-xs font-emphasis text-z-text-secondary"
role="status"
>
{{ t('accounts.quotaReset.queryingAvailableResetTimes') }}
@@ -219,13 +219,13 @@ function handleRequestConsume(creditId: string) {
<li
v-for="credit in creditItems"
:key="credit.id"
class="flex items-center gap-4 rounded-cp bg-cp-fill-quaternary px-4 py-3.5"
class="flex items-center gap-4 rounded-z bg-z-fill-quaternary px-4 py-3.5"
>
<div class="min-w-0 flex-1">
<p class="m-0 text-cp-sm leading-normal font-heavy wrap-anywhere text-cp-text">
<p class="m-0 text-z-sm leading-normal font-heavy wrap-anywhere text-z-text">
{{ credit.title }}
</p>
<p class="mt-1 mb-0 text-cp-xs leading-normal font-emphasis text-cp-text-secondary">
<p class="mt-1 mb-0 text-z-xs leading-normal font-emphasis text-z-text-secondary">
{{ credit.expiry }}
</p>
</div>
@@ -16,13 +16,13 @@ const nodes = computed(() => poolNodes(props.windows, props.pool?.accountCount))
<PoolChart v-if="nodes.length" :nodes="nodes" />
<div v-if="pool" class="flex min-h-5 min-w-0 items-center justify-between gap-x-3">
<div class="flex shrink-0 items-baseline gap-1 whitespace-nowrap" :aria-label="`${pool.states.ready} / ${pool.accountCount} ${t('accounts.upstream.ready')}`">
<strong class="font-mono text-cp-xs leading-[normal] font-heavy tabular-nums" :class="pool.states.ready > 0 ? 'text-cp-success-text' : 'text-cp-text'">{{ pool.states.ready }}</strong>
<span class="font-mono text-cp-xs leading-[normal] tabular-nums text-cp-text-quaternary">/ {{ pool.accountCount }}</span>
<strong class="font-mono text-z-xs leading-[normal] font-heavy tabular-nums" :class="pool.states.ready > 0 ? 'text-z-success-text' : 'text-z-text'">{{ pool.states.ready }}</strong>
<span class="font-mono text-z-xs leading-[normal] tabular-nums text-z-text-quaternary">/ {{ pool.accountCount }}</span>
</div>
<div v-if="pool.plans.length" class="flex min-w-0 flex-wrap items-center justify-end gap-x-3 gap-y-1.5">
<span v-for="plan in pool.plans" :key="plan.name" class="inline-flex min-w-0 items-center gap-1.5">
<AccountPlanBadge :plan-type="plan.name" :plan-type-display="plan.name" size="xs" />
<span class="font-mono text-cp-xs leading-[normal] font-emphasis tabular-nums text-cp-text-secondary">{{ plan.count }}</span>
<span class="font-mono text-z-xs leading-[normal] font-emphasis tabular-nums text-z-text-secondary">{{ plan.count }}</span>
</span>
</div>
</div>
@@ -43,16 +43,16 @@ const items = computed(() => orderedPanelQuotaWindows(props.windows)
class="grid gap-3 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] sm:items-center sm:gap-5"
>
<div class="min-w-0">
<h3 class="m-0 text-cp-sm font-heavy text-cp-text">
<h3 class="m-0 text-z-sm font-heavy text-z-text">
{{ item.label }}
</h3>
<p class="mt-1 mb-0 text-cp-xs leading-normal font-emphasis text-cp-text-secondary">
<p class="mt-1 mb-0 text-z-xs leading-normal font-emphasis text-z-text-secondary">
{{ t('accounts.quota.reset') }}<span class="font-mono tabular-nums">{{ item.resetAtDisplay }}</span>
</p>
</div>
<div class="flex min-w-0 items-center gap-3">
<div
class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-cp-border-secondary"
class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-z-border-secondary"
role="progressbar"
:aria-label="t('accounts.quota.remainingQuota', { label: item.label })"
aria-valuemin="0"
@@ -66,12 +66,12 @@ const items = computed(() => orderedPanelQuotaWindows(props.windows)
:style="item.barStyle"
/>
</div>
<span class="shrink-0 text-cp-xs font-heavy tabular-nums" :class="item.percentTextClass">
<span class="shrink-0 text-z-xs font-heavy tabular-nums" :class="item.percentTextClass">
{{ item.remainingDisplay }}
</span>
</div>
</div>
<p v-if="items.length === 0" class="m-0 text-cp-sm font-emphasis text-cp-text-secondary">
<p v-if="items.length === 0" class="m-0 text-z-sm font-emphasis text-z-text-secondary">
{{ t('accounts.quota.unobserved') }}
</p>
</section>
@@ -65,14 +65,14 @@ watch(hasPersonalInfo, (available) => {
</script>
<template>
<section class="flex min-h-0 min-w-0 flex-col rounded-lg bg-cp-bg-container p-4 shadow-cp-tertiary">
<section class="flex min-h-0 min-w-0 flex-col rounded-lg bg-z-bg-container p-4 shadow-z-tertiary">
<div class="mb-3 grid shrink-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-x-3 gap-y-1">
<h3 class="m-0 text-cp-lg font-heavy text-cp-text">
<h3 class="m-0 text-z-lg font-heavy text-z-text">
{{ account.upstream?.discovered ? t('accounts.upstream.quota') : t('accounts.quota.accountLimit') }}
</h3>
<p
v-if="account.capabilities.quota || quotaEntries.length > 0 || account.upstream?.discovered"
class="col-span-2 row-start-2 m-0 flex min-w-0 items-center gap-1.5 text-cp-xs font-emphasis text-cp-text-secondary"
class="col-span-2 row-start-2 m-0 flex min-w-0 items-center gap-1.5 text-z-xs font-emphasis text-z-text-secondary"
>
<span class="shrink-0 whitespace-nowrap">{{ t('accounts.quota.quota', { provider: formatProviderLabel(account.provider) }) }}</span>
<template v-if="account.planType || account.authenticationKind === 'api_key'">
@@ -36,7 +36,7 @@ const hasQuotaWindow = computed(() => props.windows.some(window => typeof window
<template #reference="{ open }">
<button
type="button"
class="col-span-full grid w-full min-w-0 grid-cols-subgrid cursor-pointer rounded-sm border-0 bg-transparent p-0 text-left outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
class="col-span-full grid w-full min-w-0 grid-cols-subgrid cursor-pointer rounded-sm border-0 bg-transparent p-0 text-left outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline"
:aria-label="t('accounts.quota.viewDetails', { detailTitle })"
:aria-expanded="open"
aria-haspopup="dialog"
@@ -32,7 +32,7 @@ const highestUsageWindow = computed(() => props.windows.reduce((highest, window)
const highestUsageDisplay = computed(() => highestUsageWindow.value?.usedPercentDisplay ?? '—')
const highestUsageTextClass = computed(() => highestUsageWindow.value
? quotaWindowPresentation(highestUsageWindow.value, '2px').percentTextClass
: 'text-cp-text-quaternary')
: 'text-z-text-quaternary')
const trackGridStyle = computed(() => ({
gridTemplateColumns: `repeat(${Math.max(windowItems.value.length, 1)}, minmax(0, 1fr))`,
@@ -42,7 +42,7 @@ const trackGridStyle = computed(() => ({
<template>
<div class="col-span-full grid min-w-0 grid-cols-subgrid gap-y-1.5">
<div class="col-span-full grid min-w-0 grid-cols-subgrid items-baseline text-[10px] leading-3 font-bold">
<span class="min-w-0 truncate text-cp-text-quaternary">
<span class="min-w-0 truncate text-z-text-quaternary">
{{ label }}
</span>
<strong
@@ -58,7 +58,7 @@ const trackGridStyle = computed(() => ({
<div
v-for="item in windowItems"
:key="item.key"
class="h-1 w-full overflow-hidden rounded-full bg-cp-border-secondary"
class="h-1 w-full overflow-hidden rounded-full bg-z-border-secondary"
role="progressbar"
:aria-label="item.labelTooltip"
aria-valuemin="0"
@@ -50,7 +50,7 @@ const summaryValue = computed(() => {
})
const currentUsageTextClass = computed(() => currentUsageWindow.value
? quotaWindowPresentation(currentUsageWindow.value, '2px').percentTextClass
: 'text-cp-text-quaternary')
: 'text-z-text-quaternary')
const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length - 1, 0))
</script>
@@ -65,16 +65,16 @@ const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length
class="flex min-w-0 items-baseline gap-1 font-mono tabular-nums"
:title="summaryValue.title"
>
<strong class="truncate text-cp-xs font-heavy text-cp-text">
<strong class="truncate text-z-xs font-heavy text-z-text">
{{ summaryValue.value }}
</strong>
<span class="shrink-0 text-[9px] font-emphasis tracking-[0.02em] text-cp-text-quaternary">
<span class="shrink-0 text-[9px] font-emphasis tracking-[0.02em] text-z-text-quaternary">
{{ summaryValue.label }}
</span>
</span>
<span
v-if="currentUsageWindow"
class="ml-auto flex shrink-0 items-baseline gap-1 text-[9px] font-emphasis text-cp-text-quaternary"
class="ml-auto flex shrink-0 items-baseline gap-1 text-[9px] font-emphasis text-z-text-quaternary"
:title="t('accounts.quota.usageRateHelp')"
>
<span>{{ t('accounts.quota.usageRate') }}</span>
@@ -100,7 +100,7 @@ const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length
</div>
<span
v-if="additionalEntryCount > 0"
class="grid h-5 min-w-5 shrink-0 place-items-center rounded-cp bg-cp-fill-quaternary px-1.5 font-mono text-[9px] font-heavy tabular-nums text-cp-text-tertiary"
class="grid h-5 min-w-5 shrink-0 place-items-center rounded-z bg-z-fill-quaternary px-1.5 font-mono text-[9px] font-heavy tabular-nums text-z-text-tertiary"
:title="t('accounts.quota.moreGroups', { count: additionalEntryCount })"
>
+{{ additionalEntryCount }}
@@ -66,14 +66,14 @@ const presentation = computed(() =>
>
<span
v-if="variant === 'pill'"
class="inline-flex h-7 shrink-0 items-center rounded-full px-2.5 text-cp-sm leading-none font-heavy transition-colors duration-150 motion-reduce:transition-none"
class="inline-flex h-7 shrink-0 items-center rounded-full px-2.5 text-z-sm leading-none font-heavy transition-colors duration-150 motion-reduce:transition-none"
:class="presentation.statusStyle.text"
>
{{ presentation.label }}
</span>
<span
v-else
class="inline-flex items-center gap-1.5 text-cp-sm leading-none font-emphasis transition-colors duration-150 motion-reduce:transition-none"
class="inline-flex items-center gap-1.5 text-z-sm leading-none font-emphasis transition-colors duration-150 motion-reduce:transition-none"
:class="presentation.statusStyle.text"
>
<span class="size-1.5 rounded-full" :class="presentation.statusStyle.dot" />
@@ -82,20 +82,20 @@ const presentation = computed(() =>
</component>
</template>
<section class="w-88 overflow-hidden rounded-cp-lg">
<header class="flex items-start gap-3 bg-cp-popover-header-bg px-4 py-3">
<section class="w-88 overflow-hidden rounded-z-lg">
<header class="flex items-start gap-3 bg-z-popover-header-bg px-4 py-3">
<span
class="inline-flex size-9 shrink-0 items-center justify-center rounded-cp"
class="inline-flex size-9 shrink-0 items-center justify-center rounded-z"
:class="presentation.statusStyle.icon"
>
<component :is="presentation.icon" class="size-4.5" />
</span>
<div class="min-w-0 flex-1">
<p class="m-0 text-cp-xs leading-none font-heavy tracking-[0.08em] text-cp-text-tertiary">
<p class="m-0 text-z-xs leading-none font-heavy tracking-[0.08em] text-z-text-tertiary">
{{ t('accounts.status.accountStatus') }}
</p>
<h3 class="mt-1.5 mb-0 text-cp-lg leading-5 font-heavy text-cp-text text-balance">
<h3 class="mt-1.5 mb-0 text-z-lg leading-5 font-heavy text-z-text text-balance">
{{ presentation.title }}
</h3>
</div>
@@ -108,14 +108,14 @@ const presentation = computed(() =>
</span>
</header>
<div class="grid gap-3 px-4 py-3 text-cp-sm leading-5">
<p class="m-0 text-pretty font-emphasis text-cp-text-secondary">
<div class="grid gap-3 px-4 py-3 text-z-sm leading-5">
<p class="m-0 text-pretty font-emphasis text-z-text-secondary">
{{ presentation.description }}
</p>
<div v-if="presentation.protectionRules.length" class="grid gap-1.5">
<span class="text-cp-xs font-emphasis text-cp-text-tertiary">{{ t('accounts.status.matchedProtectionRules') }}</span>
<ul class="m-0 list-none space-y-1 p-0 text-cp-text">
<span class="text-z-xs font-emphasis text-z-text-tertiary">{{ t('accounts.status.matchedProtectionRules') }}</span>
<ul class="m-0 list-none space-y-1 p-0 text-z-text">
<li v-for="rule in presentation.protectionRules" :key="rule.id" class="wrap-anywhere">
{{ rule.name }}
</li>
@@ -124,42 +124,42 @@ const presentation = computed(() =>
<div
v-if="presentation.rateLimitRecovery"
class="flex items-center justify-between gap-3 rounded-cp bg-cp-fill-quaternary px-3 py-2"
class="flex items-center justify-between gap-3 rounded-z bg-z-fill-quaternary px-3 py-2"
>
<span class="font-heavy text-cp-text-tertiary">{{ presentation.recoveryTimeLabel }}</span>
<span class="font-mono font-emphasis tabular-nums text-cp-text">
<span class="font-heavy text-z-text-tertiary">{{ presentation.recoveryTimeLabel }}</span>
<span class="font-mono font-emphasis tabular-nums text-z-text">
{{ presentation.rateLimitRecovery }}
</span>
</div>
<div
v-if="presentation.nextRefreshDisplay"
class="flex items-center justify-between gap-3 rounded-cp bg-cp-fill-quaternary px-3 py-2"
class="flex items-center justify-between gap-3 rounded-z bg-z-fill-quaternary px-3 py-2"
>
<span class="font-heavy text-cp-text-tertiary">{{ t('accounts.status.tryNextTime') }}</span>
<span class="font-mono font-emphasis tabular-nums text-cp-text">
<span class="font-heavy text-z-text-tertiary">{{ t('accounts.status.tryNextTime') }}</span>
<span class="font-mono font-emphasis tabular-nums text-z-text">
{{ presentation.nextRefreshDisplay }}
</span>
</div>
<div class="rounded-cp bg-cp-fill-quaternary px-3 py-2.5">
<p class="m-0 text-cp-xs leading-none font-heavy text-cp-text-tertiary">
<div class="rounded-z bg-z-fill-quaternary px-3 py-2.5">
<p class="m-0 text-z-xs leading-none font-heavy text-z-text-tertiary">
{{ t('accounts.status.recommendedAction') }}
</p>
<p class="mt-1.5 mb-0 text-pretty font-emphasis text-cp-text">
<p class="mt-1.5 mb-0 text-pretty font-emphasis text-z-text">
{{ presentation.recoveryHint }}
</p>
</div>
<div v-if="presentation.errorText" class="overflow-hidden rounded-cp bg-cp-fill-tertiary">
<div v-if="presentation.errorText" class="overflow-hidden rounded-z bg-z-fill-tertiary">
<div class="flex items-center justify-between gap-3 px-3 pt-2.5 pb-1.5">
<span class="text-cp-xs leading-none font-heavy text-cp-text-tertiary">{{ t('accounts.status.upstreamOriginalFeedback') }}</span>
<span class="shrink-0 text-[10px] leading-none font-emphasis text-cp-text-quaternary">{{ t('accounts.status.forTroubleshootingOnly') }}</span>
<span class="text-z-xs leading-none font-heavy text-z-text-tertiary">{{ t('accounts.status.upstreamOriginalFeedback') }}</span>
<span class="shrink-0 text-[10px] leading-none font-emphasis text-z-text-quaternary">{{ t('accounts.status.forTroubleshootingOnly') }}</span>
</div>
<ZScrollbar class="bg-cp-fill-quaternary" max-height="124px">
<ZScrollbar class="bg-z-fill-quaternary" max-height="124px">
<div class="px-3 py-2">
<pre
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-cp-xs leading-[1.55] font-emphasis text-cp-text-secondary"
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-z-xs leading-[1.55] font-emphasis text-z-text-secondary"
>{{ presentation.errorText }}</pre>
</div>
</ZScrollbar>
@@ -58,34 +58,34 @@ interface AccountStatusPresentationInput {
const statusStyles: Record<AccountStatusTone, AccountStatusStyle> = {
success: {
text: 'text-cp-success-text',
dot: 'bg-cp-success',
badge: 'bg-cp-success-container text-cp-success-on-container',
icon: 'bg-cp-success-container text-cp-success-on-container',
text: 'text-z-success-text',
dot: 'bg-z-success',
badge: 'bg-z-success-container text-z-success-on-container',
icon: 'bg-z-success-container text-z-success-on-container',
},
danger: {
text: 'text-cp-error-text',
dot: 'bg-cp-error',
badge: 'bg-cp-error-container text-cp-error-on-container',
icon: 'bg-cp-error-container text-cp-error-on-container',
text: 'text-z-error-text',
dot: 'bg-z-error',
badge: 'bg-z-error-container text-z-error-on-container',
icon: 'bg-z-error-container text-z-error-on-container',
},
warning: {
text: 'text-cp-warning-text',
dot: 'bg-cp-warning',
badge: 'bg-cp-warning-container text-cp-warning-on-container',
icon: 'bg-cp-warning-container text-cp-warning-on-container',
text: 'text-z-warning-text',
dot: 'bg-z-warning',
badge: 'bg-z-warning-container text-z-warning-on-container',
icon: 'bg-z-warning-container text-z-warning-on-container',
},
info: {
text: 'text-cp-info-text',
dot: 'bg-cp-info',
badge: 'bg-cp-info-container text-cp-info-on-container',
icon: 'bg-cp-info-container text-cp-info-on-container',
text: 'text-z-info-text',
dot: 'bg-z-info',
badge: 'bg-z-info-container text-z-info-on-container',
icon: 'bg-z-info-container text-z-info-on-container',
},
normal: {
text: 'text-cp-text-secondary',
dot: 'bg-cp-text-quaternary',
badge: 'bg-cp-fill-quaternary text-cp-text-secondary',
icon: 'bg-cp-fill-quaternary text-cp-text-secondary',
text: 'text-z-text-secondary',
dot: 'bg-z-text-quaternary',
badge: 'bg-z-fill-quaternary text-z-text-secondary',
icon: 'bg-z-fill-quaternary text-z-text-secondary',
},
}
@@ -0,0 +1,63 @@
import type { FormRules } from '@codex-proxy/ui'
import type { ApiKeyAccountForm } from '../utils/upstreamApiKey'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
export function useAccountApiKeyFormRules(editing = false) {
const { t } = useI18n({ useScope: 'global' })
return computed<FormRules<{ apiKey: ApiKeyAccountForm }>>(() => ({
'apiKey.name': {
required: !editing,
whitespace: true,
trigger: ['blur', 'change'],
message: t('accounts.credentials.nameRequired'),
},
'apiKey.transport': {
required: true,
trigger: 'change',
message: t('accounts.credentials.validation.transportRequired'),
},
'apiKey.base_url': [
{
required: true,
whitespace: true,
trigger: ['blur', 'change'],
message: t('accounts.credentials.validation.urlRequired'),
},
{
max: 2048,
transform: value => value.trim(),
trigger: ['blur', 'change'],
message: t('accounts.credentials.validation.urlLength'),
},
{
trigger: ['blur', 'change'],
validator: (_rule, value) => {
try {
const url = new URL(value)
return ['https:', 'http:'].includes(url.protocol) && !!url.hostname
&& !url.username && !url.password && !url.search && !url.hash
}
catch {
return false
}
},
message: t('accounts.credentials.validation.urlFormat'),
},
],
'apiKey.apiKey': [
{
required: !editing,
trigger: ['blur', 'change'],
message: t('accounts.credentials.apiKeyRequired'),
},
{
type: 'string',
pattern: /^[\x21-\x7E]+$/,
max: 16384,
trigger: ['blur', 'change'],
message: t('accounts.credentials.validation.apiKeyFormat'),
},
],
}))
}
@@ -6,8 +6,7 @@ import { batchUpdateAccounts } from '@/api'
import { useAsyncAction } from '@/composables/useAsyncAction'
import { t } from '@/i18n'
import { notification as ZNotification } from '@/utils/notification'
import { accountModelAccessError } from '../utils/modelAccess'
import { concurrencyLimitInput, parseAccountSchedulingForm } from '../utils/schedulingForm'
import { accountSchedulingValues, concurrencyLimitInput } from '../utils/schedulingForm'
export function useAccountBatchEditor(options: {
accounts: Ref<Account[]>
@@ -67,26 +66,11 @@ export function useAccountBatchEditor(options: {
showBatchEditModal.value = false
return
}
const modelError = accountModelAccessError(modelAccess.value)
if (modelError) {
ZNotification.warning({ message: modelError })
return
}
const scheduling = parseAccountSchedulingForm(editedFields.value.has('concurrencyLimit')
const scheduling = accountSchedulingValues(editedFields.value.has('concurrencyLimit')
? concurrencyLimit.value
: '', editedFields.value.has('weight')
? weight.value
: '1')
if (proxyMode.value === 'proxy' && !proxyId.value.trim()) {
ZNotification.warning({
message: t('accounts.batchEdit.proxyRequired'),
})
return
}
if (!scheduling.valid) {
ZNotification.warning({ message: scheduling.message })
return
}
await saveAction.run(async () => {
const accountIds = selectedAccounts().map(account => account.id)
@@ -100,10 +84,10 @@ export function useAccountBatchEditor(options: {
: proxyId.value.trim(),
enabled: schedulingEnabled.value,
concurrencyLimit: editedFields.value.has('concurrencyLimit')
? scheduling.values.concurrencyLimit
? scheduling.concurrencyLimit
: undefined,
weight: editedFields.value.has('weight')
? scheduling.values.weight
? scheduling.weight
: undefined,
groupIds: editedFields.value.has('groupIds')
? [
@@ -156,6 +140,7 @@ export function useAccountBatchEditor(options: {
concurrencyLimit,
weight,
modelAccess,
editedFields,
catalogAccountId,
proxyMode,
proxyId,

Some files were not shown because too many files have changed in this diff Show More