mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-14 16:33:06 +00:00
✨ style(web): unify design system across default frontend
🔤 Typography - Fix --font-sans stack: 'Public Sans Variable' was never applied (wrong family name), add CJK fallbacks (PingFang SC / Microsoft YaHei UI / Noto Sans SC) - Sync <html lang> with i18n language for correct CJK glyph selection - Remove dead fake font system (font-provider.tsx, config/fonts.ts, --font-inter/--font-manrope) - Typography contract: 3-tier headings, arbitrary text-[10px]/[11px] → text-xs (46 files), no uppercase table headers, font-bold → font-semibold - Numbers use tabular-nums only; font-mono reserved for keys/IDs/code (29 files) 🎨 Color discipline - Retire StatusBadge string-hash rainbow (autoColor → neutral); all variants resolve to 5 semantic voices - Add tintedBadgeClassMap as the single tinted-pill recipe - Replace hardcoded emerald/amber/rose/sky classes with semantic tokens across ~30 files (timing badges, progress bars, system panels, pricing, rankings, log row tints) - Drop all !important text overrides and [font-family:var(--font-body)] hacks 🧘 Motion restraint - Remove admin entrance animations: table row stagger, card hover lift/shadow, button press scale, dashboard shimmer line - Neuter page-transition stagger components into plain wrappers; page transitions reduced to pure opacity fade - Decorative motion stays landing-only 📐 Shape - Single radius source: --radius 1rem → 0.625rem - Sparklines use solid chart tokens instead of decorative gradients - PanelWrapper: border-only (no shadow), align radius with Card 📏 Guardrails - Add "Design System Discipline" section (3.10.1) to web/default/AGENTS.md ✅ Verified: tsgo typecheck clean, rsbuild production build passes
This commit is contained in:
Vendored
+36
@@ -125,6 +125,41 @@
|
|||||||
- 以 Tailwind 工具类为主,动态类名用 `cn()` 合并;非动态场景避免内联样式。
|
- 以 Tailwind 工具类为主,动态类名用 `cn()` 合并;非动态场景避免内联样式。
|
||||||
- 响应式采用移动优先与 Tailwind 断点(`sm:`、`md:`、`lg:` 等);主题与暗色用 CSS 变量与 `dark:`,自定义样式集中在 `src/styles/`,组件内尽量少写自定义 CSS。
|
- 响应式采用移动优先与 Tailwind 断点(`sm:`、`md:`、`lg:` 等);主题与暗色用 CSS 变量与 `dark:`,自定义样式集中在 `src/styles/`,组件内尽量少写自定义 CSS。
|
||||||
|
|
||||||
|
#### 3.10.1 设计系统纪律(强约束)
|
||||||
|
|
||||||
|
以下规则为硬约束,代码评审与 AI 改动一律遵循;违反即为缺陷。
|
||||||
|
|
||||||
|
**颜色**
|
||||||
|
|
||||||
|
- 后台界面(除 `features/home/` 营销页外)**禁止硬编码 Tailwind 调色板类**(如 `text-emerald-600`、`bg-amber-50`、`border-rose-200`),一律使用语义令牌:`success` / `warning` / `destructive` / `info` / `neutral`、`primary` / `muted` / `accent` / `border` / `ring`。暗色适配由令牌完成,禁止 `dark:text-emerald-400` 之类逐处覆写。
|
||||||
|
- 状态色仅表达状态(成功/警告/错误/信息),不得用于装饰或分类;分类信息(模型名、分组名、渠道名等)一律中性呈现,身份靠文本与图标传达。`StatusBadge` 的 `autoColor` 已废弃为 neutral,禁止恢复字符串哈希取色。
|
||||||
|
- 需要浅底状态胶囊时使用 `status-badge.tsx` 的 `tintedBadgeClassMap`,不要再手写 `bg-xxx-50 text-xxx-700 dark:...` 组合。
|
||||||
|
- 图表用 `--chart-1..5`;用户头像身份色走 `getAvatarColorClass`。这两处是仅有的多色场景。
|
||||||
|
- 禁止用 `!important`(`!text-*` 等)压制文字颜色/字号;若需要覆盖,说明层级设计有误,先修组件。
|
||||||
|
|
||||||
|
**排版**
|
||||||
|
|
||||||
|
- 字体族只有两轨:正文 `--font-sans`(含 CJK 回退,勿删栈中中文字体)与 `--font-serif`(serif 主题轴)。`font-mono` 仅用于密钥、ID、代码、原始 JSON;**数字对齐一律 `tabular-nums`,不得再加 `font-mono`**。
|
||||||
|
- 字号只用 `text-xs/sm/base/lg/xl/2xl`,**禁止任意值字号**(`text-[10px]`、`text-[11px]` 等;营销页除外)。
|
||||||
|
- 字重只用 `font-medium`(强调/表头)与 `font-semibold`(标题/关键数值);后台正文与标题**禁用 `font-bold`**。
|
||||||
|
- 标题三档契约:页题 `text-lg font-semibold tracking-tight`;卡片/区块题 `text-base font-semibold`;面板/小节题 `text-sm font-medium`。禁止响应式跳字号的标题(如 `text-base sm:text-lg`)。
|
||||||
|
- 表头不使用 `uppercase + tracking-wider`。
|
||||||
|
|
||||||
|
**圆角与阴影**
|
||||||
|
|
||||||
|
- 圆角单源 `--radius`(默认 0.625rem),组件用派生档 `rounded-md/lg/xl`;禁止在业务代码里写死圆角像素或混用 `rounded-2xl`/`rounded-4xl` 表达同类容器。
|
||||||
|
- 卡片边界「描边或投影二选一」:默认 `border`(或 Card 自带 ring),禁止再叠 `shadow-*`;`shadow` 只保留给浮层(popover/dropdown/dialog)。
|
||||||
|
|
||||||
|
**动效**
|
||||||
|
|
||||||
|
- 后台界面禁止入场动画:无 stagger、无 translate/scale/blur 入场、无按钮按压缩放。页面切换只允许纯透明度 fade(见 `lib/motion.ts` 的 `pageEnter`)。`page-transition.tsx` 的 Stagger 系列组件已固化为纯容器,禁止恢复动画。
|
||||||
|
- 允许的动效上限:颜色/透明度过渡 ≤150ms、骨架屏 shimmer、`Collapsible/Accordion` 展开、加载 spinner。装饰性动效只允许出现在 `features/home/`(landing)。
|
||||||
|
|
||||||
|
**徽章与图标**
|
||||||
|
|
||||||
|
- 文本徽章统一 `StatusBadge`(五种语义 voice);模型/分组/渠道等实体徽章统一中性底(`border-border/60 bg-muted/30`)。不要新造第三种徽章样式。
|
||||||
|
- 业务代码图标一律 `lucide-react`;`components/ui/` 由 shadcn 生成器维护(Hugeicons),不要手改基础组件图标库;AI 品牌图标用 `@lobehub/icons`(经 `getLobeIcon`)。
|
||||||
|
|
||||||
### 3.11 文件组织
|
### 3.11 文件组织
|
||||||
|
|
||||||
- **功能模块**:置于 `src/features/<feature>/`,内含 `components/`、`lib/`、`hooks/`,以及按需的 `api.ts`、`types.ts`、`constants.ts`、入口组件等。
|
- **功能模块**:置于 `src/features/<feature>/`,内含 `components/`、`lib/`、`hooks/`,以及按需的 `api.ts`、`types.ts`、`constants.ts`、入口组件等。
|
||||||
@@ -178,3 +213,4 @@
|
|||||||
- **2026-01-29**:重组文档结构,合并重复内容,明确主次与交叉引用。
|
- **2026-01-29**:重组文档结构,合并重复内容,明确主次与交叉引用。
|
||||||
- **2026-01-31**:在 3.2 中补充「类型检查」要求:改动 TS/TSX 后须执行 typecheck 并修复至无错。
|
- **2026-01-31**:在 3.2 中补充「类型检查」要求:改动 TS/TSX 后须执行 typecheck 并修复至无错。
|
||||||
- **2026-06-21**:在 3.2 中补充「Lint 检查」要求:完成代码改动前须修复所涉及文件的所有 lint error。
|
- **2026-06-21**:在 3.2 中补充「Lint 检查」要求:完成代码改动前须修复所涉及文件的所有 lint error。
|
||||||
|
- **2026-07-11**:新增 3.10.1「设计系统纪律」:语义色硬约束(去彩虹徽章/硬编码调色板)、排版契约(字号/字重/标题三档/mono 边界)、圆角单源、后台零入场动效、徽章与图标单轨。
|
||||||
|
|||||||
+1
-1
@@ -413,7 +413,7 @@ export const CodeBlockFrame = ({
|
|||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<div className='bg-muted/35 border-border/70 flex min-h-10 items-center gap-2 border-b px-2 py-1.5'>
|
<div className='bg-muted/35 border-border/70 flex min-h-10 items-center gap-2 border-b px-2 py-1.5'>
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<div className='text-muted-foreground truncate font-mono text-[11px] font-medium tracking-wide uppercase'>
|
<div className='text-muted-foreground truncate font-mono text-xs font-medium tracking-wide uppercase'>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function CompactContent<TData>({ row }: { row: Row<TData> }) {
|
|||||||
return (
|
return (
|
||||||
<div key={cell.id} className='min-w-0 flex-1 overflow-hidden'>
|
<div key={cell.id} className='min-w-0 flex-1 overflow-hidden'>
|
||||||
{label && (
|
{label && (
|
||||||
<div className='text-muted-foreground mb-0.5 text-[10px] leading-none select-none'>
|
<div className='text-muted-foreground mb-0.5 text-xs leading-none select-none'>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -177,7 +177,7 @@ function FallbackContent<TData>({ row }: { row: Row<TData> }) {
|
|||||||
key={cell.id}
|
key={cell.id}
|
||||||
className='flex items-start justify-between gap-2 overflow-hidden'
|
className='flex items-start justify-between gap-2 overflow-hidden'
|
||||||
>
|
>
|
||||||
<span className='text-muted-foreground shrink-0 text-[10px] font-medium select-none'>
|
<span className='text-muted-foreground shrink-0 text-xs font-medium select-none'>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs'>
|
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs'>
|
||||||
|
|||||||
+2
-3
@@ -24,10 +24,9 @@ export const staticDataTableClassNames = {
|
|||||||
compactHeaderRow: 'hover:bg-transparent',
|
compactHeaderRow: 'hover:bg-transparent',
|
||||||
mutedHeaderRow:
|
mutedHeaderRow:
|
||||||
'[background-color:var(--table-header)] hover:[background-color:var(--table-header-hover)]',
|
'[background-color:var(--table-header)] hover:[background-color:var(--table-header-hover)]',
|
||||||
compactHeaderCell:
|
compactHeaderCell: 'text-muted-foreground py-2 text-xs font-medium',
|
||||||
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase',
|
|
||||||
compactHeaderCellRight:
|
compactHeaderCellRight:
|
||||||
'text-muted-foreground py-2 text-right text-[10px] font-medium tracking-wider uppercase',
|
'text-muted-foreground py-2 text-right text-xs font-medium',
|
||||||
compactCell: 'py-2.5',
|
compactCell: 'py-2.5',
|
||||||
compactTopCell: 'py-2.5 align-top',
|
compactTopCell: 'py-2.5 align-top',
|
||||||
compactTopNumericCell: 'py-2.5 text-right align-top font-mono',
|
compactTopNumericCell: 'py-2.5 text-right align-top font-mono',
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ export function GroupBadge(props: GroupBadgeProps) {
|
|||||||
<span className='max-w-full min-w-0 overflow-hidden'>{badge}</span>
|
<span className='max-w-full min-w-0 overflow-hidden'>{badge}</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex h-5 shrink-0 items-center rounded-full px-1.5 font-mono text-xs leading-none font-medium tabular-nums',
|
'inline-flex h-5 shrink-0 items-center rounded-full px-1.5 text-xs leading-none font-medium tabular-nums',
|
||||||
getGroupRatioClassName(ratio)
|
getGroupRatioClassName(ratio)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-8
@@ -97,10 +97,7 @@ function hardenIsolatedHtml(html: string): string {
|
|||||||
|
|
||||||
template.content.querySelectorAll('a[target="_blank"]').forEach((link) => {
|
template.content.querySelectorAll('a[target="_blank"]').forEach((link) => {
|
||||||
const rel = new Set(
|
const rel = new Set(
|
||||||
link
|
link.getAttribute('rel')?.split(/\s+/).filter(Boolean) ?? []
|
||||||
.getAttribute('rel')
|
|
||||||
?.split(/\s+/)
|
|
||||||
.filter(Boolean) ?? []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rel.add('noopener')
|
rel.add('noopener')
|
||||||
@@ -182,10 +179,7 @@ function IsolatedHtmlContent(props: {
|
|||||||
}, [props.html])
|
}, [props.html])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={containerRef} className={cn('block w-full', props.className)} />
|
||||||
ref={containerRef}
|
|
||||||
className={cn('block w-full', props.className)}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -229,7 +229,7 @@ export function JsonCodeEditor({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1 text-xs',
|
'flex items-center gap-1 text-xs',
|
||||||
jsonStatus.valid ? 'text-emerald-600' : 'text-destructive'
|
jsonStatus.valid ? 'text-success' : 'text-destructive'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{jsonStatus.valid ? (
|
{jsonStatus.valid ? (
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function NavGroup({ title, items }: NavGroupProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup className='px-2 py-1'>
|
<SidebarGroup className='px-2 py-1'>
|
||||||
<SidebarGroupLabel className='text-muted-foreground/70 px-2 text-[11px] font-medium tracking-wider uppercase'>
|
<SidebarGroupLabel className='text-muted-foreground/70 px-2 text-xs font-medium tracking-wider uppercase'>
|
||||||
{title}
|
{title}
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
|
|||||||
)}
|
)}
|
||||||
<div className='flex flex-wrap items-center justify-between gap-x-3 gap-y-2 sm:gap-x-4'>
|
<div className='flex flex-wrap items-center justify-between gap-x-3 gap-y-2 sm:gap-x-4'>
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<h2 className='truncate text-base font-bold tracking-tight sm:text-lg'>
|
<h2 className='truncate text-lg font-semibold tracking-tight'>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+6
-6
@@ -253,7 +253,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground px-2 py-1 font-medium',
|
'text-muted-foreground px-2 py-1 font-medium',
|
||||||
isMobile ? 'text-xs' : 'text-[10px]'
|
isMobile ? 'text-xs' : 'text-xs'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t('{{category}} Models', { category })}
|
{t('{{category}} Models', { category })}
|
||||||
@@ -274,7 +274,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'truncate font-medium',
|
'truncate font-medium',
|
||||||
isMobile ? 'text-sm' : 'text-[11px]'
|
isMobile ? 'text-sm' : 'text-xs'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className='inline'>{model.label}</span>
|
<span className='inline'>{model.label}</span>
|
||||||
@@ -400,7 +400,7 @@ export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
|
|||||||
className={isMobile ? '!max-h-full flex-1 p-2' : 'max-h-[240px]'}
|
className={isMobile ? '!max-h-full flex-1 p-2' : 'max-h-[240px]'}
|
||||||
>
|
>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<div className='text-muted-foreground px-2 py-1 text-[10px] font-medium'>
|
<div className='text-muted-foreground px-2 py-1 text-xs font-medium'>
|
||||||
{t('Model Group')}
|
{t('Model Group')}
|
||||||
</div>
|
</div>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
@@ -417,7 +417,7 @@ export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
|
|||||||
>
|
>
|
||||||
<div className='flex min-w-0 flex-1 items-center gap-2 pr-4'>
|
<div className='flex min-w-0 flex-1 items-center gap-2 pr-4'>
|
||||||
<div className='flex min-w-0 flex-1 flex-col'>
|
<div className='flex min-w-0 flex-1 flex-col'>
|
||||||
<span className='text-foreground truncate text-[11px] font-medium'>
|
<span className='text-foreground truncate text-xs font-medium'>
|
||||||
{group.label}
|
{group.label}
|
||||||
</span>
|
</span>
|
||||||
{(group.desc || group.description) && (
|
{(group.desc || group.description) && (
|
||||||
@@ -658,7 +658,7 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
|
|||||||
<span className='min-w-0 truncate text-xs'>
|
<span className='min-w-0 truncate text-xs'>
|
||||||
{currentModel?.label || t('Model')}
|
{currentModel?.label || t('Model')}
|
||||||
</span>
|
</span>
|
||||||
<span className='bg-muted text-muted-foreground hidden max-w-20 shrink-0 rounded px-1.5 py-0.5 text-[10px] sm:inline-flex'>
|
<span className='bg-muted text-muted-foreground hidden max-w-20 shrink-0 rounded px-1.5 py-0.5 text-xs sm:inline-flex'>
|
||||||
{currentGroup?.label || t('Group')}
|
{currentGroup?.label || t('Group')}
|
||||||
</span>
|
</span>
|
||||||
<ChevronsUpDown className='text-muted-foreground ml-auto size-3.5 shrink-0 opacity-60' />
|
<ChevronsUpDown className='text-muted-foreground ml-auto size-3.5 shrink-0 opacity-60' />
|
||||||
@@ -672,7 +672,7 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
|
|||||||
!isMobile && modelGroupSelectorLayoutClasses.groupColumn
|
!isMobile && modelGroupSelectorLayoutClasses.groupColumn
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='text-muted-foreground px-1 text-[11px] leading-4 font-medium'>
|
<div className='text-muted-foreground px-1 text-xs leading-4 font-medium'>
|
||||||
{t('Model Group')}
|
{t('Model Group')}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
+1
-1
@@ -318,7 +318,7 @@ export function NotificationPopover({
|
|||||||
{unreadCount > 0 ? (
|
{unreadCount > 0 ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant='destructive'
|
variant='destructive'
|
||||||
className='absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center px-1 text-[10px] font-semibold tabular-nums'
|
className='absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center px-1 text-xs font-semibold tabular-nums'
|
||||||
>
|
>
|
||||||
{unreadCount > 99 ? '99+' : unreadCount}
|
{unreadCount > 99 ? '99+' : unreadCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
+12
-76
@@ -20,16 +20,7 @@ import { Outlet, useRouterState } from '@tanstack/react-router'
|
|||||||
import { motion, useReducedMotion, type Variants } from 'motion/react'
|
import { motion, useReducedMotion, type Variants } from 'motion/react'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
import {
|
import { MOTION_TRANSITION, MOTION_VARIANTS } from '@/lib/motion'
|
||||||
CARD_ITEM_VARIANTS,
|
|
||||||
CARD_STAGGER_VARIANTS,
|
|
||||||
MOTION_TRANSITION,
|
|
||||||
MOTION_VARIANTS,
|
|
||||||
STAGGER_ITEM_VARIANTS,
|
|
||||||
STAGGER_VARIANTS,
|
|
||||||
TABLE_ROW_VARIANTS,
|
|
||||||
TABLE_STAGGER_VARIANTS,
|
|
||||||
} from '@/lib/motion'
|
|
||||||
|
|
||||||
interface PageTransitionProps {
|
interface PageTransitionProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -92,23 +83,13 @@ interface StaggerContainerProps {
|
|||||||
variants?: Variants
|
variants?: Variants
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Entrance choreography is retired on admin surfaces: content appears
|
||||||
|
* immediately so tables and dashboards read as instant. The components stay
|
||||||
|
* as plain wrappers so call sites keep working; decorative motion remains
|
||||||
|
* available to the landing page via its dedicated landing-* utilities. */
|
||||||
|
|
||||||
export function StaggerContainer(props: StaggerContainerProps) {
|
export function StaggerContainer(props: StaggerContainerProps) {
|
||||||
const shouldReduce = useReducedMotion()
|
return <div className={props.className}>{props.children}</div>
|
||||||
|
|
||||||
if (shouldReduce) {
|
|
||||||
return <div className={props.className}>{props.children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
variants={props.variants ?? STAGGER_VARIANTS}
|
|
||||||
initial='initial'
|
|
||||||
animate='animate'
|
|
||||||
className={props.className}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StaggerItemProps {
|
interface StaggerItemProps {
|
||||||
@@ -118,68 +99,23 @@ interface StaggerItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StaggerItem(props: StaggerItemProps) {
|
export function StaggerItem(props: StaggerItemProps) {
|
||||||
return (
|
return <div className={props.className}>{props.children}</div>
|
||||||
<motion.div
|
|
||||||
variants={props.variants ?? STAGGER_ITEM_VARIANTS}
|
|
||||||
className={props.className}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableStaggerContainer(props: StaggerContainerProps) {
|
export function TableStaggerContainer(props: StaggerContainerProps) {
|
||||||
const shouldReduce = useReducedMotion()
|
return <tbody className={props.className}>{props.children}</tbody>
|
||||||
|
|
||||||
if (shouldReduce) {
|
|
||||||
return <>{props.children}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.tbody
|
|
||||||
variants={TABLE_STAGGER_VARIANTS}
|
|
||||||
initial='initial'
|
|
||||||
animate='animate'
|
|
||||||
className={props.className}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</motion.tbody>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableStaggerRow(props: StaggerItemProps) {
|
export function TableStaggerRow(props: StaggerItemProps) {
|
||||||
return (
|
return <tr className={props.className}>{props.children}</tr>
|
||||||
<motion.tr variants={TABLE_ROW_VARIANTS} className={props.className}>
|
|
||||||
{props.children}
|
|
||||||
</motion.tr>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardStaggerContainer(props: StaggerContainerProps) {
|
export function CardStaggerContainer(props: StaggerContainerProps) {
|
||||||
const shouldReduce = useReducedMotion()
|
return <div className={props.className}>{props.children}</div>
|
||||||
|
|
||||||
if (shouldReduce) {
|
|
||||||
return <div className={props.className}>{props.children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
variants={CARD_STAGGER_VARIANTS}
|
|
||||||
initial='initial'
|
|
||||||
animate='animate'
|
|
||||||
className={props.className}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardStaggerItem(props: StaggerItemProps) {
|
export function CardStaggerItem(props: StaggerItemProps) {
|
||||||
return (
|
return <div className={props.className}>{props.children}</div>
|
||||||
<motion.div variants={CARD_ITEM_VARIANTS} className={props.className}>
|
|
||||||
{props.children}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FadeInProps {
|
interface FadeInProps {
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ export function ProfileDropdown() {
|
|||||||
>
|
>
|
||||||
<Avatar className='size-6'>
|
<Avatar className='size-6'>
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
className={`${avatarFallbackClassName} text-[11px]`}
|
className={`${avatarFallbackClassName} text-xs`}
|
||||||
style={avatarFallbackStyle}
|
style={avatarFallbackStyle}
|
||||||
>
|
>
|
||||||
{avatarFallback}
|
{avatarFallback}
|
||||||
|
|||||||
+1
-4
@@ -16,10 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import {
|
import { HtmlContent, type HtmlContentVariant } from '@/components/html-content'
|
||||||
HtmlContent,
|
|
||||||
type HtmlContentVariant,
|
|
||||||
} from '@/components/html-content'
|
|
||||||
import { Markdown } from '@/components/ui/markdown'
|
import { Markdown } from '@/components/ui/markdown'
|
||||||
|
|
||||||
type RichContentMode = 'markdown' | 'html'
|
type RichContentMode = 'markdown' | 'html'
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ export function Search({ className = '', placeholder }: SearchProps) {
|
|||||||
size={16}
|
size={16}
|
||||||
/>
|
/>
|
||||||
<span className='ms-4'>{resolvedPlaceholder}</span>
|
<span className='ms-4'>{resolvedPlaceholder}</span>
|
||||||
<kbd className='bg-muted group-hover:bg-accent pointer-events-none absolute end-[0.3rem] top-[0.3rem] hidden h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none sm:flex'>
|
<kbd className='bg-muted group-hover:bg-accent pointer-events-none absolute end-[0.3rem] top-[0.3rem] hidden h-5 items-center gap-1 rounded border px-1.5 font-mono text-xs font-medium opacity-100 select-none sm:flex'>
|
||||||
<span className='text-xs'>⌘</span>
|
<span className='text-xs'>⌘</span>
|
||||||
{t('K')}
|
{t('K')}
|
||||||
</kbd>
|
</kbd>
|
||||||
|
|||||||
+42
-23
@@ -21,30 +21,34 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { stringToColor } from '@/lib/colors'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
/* Color discipline: badges speak exactly five voices — success / warning /
|
||||||
|
* danger / info / neutral. Legacy hue keys (blue, purple, teal, ...) remain
|
||||||
|
* valid variant names for compatibility, but they all resolve to one of the
|
||||||
|
* five semantic tokens so tables stop reading as rainbows. Chart hues stay
|
||||||
|
* reserved for data visualization. */
|
||||||
export const dotColorMap = {
|
export const dotColorMap = {
|
||||||
success: 'bg-success',
|
success: 'bg-success',
|
||||||
warning: 'bg-warning',
|
warning: 'bg-warning',
|
||||||
danger: 'bg-destructive',
|
danger: 'bg-destructive',
|
||||||
info: 'bg-info',
|
info: 'bg-info',
|
||||||
neutral: 'bg-neutral',
|
neutral: 'bg-neutral',
|
||||||
purple: 'bg-chart-4',
|
purple: 'bg-neutral',
|
||||||
amber: 'bg-warning',
|
amber: 'bg-warning',
|
||||||
blue: 'bg-chart-1',
|
blue: 'bg-neutral',
|
||||||
cyan: 'bg-chart-2',
|
cyan: 'bg-neutral',
|
||||||
green: 'bg-success',
|
green: 'bg-success',
|
||||||
grey: 'bg-neutral',
|
grey: 'bg-neutral',
|
||||||
indigo: 'bg-chart-1',
|
indigo: 'bg-neutral',
|
||||||
'light-blue': 'bg-info',
|
'light-blue': 'bg-info',
|
||||||
'light-green': 'bg-emerald-400',
|
'light-green': 'bg-success',
|
||||||
lime: 'bg-chart-3',
|
lime: 'bg-neutral',
|
||||||
orange: 'bg-warning',
|
orange: 'bg-warning',
|
||||||
pink: 'bg-chart-5',
|
pink: 'bg-neutral',
|
||||||
red: 'bg-destructive',
|
red: 'bg-destructive',
|
||||||
teal: 'bg-chart-2',
|
teal: 'bg-neutral',
|
||||||
violet: 'bg-chart-4',
|
violet: 'bg-neutral',
|
||||||
yellow: 'bg-warning',
|
yellow: 'bg-warning',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -54,26 +58,38 @@ export const textColorMap = {
|
|||||||
danger: 'text-destructive',
|
danger: 'text-destructive',
|
||||||
info: 'text-info',
|
info: 'text-info',
|
||||||
neutral: 'text-muted-foreground',
|
neutral: 'text-muted-foreground',
|
||||||
purple: 'text-chart-4',
|
purple: 'text-muted-foreground',
|
||||||
amber: 'text-warning',
|
amber: 'text-warning',
|
||||||
blue: 'text-chart-1',
|
blue: 'text-muted-foreground',
|
||||||
cyan: 'text-chart-2',
|
cyan: 'text-muted-foreground',
|
||||||
green: 'text-success',
|
green: 'text-success',
|
||||||
grey: 'text-muted-foreground',
|
grey: 'text-muted-foreground',
|
||||||
indigo: 'text-chart-1',
|
indigo: 'text-muted-foreground',
|
||||||
'light-blue': 'text-info',
|
'light-blue': 'text-info',
|
||||||
'light-green': 'text-emerald-500 dark:text-emerald-300',
|
'light-green': 'text-success',
|
||||||
lime: 'text-chart-3',
|
lime: 'text-muted-foreground',
|
||||||
orange: 'text-warning',
|
orange: 'text-warning',
|
||||||
pink: 'text-chart-5',
|
pink: 'text-muted-foreground',
|
||||||
red: 'text-destructive',
|
red: 'text-destructive',
|
||||||
teal: 'text-chart-2',
|
teal: 'text-muted-foreground',
|
||||||
violet: 'text-chart-4',
|
violet: 'text-muted-foreground',
|
||||||
yellow: 'text-warning',
|
yellow: 'text-warning',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type StatusVariant = keyof typeof dotColorMap
|
export type StatusVariant = keyof typeof dotColorMap
|
||||||
|
|
||||||
|
/** Soft tinted pill surface per semantic voice — the one sanctioned recipe
|
||||||
|
* for badges that need a filled background (timing pills, duration pills).
|
||||||
|
* Border/background/text always come from the same token so light and dark
|
||||||
|
* modes stay consistent without `!important` overrides. */
|
||||||
|
export const tintedBadgeClassMap = {
|
||||||
|
success: 'border border-success/25 bg-success/10 text-success',
|
||||||
|
warning: 'border border-warning/30 bg-warning/10 text-warning',
|
||||||
|
danger: 'border border-destructive/25 bg-destructive/10 text-destructive',
|
||||||
|
info: 'border border-info/25 bg-info/10 text-info',
|
||||||
|
neutral: 'border border-border/60 bg-muted/30 text-muted-foreground',
|
||||||
|
} as const
|
||||||
|
|
||||||
/** Controls the visual style of the badge.
|
/** Controls the visual style of the badge.
|
||||||
* - `badge` — default pill with background and padding (default)
|
* - `badge` — default pill with background and padding (default)
|
||||||
* - `text` — plain text, no background or padding, only color
|
* - `text` — plain text, no background or padding, only color
|
||||||
@@ -112,6 +128,8 @@ export interface StatusBadgeProps extends Omit<
|
|||||||
size?: 'sm' | 'md' | 'lg' | null
|
size?: 'sm' | 'md' | 'lg' | null
|
||||||
copyable?: boolean
|
copyable?: boolean
|
||||||
copyText?: string
|
copyText?: string
|
||||||
|
/** Deprecated: categorical identity is conveyed by label/icon, not hue.
|
||||||
|
* Accepted for call-site compatibility; renders as `neutral`. */
|
||||||
autoColor?: string
|
autoColor?: string
|
||||||
/** Visual style. Defaults to 'badge'. Can be overridden via StatusBadgeTypeContext. */
|
/** Visual style. Defaults to 'badge'. Can be overridden via StatusBadgeTypeContext. */
|
||||||
type?: StatusBadgeType
|
type?: StatusBadgeType
|
||||||
@@ -137,9 +155,10 @@ export function StatusBadge({
|
|||||||
const contextType = React.useContext(StatusBadgeTypeContext)
|
const contextType = React.useContext(StatusBadgeTypeContext)
|
||||||
const type = typeProp ?? contextType
|
const type = typeProp ?? contextType
|
||||||
|
|
||||||
const computedVariant: StatusVariant = autoColor
|
// String-hash rainbow coloring is retired; `autoColor` intentionally falls
|
||||||
? (stringToColor(autoColor) as StatusVariant)
|
// through to the neutral voice (see prop doc above).
|
||||||
: (variant ?? 'neutral')
|
void autoColor
|
||||||
|
const computedVariant: StatusVariant = variant ?? 'neutral'
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLSpanElement>) => {
|
const handleClick = (e: React.MouseEvent<HTMLSpanElement>) => {
|
||||||
if (copyable) {
|
if (copyable) {
|
||||||
@@ -166,7 +185,7 @@ export function StatusBadge({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex w-fit max-w-full min-w-0 shrink items-center font-medium tracking-normal whitespace-nowrap transition-colors',
|
'inline-flex w-fit max-w-full min-w-0 shrink items-center font-medium tracking-normal whitespace-nowrap transition-colors',
|
||||||
isBadge
|
isBadge
|
||||||
? cn('rounded-4xl', sizeMap[size ?? 'sm'])
|
? cn('rounded-md', sizeMap[size ?? 'sm'])
|
||||||
: cn(
|
: cn(
|
||||||
textSizeMap[size ?? 'sm'],
|
textSizeMap[size ?? 'sm'],
|
||||||
type === 'underline' && 'border-b border-current pb-px'
|
type === 'underline' && 'border-b border-current pb-px'
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export function TableId(props: TableIdProps) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground inline-block font-mono tabular-nums',
|
'text-muted-foreground inline-block tabular-nums',
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
+1
-1
@@ -269,7 +269,7 @@ function ChartTooltipContent({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{item.value != null && (
|
{item.value != null && (
|
||||||
<span className='text-foreground font-mono font-medium tabular-nums'>
|
<span className='text-foreground font-medium tabular-nums'>
|
||||||
{typeof item.value === 'number'
|
{typeof item.value === 'number'
|
||||||
? item.value.toLocaleString()
|
? item.value.toLocaleString()
|
||||||
: String(item.value)}
|
: String(item.value)}
|
||||||
|
|||||||
+4
-4
@@ -63,7 +63,7 @@ export function TitledCard({
|
|||||||
className={cn('gap-0 overflow-hidden py-0', className)}
|
className={cn('gap-0 overflow-hidden py-0', className)}
|
||||||
>
|
>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className={cn('border-b p-3 !pb-3 sm:p-5 sm:!pb-5', headerClassName)}
|
className={cn('border-b p-4 !pb-4 sm:p-5 sm:!pb-5', headerClassName)}
|
||||||
>
|
>
|
||||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
|
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
|
||||||
<div className='flex min-w-0 items-center gap-3'>
|
<div className='flex min-w-0 items-center gap-3'>
|
||||||
@@ -80,7 +80,7 @@ export function TitledCard({
|
|||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<CardTitle
|
<CardTitle
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-lg tracking-tight sm:text-xl',
|
'text-base font-semibold tracking-tight',
|
||||||
titleClassName
|
titleClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -88,7 +88,7 @@ export function TitledCard({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
{description != null && (
|
{description != null && (
|
||||||
<CardDescription
|
<CardDescription
|
||||||
className={cn('text-xs sm:text-sm', descriptionClassName)}
|
className={cn('text-sm', descriptionClassName)}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -100,7 +100,7 @@ export function TitledCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className={cn('p-3 sm:p-5', contentClassName)}>
|
<CardContent className={cn('p-4 sm:p-5', contentClassName)}>
|
||||||
{children}
|
{children}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Vendored
-37
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* List of available font names (visit the url `/settings/appearance`).
|
|
||||||
* This array is used to generate dynamic font classes (e.g., `font-inter`, `font-manrope`).
|
|
||||||
*
|
|
||||||
* 📝 How to Add a New Font (Tailwind v4+):
|
|
||||||
* 1. Add the font name here.
|
|
||||||
* 2. Update the `<link>` tag in 'index.html' to include the new font from CDN or other source.
|
|
||||||
* 3. Add the new font family to 'theme.css' using the `@theme inline` and `font-family` CSS variable.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* fonts.ts → Add 'roboto' to this array.
|
|
||||||
* index.html → Add font link for Roboto.
|
|
||||||
* theme.css → Add the new font in the CSS, e.g.:
|
|
||||||
* @theme inline {
|
|
||||||
* // ... other font families
|
|
||||||
* --font-roboto: 'Roboto', var(--font-sans);
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
export const fonts = ['inter', 'manrope', 'system'] as const
|
|
||||||
-77
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (C) 2023-2026 QuantumNous
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
|
||||||
*/
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import { fonts } from '@/config/fonts'
|
|
||||||
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
|
|
||||||
|
|
||||||
type Font = (typeof fonts)[number]
|
|
||||||
|
|
||||||
const FONT_COOKIE_NAME = 'font'
|
|
||||||
const FONT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
|
|
||||||
|
|
||||||
type FontContextType = {
|
|
||||||
font: Font
|
|
||||||
setFont: (font: Font) => void
|
|
||||||
resetFont: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const FontContext = createContext<FontContextType | null>(null)
|
|
||||||
|
|
||||||
export function FontProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [font, _setFont] = useState<Font>(() => {
|
|
||||||
const savedFont = getCookie(FONT_COOKIE_NAME)
|
|
||||||
return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const applyFont = (font: string) => {
|
|
||||||
const root = document.documentElement
|
|
||||||
root.classList.forEach((cls) => {
|
|
||||||
if (cls.startsWith('font-')) root.classList.remove(cls)
|
|
||||||
})
|
|
||||||
root.classList.add(`font-${font}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFont(font)
|
|
||||||
}, [font])
|
|
||||||
|
|
||||||
const setFont = (font: Font) => {
|
|
||||||
setCookie(FONT_COOKIE_NAME, font, FONT_COOKIE_MAX_AGE)
|
|
||||||
_setFont(font)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetFont = () => {
|
|
||||||
removeCookie(FONT_COOKIE_NAME)
|
|
||||||
_setFont(fonts[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FontContext value={{ font, setFont, resetFont }}>{children}</FontContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
|
||||||
export const useFont = () => {
|
|
||||||
const context = useContext(FontContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useFont must be used within a FontProvider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
@@ -78,7 +78,7 @@ function ChannelCardComponent({
|
|||||||
const responseCell = renderCell('response_time')
|
const responseCell = renderCell('response_time')
|
||||||
const testCell = renderCell('test_time')
|
const testCell = renderCell('test_time')
|
||||||
|
|
||||||
const labelClass = 'text-muted-foreground text-[11px] font-medium select-none'
|
const labelClass = 'text-muted-foreground text-xs font-medium select-none'
|
||||||
|
|
||||||
// In card view the enable/disable state is already conveyed by the inline
|
// In card view the enable/disable state is already conveyed by the inline
|
||||||
// power toggle, so the plain "Enabled"/"Disabled" badge is redundant. Keep
|
// power toggle, so the plain "Enabled"/"Disabled" badge is redundant. Keep
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ export function useChannelsColumns(
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={
|
||||||
<AlertTriangle className='h-3.5 w-3.5 flex-shrink-0 text-amber-500' />
|
<AlertTriangle className='text-warning h-3.5 w-3.5 flex-shrink-0' />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TooltipContent side='top'>
|
<TooltipContent side='top'>
|
||||||
|
|||||||
+15
-34
@@ -30,7 +30,11 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { Dialog } from '@/components/dialog'
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
import {
|
||||||
|
StatusBadge,
|
||||||
|
textColorMap,
|
||||||
|
type StatusBadgeProps,
|
||||||
|
} from '@/components/status-badge'
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
@@ -400,32 +404,11 @@ function formatLabelValue(label: string, value: string) {
|
|||||||
return label.endsWith(':') ? `${label}${value}` : `${label} ${value}`
|
return label.endsWith(':') ? `${label}${value}` : `${label} ${value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single source of truth for variant → text color lives in status-badge.
|
||||||
const percentTextClassName: Record<
|
const percentTextClassName: Record<
|
||||||
NonNullable<StatusBadgeProps['variant']>,
|
NonNullable<StatusBadgeProps['variant']>,
|
||||||
string
|
string
|
||||||
> = {
|
> = textColorMap
|
||||||
success: 'text-success',
|
|
||||||
warning: 'text-warning',
|
|
||||||
danger: 'text-destructive',
|
|
||||||
info: 'text-info',
|
|
||||||
neutral: 'text-muted-foreground',
|
|
||||||
purple: 'text-chart-4',
|
|
||||||
amber: 'text-warning',
|
|
||||||
blue: 'text-chart-1',
|
|
||||||
cyan: 'text-chart-2',
|
|
||||||
green: 'text-success',
|
|
||||||
grey: 'text-muted-foreground',
|
|
||||||
indigo: 'text-chart-1',
|
|
||||||
'light-blue': 'text-info',
|
|
||||||
'light-green': 'text-emerald-500 dark:text-emerald-300',
|
|
||||||
lime: 'text-chart-3',
|
|
||||||
orange: 'text-warning',
|
|
||||||
pink: 'text-chart-5',
|
|
||||||
red: 'text-destructive',
|
|
||||||
teal: 'text-chart-2',
|
|
||||||
violet: 'text-chart-4',
|
|
||||||
yellow: 'text-warning',
|
|
||||||
}
|
|
||||||
|
|
||||||
type RateLimitWindowProps = {
|
type RateLimitWindowProps = {
|
||||||
title: string
|
title: string
|
||||||
@@ -464,7 +447,7 @@ function RateLimitWindow(props: RateLimitWindowProps) {
|
|||||||
>
|
>
|
||||||
{hasData ? `${percent}%` : '-'}
|
{hasData ? `${percent}%` : '-'}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground mt-1 text-[11px]'>
|
<div className='text-muted-foreground mt-1 text-xs'>
|
||||||
{t('Used')}
|
{t('Used')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -482,7 +465,7 @@ function RateLimitWindow(props: RateLimitWindowProps) {
|
|||||||
)}
|
)}
|
||||||
<div className='mt-3 grid grid-cols-1 gap-2 text-xs sm:grid-cols-2'>
|
<div className='mt-3 grid grid-cols-1 gap-2 text-xs sm:grid-cols-2'>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<div className='text-muted-foreground text-[11px]'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{t('Reset at:')}
|
{t('Reset at:')}
|
||||||
</div>
|
</div>
|
||||||
<div className='break-all tabular-nums'>
|
<div className='break-all tabular-nums'>
|
||||||
@@ -490,7 +473,7 @@ function RateLimitWindow(props: RateLimitWindowProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='min-w-0 sm:text-right'>
|
<div className='min-w-0 sm:text-right'>
|
||||||
<div className='text-muted-foreground text-[11px]'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{t('Resets in:')}
|
{t('Resets in:')}
|
||||||
</div>
|
</div>
|
||||||
<div className='tabular-nums'>
|
<div className='tabular-nums'>
|
||||||
@@ -565,9 +548,7 @@ function RateLimitGroupSection(props: RateLimitGroupSectionProps) {
|
|||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
{props.meteredFeature ? (
|
{props.meteredFeature ? (
|
||||||
<div className='bg-background ring-border/60 inline-flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 rounded-lg px-2 py-1 text-xs ring-1'>
|
<div className='bg-background ring-border/60 inline-flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 rounded-lg px-2 py-1 text-xs ring-1'>
|
||||||
<span className='text-muted-foreground text-[11px]'>
|
<span className='text-muted-foreground text-xs'>metered_feature</span>
|
||||||
metered_feature
|
|
||||||
</span>
|
|
||||||
<span className='min-w-0 font-mono break-all'>
|
<span className='min-w-0 font-mono break-all'>
|
||||||
{props.meteredFeature}
|
{props.meteredFeature}
|
||||||
</span>
|
</span>
|
||||||
@@ -600,7 +581,7 @@ function InfoField(props: {
|
|||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='text-muted-foreground text-[11px] font-medium'>
|
<div className='text-muted-foreground text-xs font-medium'>
|
||||||
{props.label}
|
{props.label}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1 flex min-w-0 items-start justify-between gap-2'>
|
<div className='mt-1 flex min-w-0 items-start justify-between gap-2'>
|
||||||
@@ -635,7 +616,7 @@ function ResetCreditTimeField(props: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<div className='text-muted-foreground text-[11px] font-medium'>
|
<div className='text-muted-foreground text-xs font-medium'>
|
||||||
{props.label}
|
{props.label}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -679,13 +660,13 @@ function ResetCreditItem(props: { credit: CodexResetCredit; index: number }) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{props.credit.id ? (
|
{props.credit.id ? (
|
||||||
<div className='text-muted-foreground mt-1 font-mono text-[11px] break-all'>
|
<div className='text-muted-foreground mt-1 font-mono text-xs break-all'>
|
||||||
{props.credit.id}
|
{props.credit.id}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className='shrink-0 text-right'>
|
<div className='shrink-0 text-right'>
|
||||||
<div className='text-muted-foreground text-[11px] font-medium'>
|
<div className='text-muted-foreground text-xs font-medium'>
|
||||||
{t('Expires in')}
|
{t('Expires in')}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
+1
-1
@@ -344,7 +344,7 @@ export function FetchModelsDialog({
|
|||||||
{redirectOnlySet.has(normalizeModelName(model)) && (
|
{redirectOnlySet.has(normalizeModelName(model)) && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={<Info className='h-3.5 w-3.5 text-amber-500' />}
|
render={<Info className='text-warning h-3.5 w-3.5' />}
|
||||||
></TooltipTrigger>
|
></TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t('From model redirect, not yet added to models list')}
|
{t('From model redirect, not yet added to models list')}
|
||||||
|
|||||||
+25
-42
@@ -553,17 +553,11 @@ const getOperationSummary = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getModeTagTailwind = (mode: string): string => {
|
const getModeTagTailwind = (mode: string): string => {
|
||||||
if (mode.includes('header'))
|
// Only destructive-ish operations get a color voice; the rest stay neutral —
|
||||||
return 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-300 border-cyan-500/20'
|
// the mode label itself carries the identity.
|
||||||
if (mode.includes('replace') || mode.includes('trim'))
|
|
||||||
return 'bg-violet-500/15 text-violet-700 dark:text-violet-300 border-violet-500/20'
|
|
||||||
if (mode.includes('copy') || mode.includes('move'))
|
|
||||||
return 'bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/20'
|
|
||||||
if (mode.includes('error') || mode.includes('prune'))
|
if (mode.includes('error') || mode.includes('prune'))
|
||||||
return 'bg-red-500/15 text-red-700 dark:text-red-300 border-red-500/20'
|
return 'bg-destructive/10 text-destructive border-destructive/20'
|
||||||
if (mode.includes('sync'))
|
return 'bg-muted text-muted-foreground border-border/60'
|
||||||
return 'bg-green-500/15 text-green-700 dark:text-green-300 border-green-500/20'
|
|
||||||
return 'bg-muted text-muted-foreground'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getModePathLabel = (mode: string): string => {
|
const getModePathLabel = (mode: string): string => {
|
||||||
@@ -1849,7 +1843,7 @@ export function ParamOverrideEditorDialog(
|
|||||||
<span
|
<span
|
||||||
key={`mode_stat_${mode}`}
|
key={`mode_stat_${mode}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
|
'inline-flex items-center rounded-md border px-1.5 py-0.5 text-xs font-medium',
|
||||||
getModeTagTailwind(mode)
|
getModeTagTailwind(mode)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1935,24 +1929,21 @@ export function ParamOverrideEditorDialog(
|
|||||||
<span className='text-xs font-semibold'>
|
<span className='text-xs font-semibold'>
|
||||||
#{index + 1}
|
#{index + 1}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge variant='outline' className='text-xs'>
|
||||||
variant='outline'
|
|
||||||
className='text-[10px]'
|
|
||||||
>
|
|
||||||
{operation.conditions.length}
|
{operation.conditions.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground mt-0.5 line-clamp-1 text-[11px]'>
|
<p className='text-muted-foreground mt-0.5 line-clamp-1 text-xs'>
|
||||||
{getOperationSummary(operation, index)}
|
{getOperationSummary(operation, index)}
|
||||||
</p>
|
</p>
|
||||||
{operation.description.trim() && (
|
{operation.description.trim() && (
|
||||||
<p className='text-muted-foreground mt-0.5 line-clamp-2 text-[10px]'>
|
<p className='text-muted-foreground mt-0.5 line-clamp-2 text-xs'>
|
||||||
{operation.description}
|
{operation.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
|
'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-xs font-medium',
|
||||||
getModeTagTailwind(operation.mode || 'set')
|
getModeTagTailwind(operation.mode || 'set')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -2214,7 +2205,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
|
|||||||
<label className='text-xs font-medium'>
|
<label className='text-xs font-medium'>
|
||||||
{t('Rule Description (optional)')}
|
{t('Rule Description (optional)')}
|
||||||
</label>
|
</label>
|
||||||
<span className='text-muted-foreground text-[10px]'>
|
<span className='text-muted-foreground text-xs'>
|
||||||
{operation.description.length}/180
|
{operation.description.length}/180
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -2485,7 +2476,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
|
|||||||
<div className='rounded-md border'>
|
<div className='rounded-md border'>
|
||||||
<CollapsibleTrigger className='hover:bg-muted/50 flex w-full items-center justify-between px-3 py-2'>
|
<CollapsibleTrigger className='hover:bg-muted/50 flex w-full items-center justify-between px-3 py-2'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Badge variant='outline' className='text-[10px]'>
|
<Badge variant='outline' className='text-xs'>
|
||||||
C{conditionEditorProps.conditionIndex + 1}
|
C{conditionEditorProps.conditionIndex + 1}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className='text-muted-foreground text-xs'>
|
<span className='text-muted-foreground text-xs'>
|
||||||
@@ -2522,9 +2513,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className='grid gap-2 sm:grid-cols-3'>
|
<div className='grid gap-2 sm:grid-cols-3'>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<label className='text-[10px] font-medium'>
|
<label className='text-xs font-medium'>{t('Field Path')}</label>
|
||||||
{t('Field Path')}
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
value={condition.path}
|
value={condition.path}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -2539,9 +2528,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<label className='text-[10px] font-medium'>
|
<label className='text-xs font-medium'>{t('Match Mode')}</label>
|
||||||
{t('Match Mode')}
|
|
||||||
</label>
|
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={[
|
||||||
...CONDITION_MODE_OPTIONS.map((o) => ({
|
...CONDITION_MODE_OPTIONS.map((o) => ({
|
||||||
@@ -2574,7 +2561,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<label className='text-[10px] font-medium'>
|
<label className='text-xs font-medium'>
|
||||||
{t('Match Value')}
|
{t('Match Value')}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -2815,7 +2802,7 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='h-6 text-[10px]'
|
className='h-6 text-xs'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
returnErrorEditorProps.updateDraft(
|
returnErrorEditorProps.updateDraft(
|
||||||
returnErrorEditorProps.operationId,
|
returnErrorEditorProps.operationId,
|
||||||
@@ -3021,14 +3008,14 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
|||||||
className='bg-background rounded-md border p-2'
|
className='bg-background rounded-md border p-2'
|
||||||
>
|
>
|
||||||
<div className='mb-1 flex items-center justify-between'>
|
<div className='mb-1 flex items-center justify-between'>
|
||||||
<Badge variant='outline' className='text-[10px]'>
|
<Badge variant='outline' className='text-xs'>
|
||||||
R{ruleIndex + 1}
|
R{ruleIndex + 1}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='text-destructive hover:text-destructive h-6 text-[10px]'
|
className='text-destructive hover:text-destructive h-6 text-xs'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
pruneObjectsEditorProps.removeRule(
|
pruneObjectsEditorProps.removeRule(
|
||||||
pruneObjectsEditorProps.operationId,
|
pruneObjectsEditorProps.operationId,
|
||||||
@@ -3042,7 +3029,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className='grid gap-2 sm:grid-cols-3'>
|
<div className='grid gap-2 sm:grid-cols-3'>
|
||||||
<div className='space-y-0.5'>
|
<div className='space-y-0.5'>
|
||||||
<label className='text-[10px] font-medium'>
|
<label className='text-xs font-medium'>
|
||||||
{t('Field Path')}
|
{t('Field Path')}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -3059,7 +3046,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-0.5'>
|
<div className='space-y-0.5'>
|
||||||
<label className='text-[10px] font-medium'>
|
<label className='text-xs font-medium'>
|
||||||
{t('Match Mode')}
|
{t('Match Mode')}
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
@@ -3094,7 +3081,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-0.5'>
|
<div className='space-y-0.5'>
|
||||||
<label className='text-[10px] font-medium'>
|
<label className='text-xs font-medium'>
|
||||||
{t('Match Value (optional)')}
|
{t('Match Value (optional)')}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -3112,7 +3099,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1.5 flex flex-wrap gap-3'>
|
<div className='mt-1.5 flex flex-wrap gap-3'>
|
||||||
<label className='flex items-center gap-1.5 text-[10px]'>
|
<label className='flex items-center gap-1.5 text-xs'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={rule.invert}
|
checked={rule.invert}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
@@ -3125,7 +3112,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
|
|||||||
/>
|
/>
|
||||||
{t('Invert match')}
|
{t('Invert match')}
|
||||||
</label>
|
</label>
|
||||||
<label className='flex items-center gap-1.5 text-[10px]'>
|
<label className='flex items-center gap-1.5 text-xs'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={rule.pass_missing_key}
|
checked={rule.pass_missing_key}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
@@ -3171,9 +3158,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
|
|||||||
<label className='text-xs font-medium'>{t('Sync Endpoints')}</label>
|
<label className='text-xs font-medium'>{t('Sync Endpoints')}</label>
|
||||||
<div className='grid gap-3 sm:grid-cols-2'>
|
<div className='grid gap-3 sm:grid-cols-2'>
|
||||||
<div className='space-y-1.5'>
|
<div className='space-y-1.5'>
|
||||||
<label className='text-[10px] font-medium'>
|
<label className='text-xs font-medium'>{t('Source Endpoint')}</label>
|
||||||
{t('Source Endpoint')}
|
|
||||||
</label>
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={[
|
||||||
@@ -3228,9 +3213,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-1.5'>
|
<div className='space-y-1.5'>
|
||||||
<label className='text-[10px] font-medium'>
|
<label className='text-xs font-medium'>{t('Target Endpoint')}</label>
|
||||||
{t('Target Endpoint')}
|
|
||||||
</label>
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={[
|
||||||
@@ -3303,7 +3286,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='h-6 text-[10px]'
|
className='h-6 text-xs'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
syncFieldsEditorProps.updateOperation(
|
syncFieldsEditorProps.updateOperation(
|
||||||
syncFieldsEditorProps.operationId,
|
syncFieldsEditorProps.operationId,
|
||||||
|
|||||||
+8
-8
@@ -1859,7 +1859,7 @@ export function ChannelMutateDrawer({
|
|||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
{sensitiveLocked && (
|
{sensitiveLocked && (
|
||||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
<Alert className='border-warning/40 bg-warning/10'>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t(
|
{t(
|
||||||
'Sensitive channel settings are read-only for your account.'
|
'Sensitive channel settings are read-only for your account.'
|
||||||
@@ -2077,7 +2077,7 @@ export function ChannelMutateDrawer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{sensitiveLocked && (
|
{sensitiveLocked && (
|
||||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
<Alert className='border-warning/40 bg-warning/10'>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t('No permission to perform this action')}
|
{t('No permission to perform this action')}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@@ -3068,7 +3068,7 @@ export function ChannelMutateDrawer({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
<Alert className='border-warning/40 bg-warning/10'>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t(
|
{t(
|
||||||
"Disclaimer: Personal use only. Do not distribute or share any credentials. This channel has prerequisites and requires prior setup; use it only if you understand the flow and risks, and comply with OpenAI's terms and policies. Credentials and configuration are for Codex CLI integration only, and are not intended for any other client, platform, or channel."
|
"Disclaimer: Personal use only. Do not distribute or share any credentials. This channel has prerequisites and requires prior setup; use it only if you understand the flow and risks, and comply with OpenAI's terms and policies. Credentials and configuration are for Codex CLI integration only, and are not intended for any other client, platform, or channel."
|
||||||
@@ -3244,7 +3244,7 @@ export function ChannelMutateDrawer({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
{modelMappingGuardrail.exposedTargetModels
|
{modelMappingGuardrail.exposedTargetModels
|
||||||
.length > 0 && (
|
.length > 0 && (
|
||||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
<Alert className='border-warning/40 bg-warning/10'>
|
||||||
<AlertDescription className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
<AlertDescription className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
||||||
<span>
|
<span>
|
||||||
{t('The mapped upstream model(s)')}{' '}
|
{t('The mapped upstream model(s)')}{' '}
|
||||||
@@ -3451,7 +3451,7 @@ export function ChannelMutateDrawer({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{remainingMappingCount > 0 && (
|
{remainingMappingCount > 0 && (
|
||||||
<div className='text-[11px] opacity-70'>
|
<div className='text-xs opacity-70'>
|
||||||
+{remainingMappingCount}{' '}
|
+{remainingMappingCount}{' '}
|
||||||
{t('more mapping')}
|
{t('more mapping')}
|
||||||
{remainingMappingCount > 1
|
{remainingMappingCount > 1
|
||||||
@@ -3460,7 +3460,7 @@ export function ChannelMutateDrawer({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-[11px] leading-relaxed opacity-80'>
|
<p className='text-xs leading-relaxed opacity-80'>
|
||||||
{t(
|
{t(
|
||||||
'Users call the model on the left. The platform forwards the request to the upstream model on the right.'
|
'Users call the model on the left. The platform forwards the request to the upstream model on the right.'
|
||||||
)}
|
)}
|
||||||
@@ -3501,7 +3501,7 @@ export function ChannelMutateDrawer({
|
|||||||
)}
|
)}
|
||||||
{modelMappingGuardrail.missingSourceModels
|
{modelMappingGuardrail.missingSourceModels
|
||||||
.length > 0 && (
|
.length > 0 && (
|
||||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
<Alert className='border-warning/40 bg-warning/10'>
|
||||||
<AlertDescription className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
<AlertDescription className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
||||||
<span>
|
<span>
|
||||||
{t('Add')}{' '}
|
{t('Add')}{' '}
|
||||||
@@ -4026,7 +4026,7 @@ export function ChannelMutateDrawer({
|
|||||||
icon={<Settings className='h-4 w-4' />}
|
icon={<Settings className='h-4 w-4' />}
|
||||||
/>
|
/>
|
||||||
{sensitiveLocked && (
|
{sensitiveLocked && (
|
||||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
<Alert className='border-warning/40 bg-warning/10'>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t('No permission to perform this action')}
|
{t('No permission to perform this action')}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function NumericSpinnerInput({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
title={localValue}
|
title={localValue}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-7 min-w-8 max-w-16 cursor-text truncate px-1 text-center font-mono text-sm tabular-nums',
|
'h-7 min-w-8 max-w-16 cursor-text truncate px-1 text-center text-sm tabular-nums',
|
||||||
disabled && 'cursor-default opacity-50'
|
disabled && 'cursor-default opacity-50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function LogStatCards(props: LogStatCardsProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<>
|
<>
|
||||||
<div className='text-muted-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
|
<div className='text-muted-foreground mt-1.5 text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
|
||||||
--
|
--
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
|
<div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
|
||||||
@@ -179,7 +179,7 @@ export function LogStatCards(props: LogStatCardsProps) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className='text-foreground mt-1.5 max-w-full truncate font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'
|
className='text-foreground mt-1.5 max-w-full truncate text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'
|
||||||
title={it.fullValue}
|
title={it.fullValue}
|
||||||
>
|
>
|
||||||
{it.value}
|
{it.value}
|
||||||
|
|||||||
+4
-4
@@ -186,10 +186,10 @@ function InlineMetric(props: {
|
|||||||
className='text-muted-foreground/50 size-3 shrink-0'
|
className='text-muted-foreground/50 size-3 shrink-0'
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
/>
|
/>
|
||||||
<span className='text-muted-foreground text-[11px]'>{props.label}</span>
|
<span className='text-muted-foreground text-xs'>{props.label}</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-mono text-xs font-semibold tabular-nums',
|
'text-xs font-semibold tabular-nums',
|
||||||
props.valueClassName
|
props.valueClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -204,7 +204,7 @@ function ModelBadge(props: { model: PerfModelSummary }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='bg-muted/50 inline-flex items-center gap-1.5 rounded-full px-2.5 py-1'>
|
<span className='bg-muted/50 inline-flex items-center gap-1.5 rounded-full px-2.5 py-1'>
|
||||||
<span className='max-w-[10rem] truncate font-mono text-[11px]'>
|
<span className='max-w-[10rem] truncate font-mono text-xs'>
|
||||||
{model.model_name}
|
{model.model_name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -216,7 +216,7 @@ function ModelBadge(props: { model: PerfModelSummary }) {
|
|||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-mono text-[11px] font-semibold tabular-nums',
|
'text-xs font-semibold tabular-nums',
|
||||||
getSuccessRateTextClass(model.success_rate)
|
getSuccessRateTextClass(model.success_rate)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
+4
-21
@@ -36,7 +36,6 @@ import {
|
|||||||
Timer,
|
Timer,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { motion, useReducedMotion } from 'motion/react'
|
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -50,7 +49,6 @@ import { fetchTokenKey, getApiKeys } from '@/features/keys/api'
|
|||||||
import type { ApiKey } from '@/features/keys/types'
|
import type { ApiKey } from '@/features/keys/types'
|
||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { getUserModels } from '@/lib/api'
|
import { getUserModels } from '@/lib/api'
|
||||||
import { MOTION_TRANSITION } from '@/lib/motion'
|
|
||||||
import { ROLE } from '@/lib/roles'
|
import { ROLE } from '@/lib/roles'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useAuthStore } from '@/stores/auth-store'
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
@@ -202,7 +200,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
|
|||||||
'absolute right-3 [mask-image:linear-gradient(90deg,transparent_0%,black_30%,black_82%,transparent_100%)] text-right tracking-[0.38em] whitespace-pre',
|
'absolute right-3 [mask-image:linear-gradient(90deg,transparent_0%,black_30%,black_82%,transparent_100%)] text-right tracking-[0.38em] whitespace-pre',
|
||||||
props.compact
|
props.compact
|
||||||
? '-top-6 text-[9px] leading-4'
|
? '-top-6 text-[9px] leading-4'
|
||||||
: 'top-1 text-[11px] leading-5'
|
: 'top-1 text-xs leading-5'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{SETUP_GUIDE_CODE_PATTERN}
|
{SETUP_GUIDE_CODE_PATTERN}
|
||||||
@@ -254,7 +252,7 @@ function StartStepItem(props: {
|
|||||||
</span>
|
</span>
|
||||||
<span className='flex min-w-0 flex-col gap-0.5'>
|
<span className='flex min-w-0 flex-col gap-0.5'>
|
||||||
<span className='flex items-center gap-2 text-sm font-medium'>
|
<span className='flex items-center gap-2 text-sm font-medium'>
|
||||||
<span className='text-muted-foreground font-mono text-xs tabular-nums'>
|
<span className='text-muted-foreground text-xs tabular-nums'>
|
||||||
{props.index + 1}.
|
{props.index + 1}.
|
||||||
</span>
|
</span>
|
||||||
<span className='truncate'>{props.step.title}</span>
|
<span className='truncate'>{props.step.title}</span>
|
||||||
@@ -278,7 +276,6 @@ function RequestPreview(props: {
|
|||||||
signals: HeroSignal[]
|
signals: HeroSignal[]
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const shouldReduceMotion = useReducedMotion()
|
|
||||||
const [isCopying, setIsCopying] = useState(false)
|
const [isCopying, setIsCopying] = useState(false)
|
||||||
const { copyToClipboard } = useCopyToClipboard({ notify: false })
|
const { copyToClipboard } = useCopyToClipboard({ notify: false })
|
||||||
const previewCurl = buildCurlCommand({
|
const previewCurl = buildCurlCommand({
|
||||||
@@ -316,21 +313,7 @@ function RequestPreview(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div className='bg-background relative overflow-hidden rounded-2xl border p-3'>
|
||||||
initial={shouldReduceMotion ? false : { opacity: 0, y: 10, scale: 0.98 }}
|
|
||||||
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0, scale: 1 }}
|
|
||||||
transition={MOTION_TRANSITION.slow}
|
|
||||||
className='bg-background/75 relative overflow-hidden rounded-2xl border p-3 shadow-sm backdrop-blur'
|
|
||||||
>
|
|
||||||
{!shouldReduceMotion && (
|
|
||||||
<motion.div
|
|
||||||
className='via-foreground/30 pointer-events-none absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent to-transparent'
|
|
||||||
animate={{ x: ['-100%', '100%'] }}
|
|
||||||
transition={{ duration: 3.2, repeat: Infinity, ease: 'easeInOut' }}
|
|
||||||
aria-hidden='true'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between gap-3 border-b pb-3'>
|
<div className='flex items-center justify-between gap-3 border-b pb-3'>
|
||||||
<div className='flex min-w-0 items-center gap-2'>
|
<div className='flex min-w-0 items-center gap-2'>
|
||||||
<span className='bg-muted flex size-8 shrink-0 items-center justify-center rounded-lg'>
|
<span className='bg-muted flex size-8 shrink-0 items-center justify-center rounded-lg'>
|
||||||
@@ -410,7 +393,7 @@ function RequestPreview(props: {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -135,7 +135,7 @@ export function PerformanceHealthPanel() {
|
|||||||
) : (
|
) : (
|
||||||
hasData && (
|
hasData && (
|
||||||
<div>
|
<div>
|
||||||
<span className='text-muted-foreground mb-1 block text-[11px] font-medium'>
|
<span className='text-muted-foreground mb-1 block text-xs font-medium'>
|
||||||
{t('Top models by traffic')}
|
{t('Top models by traffic')}
|
||||||
</span>
|
</span>
|
||||||
<div className='grid grid-cols-1 gap-x-4 sm:grid-cols-2'>
|
<div className='grid grid-cols-1 gap-x-4 sm:grid-cols-2'>
|
||||||
@@ -144,7 +144,7 @@ export function PerformanceHealthPanel() {
|
|||||||
key={model.model_name}
|
key={model.model_name}
|
||||||
className='flex items-center justify-between gap-2 rounded px-1.5 py-1'
|
className='flex items-center justify-between gap-2 rounded px-1.5 py-1'
|
||||||
>
|
>
|
||||||
<span className='min-w-0 flex-1 truncate font-mono text-[11px]'>
|
<span className='min-w-0 flex-1 truncate font-mono text-xs'>
|
||||||
{model.model_name}
|
{model.model_name}
|
||||||
</span>
|
</span>
|
||||||
<span className='inline-flex shrink-0 items-center gap-1'>
|
<span className='inline-flex shrink-0 items-center gap-1'>
|
||||||
@@ -157,7 +157,7 @@ export function PerformanceHealthPanel() {
|
|||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-mono text-[11px] font-semibold tabular-nums',
|
'text-xs font-semibold tabular-nums',
|
||||||
getSuccessRateTextClass(model.success_rate)
|
getSuccessRateTextClass(model.success_rate)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -185,7 +185,7 @@ function MetricCell(props: {
|
|||||||
const Icon = props.icon
|
const Icon = props.icon
|
||||||
return (
|
return (
|
||||||
<div className='bg-muted/40 rounded-xl px-3 py-2.5'>
|
<div className='bg-muted/40 rounded-xl px-3 py-2.5'>
|
||||||
<div className='text-muted-foreground flex items-center gap-1.5 text-[11px] font-medium'>
|
<div className='text-muted-foreground flex items-center gap-1.5 text-xs font-medium'>
|
||||||
<Icon className='size-3 shrink-0' aria-hidden='true' />
|
<Icon className='size-3 shrink-0' aria-hidden='true' />
|
||||||
<span className='truncate'>{props.label}</span>
|
<span className='truncate'>{props.label}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,7 +194,7 @@ function MetricCell(props: {
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-1.5 font-mono text-sm font-semibold tabular-nums',
|
'mt-1.5 text-sm font-semibold tabular-nums',
|
||||||
props.valueClassName
|
props.valueClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ export function SummaryCards() {
|
|||||||
className={cn('size-1.5 rounded-full', healthCfg.dotClass)}
|
className={cn('size-1.5 rounded-full', healthCfg.dotClass)}
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
/>
|
/>
|
||||||
<span className='text-muted-foreground text-[11px] font-medium'>
|
<span className='text-muted-foreground text-xs font-medium'>
|
||||||
{t(healthCfg.labelKey)}
|
{t(healthCfg.labelKey)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -293,7 +293,7 @@ export function SummaryCards() {
|
|||||||
|
|
||||||
<div className='grid grid-cols-2 gap-2'>
|
<div className='grid grid-cols-2 gap-2'>
|
||||||
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
|
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
|
||||||
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
|
<div className='text-muted-foreground flex items-center gap-1 text-xs leading-none font-medium'>
|
||||||
<Flame className='size-3 shrink-0' aria-hidden='true' />
|
<Flame className='size-3 shrink-0' aria-hidden='true' />
|
||||||
<span className='truncate'>{t('Last 24h usage')}</span>
|
<span className='truncate'>{t('Last 24h usage')}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,7 +302,7 @@ export function SummaryCards() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
|
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
|
||||||
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
|
<div className='text-muted-foreground flex items-center gap-1 text-xs leading-none font-medium'>
|
||||||
{runwayDays !== null && runwayDays < 3 ? (
|
{runwayDays !== null && runwayDays < 3 ? (
|
||||||
<TrendingDown
|
<TrendingDown
|
||||||
className='size-3 shrink-0'
|
className='size-3 shrink-0'
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ import { cn } from '@/lib/utils'
|
|||||||
import { PanelWrapper } from '../ui/panel-wrapper'
|
import { PanelWrapper } from '../ui/panel-wrapper'
|
||||||
|
|
||||||
const STATUS_COLOR_MAP: Record<number, string> = {
|
const STATUS_COLOR_MAP: Record<number, string> = {
|
||||||
1: 'bg-emerald-500',
|
1: 'bg-success',
|
||||||
0: 'bg-red-500',
|
0: 'bg-destructive',
|
||||||
2: 'bg-amber-500',
|
2: 'bg-warning',
|
||||||
3: 'bg-blue-500',
|
3: 'bg-info',
|
||||||
}
|
}
|
||||||
const DEFAULT_STATUS_COLOR = 'bg-muted-foreground/40'
|
const DEFAULT_STATUS_COLOR = 'bg-muted-foreground/40'
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ export function UptimePanel() {
|
|||||||
<h4 className='text-muted-foreground text-xs font-semibold tracking-wider uppercase'>
|
<h4 className='text-muted-foreground text-xs font-semibold tracking-wider uppercase'>
|
||||||
{group.categoryName}
|
{group.categoryName}
|
||||||
</h4>
|
</h4>
|
||||||
<span className='text-muted-foreground/40 font-mono text-xs tabular-nums'>
|
<span className='text-muted-foreground/40 text-xs tabular-nums'>
|
||||||
{group.monitors?.length || 0}
|
{group.monitors?.length || 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +159,7 @@ export function UptimePanel() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className='text-foreground shrink-0 font-mono text-sm font-semibold tabular-nums'>
|
<span className='text-foreground shrink-0 text-sm font-semibold tabular-nums'>
|
||||||
{((monitor.uptime ?? 0) * 100).toFixed(2)}%
|
{((monitor.uptime ?? 0) * 100).toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ function PanelHeader(props: {
|
|||||||
}) {
|
}) {
|
||||||
const heading = (
|
const heading = (
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='text-sm font-semibold'>{props.title}</div>
|
<div className='text-base font-semibold tracking-tight'>
|
||||||
|
{props.title}
|
||||||
|
</div>
|
||||||
{props.description != null && (
|
{props.description != null && (
|
||||||
<div className='text-muted-foreground text-xs'>{props.description}</div>
|
<div className='text-muted-foreground text-xs'>{props.description}</div>
|
||||||
)}
|
)}
|
||||||
@@ -68,7 +70,7 @@ export function PanelWrapper(props: PanelWrapperProps) {
|
|||||||
const resolvedEmptyMessage = props.emptyMessage ?? t('No data available')
|
const resolvedEmptyMessage = props.emptyMessage ?? t('No data available')
|
||||||
const height = props.height ?? 'h-64'
|
const height = props.height ?? 'h-64'
|
||||||
const frameClassName = cn(
|
const frameClassName = cn(
|
||||||
'overflow-hidden rounded-2xl border bg-card shadow-xs',
|
'overflow-hidden rounded-xl border bg-card',
|
||||||
props.className
|
props.className
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -51,15 +51,17 @@ interface StatCardProps {
|
|||||||
action?: ReactNode
|
action?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sparklines are data visualization, so they draw from chart tokens — solid
|
||||||
|
* fills, no decorative gradients, consistent with the dashboard charts. */
|
||||||
const TONE_CLASSES: Record<StatCardTone, string> = {
|
const TONE_CLASSES: Record<StatCardTone, string> = {
|
||||||
rose: 'from-rose-500/80 via-rose-300/70 to-rose-200/20 dark:from-rose-400/70 dark:via-rose-500/30 dark:to-rose-500/5',
|
rose: 'bg-chart-5/60',
|
||||||
teal: 'from-teal-500/80 via-teal-300/70 to-teal-200/20 dark:from-teal-400/70 dark:via-teal-500/30 dark:to-teal-500/5',
|
teal: 'bg-chart-2/60',
|
||||||
gray: 'from-muted-foreground/50 via-muted-foreground/20 to-transparent dark:from-muted-foreground/40 dark:via-muted-foreground/20',
|
gray: 'bg-muted-foreground/30',
|
||||||
}
|
}
|
||||||
|
|
||||||
const LINE_TONE_CLASSES: Record<StatCardTone, string> = {
|
const LINE_TONE_CLASSES: Record<StatCardTone, string> = {
|
||||||
rose: 'text-warning',
|
rose: 'text-chart-5',
|
||||||
teal: 'text-primary',
|
teal: 'text-chart-2',
|
||||||
gray: 'text-muted-foreground',
|
gray: 'text-muted-foreground',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +168,7 @@ function BarSparkline(props: { values?: number[]; tone: StatCardTone }) {
|
|||||||
<span
|
<span
|
||||||
key={`spark-${index}`}
|
key={`spark-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-1 rounded-t-sm bg-linear-to-t',
|
'flex-1 rounded-t-sm',
|
||||||
height <= 0 && 'opacity-20',
|
height <= 0 && 'opacity-20',
|
||||||
TONE_CLASSES[props.tone]
|
TONE_CLASSES[props.tone]
|
||||||
)}
|
)}
|
||||||
@@ -185,7 +187,7 @@ function StatCardDetails(props: { details: StatCardDetail[] }) {
|
|||||||
key={detail.label}
|
key={detail.label}
|
||||||
className='bg-muted/40 rounded-lg border border-transparent px-2.5 py-2'
|
className='bg-muted/40 rounded-lg border border-transparent px-2.5 py-2'
|
||||||
>
|
>
|
||||||
<div className='text-muted-foreground truncate text-[11px] leading-none font-medium'>
|
<div className='text-muted-foreground truncate text-xs leading-none font-medium'>
|
||||||
{detail.label}
|
{detail.label}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -228,7 +230,7 @@ export function StatCard(props: StatCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : props.error ? (
|
) : props.error ? (
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='text-muted-foreground mt-0.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:text-2xl'>
|
<div className='text-muted-foreground mt-0.5 text-2xl font-semibold tracking-tight break-all tabular-nums'>
|
||||||
--
|
--
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground/60 text-xs'>
|
<p className='text-muted-foreground/60 text-xs'>
|
||||||
@@ -237,7 +239,7 @@ export function StatCard(props: StatCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='text-foreground font-mono text-2xl font-semibold tracking-tight break-all tabular-nums'>
|
<div className='text-foreground text-2xl font-semibold tracking-tight break-all tabular-nums'>
|
||||||
{props.value}
|
{props.value}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground/60 text-xs leading-relaxed'>
|
<p className='text-muted-foreground/60 text-xs leading-relaxed'>
|
||||||
|
|||||||
@@ -62,19 +62,19 @@ function formatGroupRatio(
|
|||||||
|
|
||||||
function getRatioBadgeClassName(ratio: ApiKeyGroupOption['ratio']) {
|
function getRatioBadgeClassName(ratio: ApiKeyGroupOption['ratio']) {
|
||||||
if (typeof ratio !== 'number') {
|
if (typeof ratio !== 'number') {
|
||||||
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-300'
|
return 'border-success/25 bg-success/10 text-success'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ratio > 5) {
|
if (ratio > 5) {
|
||||||
return 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-300'
|
return 'border-destructive/25 bg-destructive/10 text-destructive'
|
||||||
}
|
}
|
||||||
if (ratio > 3) {
|
if (ratio > 3) {
|
||||||
return 'border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-900/60 dark:bg-orange-950/40 dark:text-orange-300'
|
return 'border-warning/30 bg-warning/10 text-warning'
|
||||||
}
|
}
|
||||||
if (ratio > 1) {
|
if (ratio > 1) {
|
||||||
return 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/60 dark:bg-blue-950/40 dark:text-blue-300'
|
return 'border-info/25 bg-info/10 text-info'
|
||||||
}
|
}
|
||||||
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-300'
|
return 'border-success/25 bg-success/10 text-success'
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupRatioBadge({ ratio }: { ratio: ApiKeyGroupOption['ratio'] }) {
|
function GroupRatioBadge({ ratio }: { ratio: ApiKeyGroupOption['ratio'] }) {
|
||||||
@@ -87,7 +87,7 @@ function GroupRatioBadge({ ratio }: { ratio: ApiKeyGroupOption['ratio'] }) {
|
|||||||
<Badge
|
<Badge
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className={cn(
|
className={cn(
|
||||||
'max-w-24 shrink-0 truncate text-[10px] sm:max-w-none sm:text-xs',
|
'max-w-24 shrink-0 truncate text-xs sm:max-w-none sm:text-xs',
|
||||||
getRatioBadgeClassName(ratio)
|
getRatioBadgeClassName(ratio)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -149,7 +149,7 @@ export function ApiKeyGroupCombobox({
|
|||||||
{selectedOption?.label || placeholder || t('Select a group')}
|
{selectedOption?.label || placeholder || t('Select a group')}
|
||||||
</span>
|
</span>
|
||||||
{selectedOption?.desc && (
|
{selectedOption?.desc && (
|
||||||
<span className='text-muted-foreground block truncate text-[11px] sm:text-xs'>
|
<span className='text-muted-foreground block truncate text-xs sm:text-xs'>
|
||||||
{selectedOption.desc}
|
{selectedOption.desc}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ import {
|
|||||||
import { DataTableRowActions } from './data-table-row-actions'
|
import { DataTableRowActions } from './data-table-row-actions'
|
||||||
|
|
||||||
function getQuotaProgressColor(percentage: number): string {
|
function getQuotaProgressColor(percentage: number): string {
|
||||||
if (percentage <= 10) return '[&_[data-slot=progress-indicator]]:bg-rose-500'
|
if (percentage <= 10)
|
||||||
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-amber-500'
|
return '[&_[data-slot=progress-indicator]]:bg-destructive'
|
||||||
return '[&_[data-slot=progress-indicator]]:bg-emerald-500'
|
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-warning'
|
||||||
|
return '[&_[data-slot=progress-indicator]]:bg-success'
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGroupRatios(): Record<string, number> {
|
function useGroupRatios(): Record<string, number> {
|
||||||
@@ -254,7 +255,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
|||||||
accessorKey: 'created_time',
|
accessorKey: 'created_time',
|
||||||
header: t('Created'),
|
header: t('Created'),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className='text-muted-foreground block truncate font-mono text-xs tabular-nums'>
|
<span className='text-muted-foreground block truncate text-xs tabular-nums'>
|
||||||
{formatTimestampToDate(row.getValue('created_time'))}
|
{formatTimestampToDate(row.getValue('created_time'))}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -270,7 +271,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
|||||||
return <span className='text-muted-foreground text-xs'>-</span>
|
return <span className='text-muted-foreground text-xs'>-</span>
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className='text-muted-foreground block truncate font-mono text-xs tabular-nums'>
|
<span className='text-muted-foreground block truncate text-xs tabular-nums'>
|
||||||
{formatTimestampToDate(accessedTime)}
|
{formatTimestampToDate(accessedTime)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -296,7 +297,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'block truncate font-mono text-xs tabular-nums',
|
'block truncate text-xs tabular-nums',
|
||||||
isExpired ? 'text-destructive' : 'text-muted-foreground'
|
isExpired ? 'text-destructive' : 'text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ function ApiKeysMobileList({
|
|||||||
<div className='truncate text-sm font-semibold'>
|
<div className='truncate text-sm font-semibold'>
|
||||||
{apiKey.name}
|
{apiKey.name}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-[11px]'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{t('API Key')}
|
{t('API Key')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export function DataTableRowActions<TData>({
|
|||||||
className={
|
className={
|
||||||
isEnabled
|
isEnabled
|
||||||
? 'text-destructive hover:text-destructive'
|
? 'text-destructive hover:text-destructive'
|
||||||
: 'text-emerald-600 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-400'
|
: 'text-success hover:text-success'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -125,11 +125,7 @@ export function LegalDocument({
|
|||||||
return (
|
return (
|
||||||
<PublicLayout showMainContainer={!contentIsHtml}>
|
<PublicLayout showMainContainer={!contentIsHtml}>
|
||||||
{contentIsHtml ? (
|
{contentIsHtml ? (
|
||||||
<RichContent
|
<RichContent mode='html' htmlVariant='isolated' content={rawContent} />
|
||||||
mode='html'
|
|
||||||
htmlVariant='isolated'
|
|
||||||
content={rawContent}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className='mx-auto max-w-4xl space-y-6 py-12'>
|
<div className='mx-auto max-w-4xl space-y-6 py-12'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ export function DeploymentAccessGuard({
|
|||||||
return (
|
return (
|
||||||
<div className='mx-auto mt-8 max-w-md'>
|
<div className='mx-auto mt-8 max-w-md'>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<div className='mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-amber-100 dark:bg-amber-900/20'>
|
<div className='bg-warning/10 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl'>
|
||||||
<Server className='h-8 w-8 text-amber-600 dark:text-amber-400' />
|
<Server className='text-warning h-8 w-8' />
|
||||||
</div>
|
</div>
|
||||||
<h3 className='mb-6 text-xl font-semibold'>
|
<h3 className='mb-6 text-xl font-semibold'>
|
||||||
{t('Model deployment service is disabled')}
|
{t('Model deployment service is disabled')}
|
||||||
|
|||||||
@@ -61,18 +61,18 @@ export function getSuccessRateLevel(rate: number): SuccessRateLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SUCCESS_RATE_TEXT_CLASS: Record<SuccessRateLevel, string> = {
|
const SUCCESS_RATE_TEXT_CLASS: Record<SuccessRateLevel, string> = {
|
||||||
excellent: 'text-emerald-600 dark:text-emerald-400',
|
excellent: 'text-success',
|
||||||
good: 'text-emerald-500 dark:text-emerald-300',
|
good: 'text-success/80',
|
||||||
warning: 'text-amber-600 dark:text-amber-400',
|
warning: 'text-warning',
|
||||||
critical: 'text-red-600 dark:text-red-400',
|
critical: 'text-destructive',
|
||||||
unknown: 'text-muted-foreground',
|
unknown: 'text-muted-foreground',
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUCCESS_RATE_DOT_CLASS: Record<SuccessRateLevel, string> = {
|
const SUCCESS_RATE_DOT_CLASS: Record<SuccessRateLevel, string> = {
|
||||||
excellent: 'bg-emerald-500',
|
excellent: 'bg-success',
|
||||||
good: 'bg-emerald-400',
|
good: 'bg-success/80',
|
||||||
warning: 'bg-amber-500',
|
warning: 'bg-warning',
|
||||||
critical: 'bg-red-500',
|
critical: 'bg-destructive',
|
||||||
unknown: 'bg-muted-foreground',
|
unknown: 'bg-muted-foreground',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -124,7 +124,7 @@ function PlaygroundParameterContent({
|
|||||||
{t(control.labelKey)}
|
{t(control.labelKey)}
|
||||||
</label>
|
</label>
|
||||||
<Badge
|
<Badge
|
||||||
className='h-5 max-w-24 shrink-0 px-1.5 font-mono text-[11px]'
|
className='h-5 max-w-24 shrink-0 px-1.5 font-mono text-xs'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
>
|
>
|
||||||
{t(getParameterControlValueText(control.key, value))}
|
{t(getParameterControlValueText(control.key, value))}
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ export function MessageMetadata(props: MessageMetadataProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground mt-1 flex min-h-4 items-center gap-1.5 text-[11px] leading-none',
|
'text-muted-foreground mt-1 flex min-h-4 items-center gap-1.5 text-xs leading-none',
|
||||||
props.alignment === 'right' && 'justify-end'
|
props.alignment === 'right' && 'justify-end'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export function DynamicPricingBreakdown({
|
|||||||
<section className={cn('min-w-0', !compact && 'py-4')}>
|
<section className={cn('min-w-0', !compact && 'py-4')}>
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<div className='mb-3 flex items-center gap-2'>
|
<div className='mb-3 flex items-center gap-2'>
|
||||||
<span className='inline-flex size-6 items-center justify-center rounded-lg bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
|
<span className='bg-warning/15 text-warning inline-flex size-6 items-center justify-center rounded-lg'>
|
||||||
<TagIcon className='size-3.5' />
|
<TagIcon className='size-3.5' />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -212,7 +212,7 @@ export function DynamicPricingBreakdown({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
|
||||||
{t('Raw expression')}
|
{t('Raw expression')}
|
||||||
</div>
|
</div>
|
||||||
<code className='text-muted-foreground block text-xs break-all'>
|
<code className='text-muted-foreground block text-xs break-all'>
|
||||||
@@ -234,7 +234,7 @@ export function DynamicPricingBreakdown({
|
|||||||
<section className={cn('min-w-0', !compact && 'py-3 sm:py-4')}>
|
<section className={cn('min-w-0', !compact && 'py-3 sm:py-4')}>
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<div className='mb-3 flex items-start gap-2 sm:mb-4'>
|
<div className='mb-3 flex items-start gap-2 sm:mb-4'>
|
||||||
<span className='mt-0.5 inline-flex size-6 items-center justify-center rounded-lg bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
|
<span className='bg-warning/15 text-warning mt-0.5 inline-flex size-6 items-center justify-center rounded-lg'>
|
||||||
<TagIcon className='size-3.5' />
|
<TagIcon className='size-3.5' />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -271,7 +271,7 @@ export function DynamicPricingBreakdown({
|
|||||||
key={`tier-mobile-${i}`}
|
key={`tier-mobile-${i}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md border p-2',
|
'rounded-md border p-2',
|
||||||
isMatched && 'border-emerald-500/40 bg-emerald-500/10'
|
isMatched && 'border-success/40 bg-success/10'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='mb-1.5 flex flex-wrap items-center gap-1.5'>
|
<div className='mb-1.5 flex flex-wrap items-center gap-1.5'>
|
||||||
@@ -284,7 +284,7 @@ export function DynamicPricingBreakdown({
|
|||||||
{isMatched && (
|
{isMatched && (
|
||||||
<Badge
|
<Badge
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
className='bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
|
className='bg-success/10 text-success'
|
||||||
>
|
>
|
||||||
{t('Matched')}
|
{t('Matched')}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -302,7 +302,7 @@ export function DynamicPricingBreakdown({
|
|||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div key={v.field} className='min-w-0'>
|
<div key={v.field} className='min-w-0'>
|
||||||
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
|
||||||
{t(v.shortLabel)}
|
{t(v.shortLabel)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -337,10 +337,7 @@ export function DynamicPricingBreakdown({
|
|||||||
const isMatched =
|
const isMatched =
|
||||||
normalizedMatchedTierLabel !== '' &&
|
normalizedMatchedTierLabel !== '' &&
|
||||||
normalizeTierLabel(tier.label) === normalizedMatchedTierLabel
|
normalizeTierLabel(tier.label) === normalizedMatchedTierLabel
|
||||||
return cn(
|
return cn(isMatched && 'bg-success/10 hover:bg-success/10')
|
||||||
isMatched &&
|
|
||||||
'bg-emerald-50/70 hover:bg-emerald-50/70 dark:bg-emerald-500/10 dark:hover:bg-emerald-500/10'
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
@@ -369,7 +366,7 @@ export function DynamicPricingBreakdown({
|
|||||||
{isMatched && (
|
{isMatched && (
|
||||||
<Badge
|
<Badge
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
className='bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
|
className='bg-success/10 text-success'
|
||||||
>
|
>
|
||||||
{t('Matched')}
|
{t('Matched')}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -96,10 +96,10 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
|
|||||||
if (dynamicSummary.isSpecialExpression) {
|
if (dynamicSummary.isSpecialExpression) {
|
||||||
priceSummary = (
|
priceSummary = (
|
||||||
<span className='min-w-0'>
|
<span className='min-w-0'>
|
||||||
<span className='text-amber-700 dark:text-amber-300'>
|
<span className='text-warning'>
|
||||||
{t('Special billing expression')}
|
{t('Special billing expression')}
|
||||||
</span>
|
</span>
|
||||||
<code className='text-muted-foreground/70 mt-0.5 line-clamp-1 block font-mono text-[11px] break-all'>
|
<code className='text-muted-foreground/70 mt-0.5 line-clamp-1 block font-mono text-xs break-all'>
|
||||||
{dynamicSummary.rawExpression}
|
{dynamicSummary.rawExpression}
|
||||||
</code>
|
</code>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ function CodeSamplesSection(props: {
|
|||||||
|
|
||||||
<p className='text-muted-foreground mt-2 text-xs'>
|
<p className='text-muted-foreground mt-2 text-xs'>
|
||||||
{t('Replace')}{' '}
|
{t('Replace')}{' '}
|
||||||
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
|
<code className='bg-muted rounded px-1 py-0.5 font-mono text-xs'>
|
||||||
{'<YOUR_API_KEY>'}
|
{'<YOUR_API_KEY>'}
|
||||||
</code>{' '}
|
</code>{' '}
|
||||||
{t('with the API key from your token settings.')}
|
{t('with the API key from your token settings.')}
|
||||||
@@ -582,7 +582,7 @@ function SupportedParametersSection(props: { model: PricingModel }) {
|
|||||||
{p.required && (
|
{p.required && (
|
||||||
<Badge
|
<Badge
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='h-6 border-rose-500/40 px-2 text-sm text-rose-600 dark:text-rose-400'
|
className='border-destructive/40 text-destructive h-6 px-2 text-sm'
|
||||||
>
|
>
|
||||||
{t('required')}
|
{t('required')}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -711,7 +711,7 @@ function RateLimitsSection(props: { model: PricingModel }) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<p className='text-muted-foreground mt-2 text-[11px] leading-relaxed'>
|
<p className='text-muted-foreground mt-2 text-xs leading-relaxed'>
|
||||||
{t(
|
{t(
|
||||||
'RPM = requests per minute, TPM = tokens per minute, RPD = requests per day. Limits apply per token group.'
|
'RPM = requests per minute, TPM = tokens per minute, RPD = requests per day. Limits apply per token group.'
|
||||||
)}
|
)}
|
||||||
@@ -734,11 +734,11 @@ function AuthSection() {
|
|||||||
<div className='space-y-1.5 text-xs leading-relaxed'>
|
<div className='space-y-1.5 text-xs leading-relaxed'>
|
||||||
<p>
|
<p>
|
||||||
{t('All requests must include')}{' '}
|
{t('All requests must include')}{' '}
|
||||||
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
|
<code className='bg-muted rounded px-1 py-0.5 font-mono text-xs'>
|
||||||
Authorization: Bearer <TOKEN>
|
Authorization: Bearer <TOKEN>
|
||||||
</code>{' '}
|
</code>{' '}
|
||||||
{t('header. Anthropic-formatted endpoints accept the')}{' '}
|
{t('header. Anthropic-formatted endpoints accept the')}{' '}
|
||||||
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
|
<code className='bg-muted rounded px-1 py-0.5 font-mono text-xs'>
|
||||||
x-api-key
|
x-api-key
|
||||||
</code>{' '}
|
</code>{' '}
|
||||||
{t('header instead.')}
|
{t('header instead.')}
|
||||||
|
|||||||
@@ -48,16 +48,14 @@ function RankBadge(props: { rank: number }) {
|
|||||||
const isPodium = rank <= 3
|
const isPodium = rank <= 3
|
||||||
const palette =
|
const palette =
|
||||||
rank === 1
|
rank === 1
|
||||||
? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'
|
? 'bg-warning/15 text-warning'
|
||||||
: rank === 2
|
: rank <= 3
|
||||||
? 'bg-slate-100 text-slate-700 dark:bg-slate-500/20 dark:text-slate-300'
|
? 'bg-muted text-foreground'
|
||||||
: rank === 3
|
: 'bg-muted text-muted-foreground'
|
||||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300'
|
|
||||||
: 'bg-muted text-muted-foreground'
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex size-7 shrink-0 items-center justify-center rounded-md font-mono text-xs font-bold tabular-nums',
|
'inline-flex size-7 shrink-0 items-center justify-center rounded-md text-xs font-bold tabular-nums',
|
||||||
palette
|
palette
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -71,16 +69,16 @@ function GrowthChip(props: { value: number }) {
|
|||||||
const isUp = value > 0
|
const isUp = value > 0
|
||||||
const isDown = value < 0
|
const isDown = value < 0
|
||||||
const palette = isUp
|
const palette = isUp
|
||||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
|
? 'bg-success/10 text-success'
|
||||||
: isDown
|
: isDown
|
||||||
? 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-300'
|
? 'bg-destructive/10 text-destructive'
|
||||||
: 'bg-muted text-muted-foreground'
|
: 'bg-muted text-muted-foreground'
|
||||||
const Icon = isUp ? ArrowUpRight : isDown ? ArrowDownRight : null
|
const Icon = isUp ? ArrowUpRight : isDown ? ArrowDownRight : null
|
||||||
const formatted = `${value > 0 ? '+' : ''}${value.toFixed(1)}%`
|
const formatted = `${value > 0 ? '+' : ''}${value.toFixed(1)}%`
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 font-mono text-[11px] font-semibold tabular-nums',
|
'inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-xs font-semibold tabular-nums',
|
||||||
palette
|
palette
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -125,35 +123,35 @@ export function ModelDetailsApps(props: { model: PricingModel }) {
|
|||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
<div className='grid grid-cols-1 gap-2 sm:grid-cols-3'>
|
<div className='grid grid-cols-1 gap-2 sm:grid-cols-3'>
|
||||||
<div className='bg-muted/20 rounded-lg border p-3'>
|
<div className='bg-muted/20 rounded-lg border p-3'>
|
||||||
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
|
||||||
{t('Tracked apps')}
|
{t('Tracked apps')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-foreground mt-1 font-mono text-lg font-semibold tabular-nums'>
|
<div className='text-foreground mt-1 text-lg font-semibold tabular-nums'>
|
||||||
{apps.length}
|
{apps.length}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground/70 text-[11px]'>
|
<p className='text-muted-foreground/70 text-xs'>
|
||||||
{t('Top integrations using this model')}
|
{t('Top integrations using this model')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-muted/20 rounded-lg border p-3'>
|
<div className='bg-muted/20 rounded-lg border p-3'>
|
||||||
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
|
||||||
{t('Monthly tokens')}
|
{t('Monthly tokens')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-foreground mt-1 font-mono text-lg font-semibold tabular-nums'>
|
<div className='text-foreground mt-1 text-lg font-semibold tabular-nums'>
|
||||||
{COMPACT_NUMBER.format(totalMonthlyTokens)}
|
{COMPACT_NUMBER.format(totalMonthlyTokens)}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground/70 text-[11px]'>
|
<p className='text-muted-foreground/70 text-xs'>
|
||||||
{t('Aggregated across the apps below')}
|
{t('Aggregated across the apps below')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-muted/20 rounded-lg border p-3'>
|
<div className='bg-muted/20 rounded-lg border p-3'>
|
||||||
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
|
||||||
{t('#1 by usage')}
|
{t('#1 by usage')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-foreground mt-1 truncate text-base font-semibold'>
|
<div className='text-foreground mt-1 truncate text-base font-semibold'>
|
||||||
{top.name}
|
{top.name}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground/70 truncate text-[11px]'>
|
<p className='text-muted-foreground/70 truncate text-xs'>
|
||||||
{top.category} · {formatTokenVolume(top.monthly_tokens)}{' '}
|
{top.category} · {formatTokenVolume(top.monthly_tokens)}{' '}
|
||||||
{t('tokens / mo')}
|
{t('tokens / mo')}
|
||||||
</p>
|
</p>
|
||||||
@@ -225,7 +223,7 @@ export function ModelDetailsApps(props: { model: PricingModel }) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className='text-muted-foreground/60 text-[11px] leading-relaxed'>
|
<p className='text-muted-foreground/60 text-xs leading-relaxed'>
|
||||||
{t(
|
{t(
|
||||||
'App rankings shown here are simulated for preview purposes and will be replaced with live usage data once the backend integration is complete.'
|
'App rankings shown here are simulated for preview purposes and will be replaced with live usage data once the backend integration is complete.'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -51,22 +51,20 @@ function StatCard(props: {
|
|||||||
const Icon = props.icon
|
const Icon = props.icon
|
||||||
return (
|
return (
|
||||||
<div className='bg-background flex flex-col gap-1 rounded-lg border p-3'>
|
<div className='bg-background flex flex-col gap-1 rounded-lg border p-3'>
|
||||||
<span className='text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider uppercase'>
|
<span className='text-muted-foreground inline-flex items-center gap-1.5 text-xs font-medium tracking-wider uppercase'>
|
||||||
<Icon className='size-3' />
|
<Icon className='size-3' />
|
||||||
{props.label}
|
{props.label}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-foreground font-mono text-lg font-semibold tabular-nums',
|
'text-foreground text-lg font-semibold tabular-nums',
|
||||||
props.valueClassName
|
props.valueClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.value}
|
{props.value}
|
||||||
</span>
|
</span>
|
||||||
{props.hint && (
|
{props.hint && (
|
||||||
<span className='text-muted-foreground/70 text-[11px]'>
|
<span className='text-muted-foreground/70 text-xs'>{props.hint}</span>
|
||||||
{props.hint}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -330,7 +328,7 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
|
|||||||
}
|
}
|
||||||
accent={
|
accent={
|
||||||
incidentCount > 0 ? (
|
incidentCount > 0 ? (
|
||||||
<span className='inline-flex items-center gap-1 text-amber-600 dark:text-amber-400'>
|
<span className='text-warning inline-flex items-center gap-1'>
|
||||||
<AlertTriangle className='size-3.5' />
|
<AlertTriangle className='size-3.5' />
|
||||||
{t('{{count}} incidents', {
|
{t('{{count}} incidents', {
|
||||||
count: incidentCount,
|
count: incidentCount,
|
||||||
|
|||||||
+6
-8
@@ -128,7 +128,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
|
|||||||
{showOverall && (
|
{showOverall && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-mono text-sm font-semibold tabular-nums',
|
'text-sm font-semibold tabular-nums',
|
||||||
getSuccessRateTextClass(overall)
|
getSuccessRateTextClass(overall)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -164,13 +164,11 @@ export function UptimeStatusRow(props: {
|
|||||||
: AlertCircle
|
: AlertCircle
|
||||||
|
|
||||||
const statusColour =
|
const statusColour =
|
||||||
status === 'operational'
|
status === 'operational' || status === 'minor'
|
||||||
? 'text-emerald-600 dark:text-emerald-400'
|
? 'text-success'
|
||||||
: status === 'minor'
|
: status === 'degraded'
|
||||||
? 'text-emerald-600 dark:text-emerald-400'
|
? 'text-warning'
|
||||||
: status === 'degraded'
|
: 'text-destructive'
|
||||||
? 'text-amber-600 dark:text-amber-400'
|
|
||||||
: 'text-rose-600 dark:text-rose-400'
|
|
||||||
|
|
||||||
const statusLabel =
|
const statusLabel =
|
||||||
status === 'operational'
|
status === 'operational'
|
||||||
|
|||||||
+20
-20
@@ -156,12 +156,12 @@ function OverviewMetric(props: {
|
|||||||
<div className='flex min-w-0 items-center gap-2 px-3 py-2'>
|
<div className='flex min-w-0 items-center gap-2 px-3 py-2'>
|
||||||
<Icon className='text-muted-foreground/70 size-3.5 shrink-0' />
|
<Icon className='text-muted-foreground/70 size-3.5 shrink-0' />
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
|
||||||
{props.label}
|
{props.label}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-foreground truncate font-mono text-sm font-semibold tabular-nums',
|
'text-foreground truncate text-sm font-semibold tabular-nums',
|
||||||
props.valueClassName
|
props.valueClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -254,7 +254,7 @@ function CatalogTextValue(props: { children: React.ReactNode }) {
|
|||||||
function CatalogInfoCell(props: { label: string; children: React.ReactNode }) {
|
function CatalogInfoCell(props: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className='bg-card flex min-w-0 flex-col gap-1 px-3 py-2.5'>
|
<div className='bg-card flex min-w-0 flex-col gap-1 px-3 py-2.5'>
|
||||||
<span className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
|
<span className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
|
||||||
{props.label}
|
{props.label}
|
||||||
</span>
|
</span>
|
||||||
{props.children}
|
{props.children}
|
||||||
@@ -361,7 +361,7 @@ function ModelBackendQuickStats(props: { model: PricingModel }) {
|
|||||||
key={stat.key}
|
key={stat.key}
|
||||||
className='bg-background flex min-w-0 flex-col gap-0.5 px-3 py-2.5'
|
className='bg-background flex min-w-0 flex-col gap-0.5 px-3 py-2.5'
|
||||||
>
|
>
|
||||||
<span className='text-muted-foreground inline-flex min-w-0 items-center gap-1 text-[10px] font-medium tracking-wider uppercase'>
|
<span className='text-muted-foreground inline-flex min-w-0 items-center gap-1 text-xs font-medium tracking-wider uppercase'>
|
||||||
<Icon className='size-3 shrink-0' />
|
<Icon className='size-3 shrink-0' />
|
||||||
<span className='truncate'>{stat.label}</span>
|
<span className='truncate'>{stat.label}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -369,7 +369,7 @@ function ModelBackendQuickStats(props: { model: PricingModel }) {
|
|||||||
{stat.value}
|
{stat.value}
|
||||||
</span>
|
</span>
|
||||||
{stat.hint && (
|
{stat.hint && (
|
||||||
<span className='text-muted-foreground/60 truncate text-[10px]'>
|
<span className='text-muted-foreground/60 truncate text-xs'>
|
||||||
{stat.hint}
|
{stat.hint}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -565,7 +565,7 @@ function ModelHeader(props: { model: PricingModel }) {
|
|||||||
{model.billing_mode === 'tiered_expr' && model.billing_expr && (
|
{model.billing_mode === 'tiered_expr' && model.billing_expr && (
|
||||||
<>
|
<>
|
||||||
<span className='text-muted-foreground/30'>·</span>
|
<span className='text-muted-foreground/30'>·</span>
|
||||||
<span className='rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'>
|
<span className='bg-warning/15 text-warning rounded px-1.5 py-0.5 text-xs font-medium'>
|
||||||
{isSpecialExpression
|
{isSpecialExpression
|
||||||
? t('Special billing expression')
|
? t('Special billing expression')
|
||||||
: t('Dynamic Pricing')}
|
: t('Dynamic Pricing')}
|
||||||
@@ -649,15 +649,15 @@ function PriceSection(props: {
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<SectionTitle>{t('Base Price')}</SectionTitle>
|
<SectionTitle>{t('Base Price')}</SectionTitle>
|
||||||
<div className='rounded-lg border border-amber-200/70 bg-amber-50/70 p-3 dark:border-amber-500/20 dark:bg-amber-500/10'>
|
<div className='border-warning/30 bg-warning/10 rounded-lg border p-3'>
|
||||||
<div className='text-sm font-medium text-amber-800 dark:text-amber-200'>
|
<div className='text-warning text-sm font-medium'>
|
||||||
{t('Special billing expression')}
|
{t('Special billing expression')}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground mt-1 text-xs'>
|
<p className='text-muted-foreground mt-1 text-xs'>
|
||||||
{t('Unable to parse structured pricing')}
|
{t('Unable to parse structured pricing')}
|
||||||
</p>
|
</p>
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
|
||||||
{t('Raw expression')}
|
{t('Raw expression')}
|
||||||
</div>
|
</div>
|
||||||
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
|
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
|
||||||
@@ -682,7 +682,7 @@ function PriceSection(props: {
|
|||||||
<div className='text-muted-foreground text-xs'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{t(entry.shortLabel)}
|
{t(entry.shortLabel)}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-foreground mt-1 font-mono text-base font-semibold tabular-nums'>
|
<div className='text-foreground mt-1 text-base font-semibold tabular-nums'>
|
||||||
{entry.formatted}
|
{entry.formatted}
|
||||||
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
|
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
|
||||||
/ {tokenUnitLabel}
|
/ {tokenUnitLabel}
|
||||||
@@ -707,7 +707,7 @@ function PriceSection(props: {
|
|||||||
<span className='text-muted-foreground/70 text-sm'>
|
<span className='text-muted-foreground/70 text-sm'>
|
||||||
{t(entry.shortLabel)}
|
{t(entry.shortLabel)}
|
||||||
</span>
|
</span>
|
||||||
<span className='text-muted-foreground font-mono text-sm tabular-nums'>
|
<span className='text-muted-foreground text-sm tabular-nums'>
|
||||||
{entry.formatted}
|
{entry.formatted}
|
||||||
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
|
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
|
||||||
/ {tokenUnitLabel}
|
/ {tokenUnitLabel}
|
||||||
@@ -730,7 +730,7 @@ function PriceSection(props: {
|
|||||||
<span className='text-muted-foreground text-sm'>
|
<span className='text-muted-foreground text-sm'>
|
||||||
{t('Per request')}
|
{t('Per request')}
|
||||||
</span>
|
</span>
|
||||||
<span className='text-foreground font-mono text-sm font-semibold tabular-nums'>
|
<span className='text-foreground text-sm font-semibold tabular-nums'>
|
||||||
{formatFixedPrice(
|
{formatFixedPrice(
|
||||||
props.model,
|
props.model,
|
||||||
baseGroupKey,
|
baseGroupKey,
|
||||||
@@ -771,7 +771,7 @@ function PriceSection(props: {
|
|||||||
{primaryPriceTypes.map((item) => (
|
{primaryPriceTypes.map((item) => (
|
||||||
<div key={item.type} className='bg-muted/20 rounded-lg border p-3'>
|
<div key={item.type} className='bg-muted/20 rounded-lg border p-3'>
|
||||||
<div className='text-muted-foreground text-xs'>{item.label}</div>
|
<div className='text-muted-foreground text-xs'>{item.label}</div>
|
||||||
<div className='text-foreground mt-1 font-mono text-base font-semibold tabular-nums'>
|
<div className='text-foreground mt-1 text-base font-semibold tabular-nums'>
|
||||||
{renderPrice(item.type)}
|
{renderPrice(item.type)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -788,7 +788,7 @@ function PriceSection(props: {
|
|||||||
<span className='text-muted-foreground/70 text-sm'>
|
<span className='text-muted-foreground/70 text-sm'>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
<span className='text-muted-foreground font-mono text-sm tabular-nums'>
|
<span className='text-muted-foreground text-sm tabular-nums'>
|
||||||
{renderPrice(item.type)}
|
{renderPrice(item.type)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -923,7 +923,7 @@ function GroupPricingSection(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const thClass =
|
const thClass =
|
||||||
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase'
|
'text-muted-foreground py-2 text-xs font-medium tracking-wider uppercase'
|
||||||
|
|
||||||
if (isDynamicPricingModel(props.model)) {
|
if (isDynamicPricingModel(props.model)) {
|
||||||
const dynamicTiers = getDynamicPricingTiers(props.model)
|
const dynamicTiers = getDynamicPricingTiers(props.model)
|
||||||
@@ -933,8 +933,8 @@ function GroupPricingSection(props: {
|
|||||||
<section>
|
<section>
|
||||||
<SectionTitle>{t('Pricing by Group')}</SectionTitle>
|
<SectionTitle>{t('Pricing by Group')}</SectionTitle>
|
||||||
<AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
|
<AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
|
||||||
<div className='rounded-lg border border-amber-200/70 bg-amber-50/70 p-3 dark:border-amber-500/20 dark:bg-amber-500/10'>
|
<div className='border-warning/30 bg-warning/10 rounded-lg border p-3'>
|
||||||
<div className='text-sm font-medium text-amber-800 dark:text-amber-200'>
|
<div className='text-warning text-sm font-medium'>
|
||||||
{t('Special billing expression')}
|
{t('Special billing expression')}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground mt-1 text-xs'>
|
<p className='text-muted-foreground mt-1 text-xs'>
|
||||||
@@ -943,7 +943,7 @@ function GroupPricingSection(props: {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
|
||||||
{t('Raw expression')}
|
{t('Raw expression')}
|
||||||
</div>
|
</div>
|
||||||
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
|
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
|
||||||
@@ -1028,7 +1028,7 @@ function GroupPricingSection(props: {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<p className='text-muted-foreground/40 mt-1.5 text-[10px]'>
|
<p className='text-muted-foreground/40 mt-1.5 text-xs'>
|
||||||
{t('Prices shown per')} {tokenUnitLabel} tokens
|
{t('Prices shown per')} {tokenUnitLabel} tokens
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1119,7 +1119,7 @@ function GroupPricingSection(props: {
|
|||||||
/>
|
/>
|
||||||
<div className='-mx-4 sm:mx-0'>
|
<div className='-mx-4 sm:mx-0'>
|
||||||
{isTokenBased && (
|
{isTokenBased && (
|
||||||
<p className='text-muted-foreground/40 mt-1.5 px-4 text-[10px] sm:px-0'>
|
<p className='text-muted-foreground/40 mt-1.5 px-4 text-xs sm:px-0'>
|
||||||
{t('Prices shown per')} {tokenUnitLabel} tokens
|
{t('Prices shown per')} {tokenUnitLabel} tokens
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div title={t('Average latency')} className='min-w-0'>
|
<div title={t('Average latency')} className='min-w-0'>
|
||||||
<div className='text-muted-foreground/55 text-[10px] leading-4'>
|
<div className='text-muted-foreground/55 text-xs leading-4'>
|
||||||
{t('Latency short')}
|
{t('Latency short')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground/80 font-mono text-xs leading-4 whitespace-nowrap'>
|
<div className='text-muted-foreground/80 font-mono text-xs leading-4 whitespace-nowrap'>
|
||||||
@@ -87,7 +87,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div title={t('Throughput')} className='min-w-0'>
|
<div title={t('Throughput')} className='min-w-0'>
|
||||||
<div className='text-muted-foreground/55 truncate text-[10px] leading-4'>
|
<div className='text-muted-foreground/55 truncate text-xs leading-4'>
|
||||||
{t('Throughput short')}
|
{t('Throughput short')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground/80 font-mono text-xs leading-4 whitespace-nowrap'>
|
<div className='text-muted-foreground/80 font-mono text-xs leading-4 whitespace-nowrap'>
|
||||||
@@ -98,7 +98,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
|
|||||||
title={`${t('Success rate')}: ${success_rate.toFixed(1)}%`}
|
title={`${t('Success rate')}: ${success_rate.toFixed(1)}%`}
|
||||||
className='min-w-0'
|
className='min-w-0'
|
||||||
>
|
>
|
||||||
<div className='text-muted-foreground/55 truncate text-[10px] leading-4'>
|
<div className='text-muted-foreground/55 truncate text-xs leading-4'>
|
||||||
{t('Status short')}
|
{t('Status short')}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex h-4 items-center justify-end gap-0.5'>
|
<div className='flex h-4 items-center justify-end gap-0.5'>
|
||||||
|
|||||||
+13
-15
@@ -135,13 +135,13 @@ export function usePricingColumns(
|
|||||||
if (dynamicSummary.isSpecialExpression) {
|
if (dynamicSummary.isSpecialExpression) {
|
||||||
return (
|
return (
|
||||||
<div className='max-w-full min-w-0'>
|
<div className='max-w-full min-w-0'>
|
||||||
<div className='text-xs font-medium text-amber-700 dark:text-amber-300'>
|
<div className='text-warning text-xs font-medium'>
|
||||||
{t('Special billing expression')}
|
{t('Special billing expression')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-[11px]'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{t('Unable to parse structured pricing')}
|
{t('Unable to parse structured pricing')}
|
||||||
</div>
|
</div>
|
||||||
<code className='text-muted-foreground/70 mt-1 line-clamp-2 block font-mono text-[10px] leading-relaxed break-all'>
|
<code className='text-muted-foreground/70 mt-1 line-clamp-2 block font-mono text-xs leading-relaxed break-all'>
|
||||||
{dynamicSummary.rawExpression}
|
{dynamicSummary.rawExpression}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +159,7 @@ export function usePricingColumns(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-full min-w-0'>
|
<div className='max-w-full min-w-0'>
|
||||||
<span className='font-mono text-sm tabular-nums'>
|
<span className='text-sm tabular-nums'>
|
||||||
{primaryEntries.map((entry, index) => (
|
{primaryEntries.map((entry, index) => (
|
||||||
<span key={entry.key}>
|
<span key={entry.key}>
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
@@ -169,7 +169,7 @@ export function usePricingColumns(
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
<div className='text-muted-foreground/50 text-[10px]'>
|
<div className='text-muted-foreground/50 text-xs'>
|
||||||
/ {tokenUnitLabel} tokens
|
/ {tokenUnitLabel} tokens
|
||||||
{dynamicSummary.tierCount > 1 &&
|
{dynamicSummary.tierCount > 1 &&
|
||||||
` · ${t('{{count}} tiers', {
|
` · ${t('{{count}} tiers', {
|
||||||
@@ -208,12 +208,12 @@ export function usePricingColumns(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-full min-w-0'>
|
<div className='max-w-full min-w-0'>
|
||||||
<span className='font-mono text-sm tabular-nums'>
|
<span className='text-sm tabular-nums'>
|
||||||
{inputPrice}
|
{inputPrice}
|
||||||
<span className='text-muted-foreground/40 mx-1'>/</span>
|
<span className='text-muted-foreground/40 mx-1'>/</span>
|
||||||
{outputPrice}
|
{outputPrice}
|
||||||
</span>
|
</span>
|
||||||
<div className='text-muted-foreground/50 text-[10px]'>
|
<div className='text-muted-foreground/50 text-xs'>
|
||||||
/ {tokenUnitLabel} tokens
|
/ {tokenUnitLabel} tokens
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,8 +232,8 @@ export function usePricingColumns(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-full min-w-0'>
|
<div className='max-w-full min-w-0'>
|
||||||
<span className='font-mono text-sm tabular-nums'>{price}</span>
|
<span className='text-sm tabular-nums'>{price}</span>
|
||||||
<div className='text-muted-foreground/50 text-[10px]'>
|
<div className='text-muted-foreground/50 text-xs'>
|
||||||
/ {t('request')}
|
/ {t('request')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,10 +278,10 @@ export function usePricingColumns(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-full min-w-0'>
|
<div className='max-w-full min-w-0'>
|
||||||
<span className='font-mono text-sm tabular-nums'>
|
<span className='text-sm tabular-nums'>
|
||||||
{stripTrailingZeros(cacheEntry.formatted)}
|
{stripTrailingZeros(cacheEntry.formatted)}
|
||||||
</span>
|
</span>
|
||||||
<div className='text-muted-foreground/50 text-[10px]'>
|
<div className='text-muted-foreground/50 text-xs'>
|
||||||
/ {tokenUnitLabel}
|
/ {tokenUnitLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,10 +308,8 @@ export function usePricingColumns(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-full min-w-0'>
|
<div className='max-w-full min-w-0'>
|
||||||
<span className='font-mono text-sm tabular-nums'>
|
<span className='text-sm tabular-nums'>{cachedPrice}</span>
|
||||||
{cachedPrice}
|
<div className='text-muted-foreground/50 text-xs'>
|
||||||
</span>
|
|
||||||
<div className='text-muted-foreground/50 text-[10px]'>
|
|
||||||
/ {tokenUnitLabel}
|
/ {tokenUnitLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ function FilterChip(props: {
|
|||||||
{(props.option.suffix || props.option.count != null) && (
|
{(props.option.suffix || props.option.count != null) && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md px-1.5 py-0.5 text-[10px]',
|
'rounded-md px-1.5 py-0.5 text-xs',
|
||||||
props.active
|
props.active
|
||||||
? 'bg-background text-foreground'
|
? 'bg-background text-foreground'
|
||||||
: 'bg-muted text-muted-foreground'
|
: 'bg-muted text-muted-foreground'
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export function PricingToolbar(props: PricingToolbarProps) {
|
|||||||
<Filter className='size-4' />
|
<Filter className='size-4' />
|
||||||
{t('Filter')}
|
{t('Filter')}
|
||||||
{props.activeFilterCount > 0 && (
|
{props.activeFilterCount > 0 && (
|
||||||
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
|
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
|
||||||
{props.activeFilterCount}
|
{props.activeFilterCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export function SearchBar(props: SearchBarProps) {
|
|||||||
<X className='size-4' />
|
<X className='size-4' />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<kbd className='bg-muted text-muted-foreground pointer-events-none hidden rounded border px-1.5 py-0.5 font-mono text-[10px] sm:inline-block'>
|
<kbd className='bg-muted text-muted-foreground pointer-events-none hidden rounded border px-1.5 py-0.5 font-mono text-xs sm:inline-block'>
|
||||||
⌘K
|
⌘K
|
||||||
</kbd>
|
</kbd>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export function CheckinCalendarCard({
|
|||||||
{t('Daily Check-in')}
|
{t('Daily Check-in')}
|
||||||
</h3>
|
</h3>
|
||||||
{checkedToday && (
|
{checkedToday && (
|
||||||
<div className='inline-flex items-center gap-1 rounded-md bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-600 sm:gap-1.5 sm:px-2.5 sm:text-xs dark:text-emerald-400'>
|
<div className='bg-success/10 text-success inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium sm:gap-1.5 sm:px-2.5'>
|
||||||
<Sparkles className='h-2.5 w-2.5 sm:h-3 sm:w-3' />
|
<Sparkles className='h-2.5 w-2.5 sm:h-3 sm:w-3' />
|
||||||
{t('Checked in')}
|
{t('Checked in')}
|
||||||
</div>
|
</div>
|
||||||
@@ -337,7 +337,7 @@ export function CheckinCalendarCard({
|
|||||||
<div className='text-xl font-semibold tracking-tight tabular-nums sm:text-2xl'>
|
<div className='text-xl font-semibold tracking-tight tabular-nums sm:text-2xl'>
|
||||||
{checkinData?.stats?.total_checkins || 0}
|
{checkinData?.stats?.total_checkins || 0}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground mt-0.5 text-[10px] font-medium sm:mt-1 sm:text-xs'>
|
<div className='text-muted-foreground mt-0.5 text-xs font-medium sm:mt-1 sm:text-xs'>
|
||||||
{t('Total check-ins')}
|
{t('Total check-ins')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,7 +345,7 @@ export function CheckinCalendarCard({
|
|||||||
<div className='text-xl font-semibold tracking-tight tabular-nums sm:text-2xl'>
|
<div className='text-xl font-semibold tracking-tight tabular-nums sm:text-2xl'>
|
||||||
{formatQuotaWithCurrency(monthlyQuota, { digitsLarge: 0 })}
|
{formatQuotaWithCurrency(monthlyQuota, { digitsLarge: 0 })}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground mt-0.5 text-[10px] font-medium sm:mt-1 sm:text-xs'>
|
<div className='text-muted-foreground mt-0.5 text-xs font-medium sm:mt-1 sm:text-xs'>
|
||||||
{t('This month')}
|
{t('This month')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -358,7 +358,7 @@ export function CheckinCalendarCard({
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground mt-0.5 text-[10px] font-medium sm:mt-1 sm:text-xs'>
|
<div className='text-muted-foreground mt-0.5 text-xs font-medium sm:mt-1 sm:text-xs'>
|
||||||
{t('Total earned')}
|
{t('Total earned')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -398,7 +398,7 @@ export function CheckinCalendarCard({
|
|||||||
{weekDays.map((day) => (
|
{weekDays.map((day) => (
|
||||||
<div
|
<div
|
||||||
key={day}
|
key={day}
|
||||||
className='text-muted-foreground flex h-7 items-center justify-center text-[10px] font-medium sm:h-8 sm:text-xs'
|
className='text-muted-foreground flex h-7 items-center justify-center text-xs font-medium sm:h-8 sm:text-xs'
|
||||||
>
|
>
|
||||||
{day}
|
{day}
|
||||||
</div>
|
</div>
|
||||||
@@ -430,7 +430,7 @@ export function CheckinCalendarCard({
|
|||||||
>
|
>
|
||||||
<span className='tabular-nums'>{dayNum}</span>
|
<span className='tabular-nums'>{dayNum}</span>
|
||||||
{isCheckedIn && !isToday && (
|
{isCheckedIn && !isToday && (
|
||||||
<span className='absolute bottom-0.5 h-1 w-1 rounded-full bg-emerald-500 sm:bottom-1' />
|
<span className='bg-success absolute bottom-0.5 h-1 w-1 rounded-full sm:bottom-1' />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
@@ -458,7 +458,7 @@ export function CheckinCalendarCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer hint */}
|
{/* Footer hint */}
|
||||||
<div className='text-muted-foreground border-t pt-3 text-center text-[11px] sm:pt-4 sm:text-xs'>
|
<div className='text-muted-foreground border-t pt-3 text-center text-xs sm:pt-4 sm:text-xs'>
|
||||||
{t('You can only check in once per day')}
|
{t('You can only check in once per day')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
|
|||||||
|
|
||||||
{showUnsupportedNotice && (
|
{showUnsupportedNotice && (
|
||||||
<div className='bg-muted/60 text-muted-foreground flex items-start gap-3 rounded-md p-4 text-sm'>
|
<div className='bg-muted/60 text-muted-foreground flex items-start gap-3 rounded-md p-4 text-sm'>
|
||||||
<ShieldAlert className='mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500' />
|
<ShieldAlert className='text-warning mt-0.5 h-4 w-4 flex-shrink-0' />
|
||||||
<div>
|
<div>
|
||||||
<p className='text-foreground font-medium'>
|
<p className='text-foreground font-medium'>
|
||||||
{t('Passkey not supported on this device')}
|
{t('Passkey not supported on this device')}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-foreground mt-1.5 truncate font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
|
<div className='text-foreground mt-1.5 truncate text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
|
||||||
{item.value}
|
{item.value}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
|
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ export function GrowthText(props: GrowthTextProps) {
|
|||||||
if (!Number.isFinite(v) || v === 0) {
|
if (!Number.isFinite(v) || v === 0) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn('text-muted-foreground/80 tabular-nums', props.className)}
|
||||||
'text-muted-foreground/80 font-mono tabular-nums',
|
|
||||||
props.className
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
0%
|
0%
|
||||||
</span>
|
</span>
|
||||||
@@ -46,10 +43,8 @@ export function GrowthText(props: GrowthTextProps) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-mono tabular-nums',
|
'tabular-nums',
|
||||||
isUp
|
isUp ? 'text-success' : 'text-destructive',
|
||||||
? 'text-emerald-600 dark:text-emerald-400'
|
|
||||||
: 'text-rose-600 dark:text-rose-400',
|
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ function VendorList(props: {
|
|||||||
<ul>
|
<ul>
|
||||||
{props.rows.map((vendor) => (
|
{props.rows.map((vendor) => (
|
||||||
<li key={vendor.vendor} className='flex items-center gap-3 py-2.5'>
|
<li key={vendor.vendor} className='flex items-center gap-3 py-2.5'>
|
||||||
<span className='text-muted-foreground/80 w-6 shrink-0 text-right font-mono text-xs tabular-nums'>
|
<span className='text-muted-foreground/80 w-6 shrink-0 text-right text-xs tabular-nums'>
|
||||||
{vendor.rank}.
|
{vendor.rank}.
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -296,10 +296,10 @@ function VendorList(props: {
|
|||||||
{vendor.vendor}
|
{vendor.vendor}
|
||||||
</VendorLink>
|
</VendorLink>
|
||||||
<div className='shrink-0 text-right'>
|
<div className='shrink-0 text-right'>
|
||||||
<div className='text-foreground font-mono text-sm font-semibold tabular-nums'>
|
<div className='text-foreground text-sm font-semibold tabular-nums'>
|
||||||
{formatTokens(vendor.total_tokens)}
|
{formatTokens(vendor.total_tokens)}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground/80 font-mono text-[11px] tabular-nums'>
|
<div className='text-muted-foreground/80 text-xs tabular-nums'>
|
||||||
{formatShare(vendor.share)}
|
{formatShare(vendor.share)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function ModelList(props: {
|
|||||||
: 'flex items-center gap-3 py-2.5'
|
: 'flex items-center gap-3 py-2.5'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className='text-muted-foreground/80 w-6 shrink-0 text-right font-mono text-xs tabular-nums'>
|
<span className='text-muted-foreground/80 w-6 shrink-0 text-right text-xs tabular-nums'>
|
||||||
{row.rank}.
|
{row.rank}.
|
||||||
</span>
|
</span>
|
||||||
<span className='shrink-0'>
|
<span className='shrink-0'>
|
||||||
@@ -99,7 +99,7 @@ function ModelList(props: {
|
|||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
compact
|
compact
|
||||||
? 'text-muted-foreground/80 truncate text-[11px] italic'
|
? 'text-muted-foreground/80 truncate text-xs italic'
|
||||||
: 'text-muted-foreground/80 truncate text-xs italic'
|
: 'text-muted-foreground/80 truncate text-xs italic'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -113,8 +113,8 @@ function ModelList(props: {
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
compact
|
compact
|
||||||
? 'text-foreground font-mono text-xs font-semibold tabular-nums'
|
? 'text-foreground text-xs font-semibold tabular-nums'
|
||||||
: 'text-foreground font-mono text-sm font-semibold tabular-nums'
|
: 'text-foreground text-sm font-semibold tabular-nums'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{formatTokens(row.total_tokens)}
|
{formatTokens(row.total_tokens)}
|
||||||
@@ -129,7 +129,7 @@ function ModelList(props: {
|
|||||||
</div>
|
</div>
|
||||||
<GrowthText
|
<GrowthText
|
||||||
value={row.growth_pct}
|
value={row.growth_pct}
|
||||||
className={compact ? 'text-[10px]' : 'text-[11px]'}
|
className={compact ? 'text-xs' : 'text-xs'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -177,10 +177,10 @@ export function ModelsSection(props: ModelsSectionProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='shrink-0 text-right'>
|
<div className='shrink-0 text-right'>
|
||||||
<div className='text-foreground font-mono text-2xl font-semibold tabular-nums'>
|
<div className='text-foreground text-2xl font-semibold tabular-nums'>
|
||||||
{formatTokens(totalTokens)}
|
{formatTokens(totalTokens)}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground/80 text-[10px] font-medium tracking-widest uppercase'>
|
<div className='text-muted-foreground/80 text-xs font-medium tracking-widest uppercase'>
|
||||||
{t('tokens')}
|
{t('tokens')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +210,7 @@ export function ModelsSection(props: ModelsSectionProps) {
|
|||||||
<div className='border-t'>
|
<div className='border-t'>
|
||||||
<header className='px-5 pt-4 pb-2'>
|
<header className='px-5 pt-4 pb-2'>
|
||||||
<h3 className='text-foreground inline-flex items-center gap-2 text-sm font-semibold'>
|
<h3 className='text-foreground inline-flex items-center gap-2 text-sm font-semibold'>
|
||||||
<Trophy className='size-3.5 text-amber-500' />
|
<Trophy className='text-warning size-3.5' />
|
||||||
{t('LLM Leaderboard')}
|
{t('LLM Leaderboard')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-muted-foreground/80 mt-0.5 text-xs'>
|
<p className='text-muted-foreground/80 mt-0.5 text-xs'>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function PulseSection(props: PulseSectionProps) {
|
|||||||
<PulseCard
|
<PulseCard
|
||||||
title={t('Trending up')}
|
title={t('Trending up')}
|
||||||
description={t('Models climbing the leaderboard')}
|
description={t('Models climbing the leaderboard')}
|
||||||
icon={<TrendingUp className='size-4 text-emerald-500' />}
|
icon={<TrendingUp className='text-success size-4' />}
|
||||||
>
|
>
|
||||||
{props.movers.length === 0 ? (
|
{props.movers.length === 0 ? (
|
||||||
<PulseEmpty label={t('No notable climbers right now')} />
|
<PulseEmpty label={t('No notable climbers right now')} />
|
||||||
@@ -62,7 +62,7 @@ export function PulseSection(props: PulseSectionProps) {
|
|||||||
<PulseCard
|
<PulseCard
|
||||||
title={t('Trending down')}
|
title={t('Trending down')}
|
||||||
description={t('Models losing positions')}
|
description={t('Models losing positions')}
|
||||||
icon={<TrendingDown className='size-4 text-rose-500' />}
|
icon={<TrendingDown className='text-destructive size-4' />}
|
||||||
>
|
>
|
||||||
{props.droppers.length === 0 ? (
|
{props.droppers.length === 0 ? (
|
||||||
<PulseEmpty label={t('No notable drops right now')} />
|
<PulseEmpty label={t('No notable drops right now')} />
|
||||||
@@ -119,7 +119,7 @@ function MoverRow(props: { row: RankingMover; intent: 'up' | 'down' }) {
|
|||||||
>
|
>
|
||||||
{props.row.model_name}
|
{props.row.model_name}
|
||||||
</ModelLink>
|
</ModelLink>
|
||||||
<p className='text-muted-foreground/80 truncate text-[11px]'>
|
<p className='text-muted-foreground/80 truncate text-xs'>
|
||||||
#{props.row.current_rank} ·{' '}
|
#{props.row.current_rank} ·{' '}
|
||||||
<VendorLink vendor={props.row.vendor}>
|
<VendorLink vendor={props.row.vendor}>
|
||||||
{props.row.vendor.toLowerCase()}
|
{props.row.vendor.toLowerCase()}
|
||||||
@@ -128,10 +128,8 @@ function MoverRow(props: { row: RankingMover; intent: 'up' | 'down' }) {
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex shrink-0 items-center gap-0.5 font-mono text-xs font-semibold tabular-nums',
|
'inline-flex shrink-0 items-center gap-0.5 text-xs font-semibold tabular-nums',
|
||||||
props.intent === 'up'
|
props.intent === 'up' ? 'text-success' : 'text-destructive'
|
||||||
? 'text-emerald-600 dark:text-emerald-400'
|
|
||||||
: 'text-rose-600 dark:text-rose-400'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.intent === 'up' ? (
|
{props.intent === 'up' ? (
|
||||||
|
|||||||
+4
-1
@@ -22,7 +22,10 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { DataTableRowActionMenu, StaticDataTable } from '@/components/data-table'
|
import {
|
||||||
|
DataTableRowActionMenu,
|
||||||
|
StaticDataTable,
|
||||||
|
} from '@/components/data-table'
|
||||||
import {
|
import {
|
||||||
sideDrawerContentClassName,
|
sideDrawerContentClassName,
|
||||||
sideDrawerFormClassName,
|
sideDrawerFormClassName,
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
|
|||||||
id: 'price',
|
id: 'price',
|
||||||
header: t('Price'),
|
header: t('Price'),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className='font-semibold text-emerald-600'>
|
<span className='text-success font-semibold'>
|
||||||
${Number(row.original.plan.price_amount || 0).toFixed(2)}
|
${Number(row.original.plan.price_amount || 0).toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|||||||
+11
-14
@@ -74,14 +74,13 @@ const INSTANCE_SKELETON_KEYS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const STATUS_CLASS_NAME: Record<SystemInstanceStatus, string> = {
|
const STATUS_CLASS_NAME: Record<SystemInstanceStatus, string> = {
|
||||||
online:
|
online: 'bg-success/10 text-success',
|
||||||
'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300',
|
stale: 'bg-warning/10 text-warning',
|
||||||
stale: 'bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_DOT_CLASS_NAME: Record<SystemInstanceStatus, string> = {
|
const STATUS_DOT_CLASS_NAME: Record<SystemInstanceStatus, string> = {
|
||||||
online: 'bg-emerald-500',
|
online: 'bg-success',
|
||||||
stale: 'bg-amber-500',
|
stale: 'bg-warning',
|
||||||
}
|
}
|
||||||
|
|
||||||
function roleLabel(instance: SystemInstance) {
|
function roleLabel(instance: SystemInstance) {
|
||||||
@@ -136,9 +135,9 @@ function formatBytes(bytes?: number): string {
|
|||||||
|
|
||||||
function ringColorClass(percent: number | null) {
|
function ringColorClass(percent: number | null) {
|
||||||
if (percent === null) return 'text-muted-foreground/40'
|
if (percent === null) return 'text-muted-foreground/40'
|
||||||
if (percent >= 90) return 'text-red-500'
|
if (percent >= 90) return 'text-destructive'
|
||||||
if (percent >= 70) return 'text-amber-500'
|
if (percent >= 70) return 'text-warning'
|
||||||
return 'text-emerald-500'
|
return 'text-success'
|
||||||
}
|
}
|
||||||
|
|
||||||
type RingProgressProps = {
|
type RingProgressProps = {
|
||||||
@@ -205,9 +204,7 @@ function ResourceCell(props: ResourceCellProps) {
|
|||||||
const content = (
|
const content = (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<RingProgress percent={percent} />
|
<RingProgress percent={percent} />
|
||||||
<span className='font-mono text-[11px] tabular-nums'>
|
<span className='text-xs tabular-nums'>{formatPercent(props.value)}</span>
|
||||||
{formatPercent(props.value)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -304,7 +301,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
|
|||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/15 dark:text-amber-300'
|
className='border-warning/30 bg-warning/10 text-warning'
|
||||||
>
|
>
|
||||||
<AlertTriangle
|
<AlertTriangle
|
||||||
className='size-3'
|
className='size-3'
|
||||||
@@ -328,7 +325,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
|
|||||||
<div className='mb-1 font-medium'>
|
<div className='mb-1 font-medium'>
|
||||||
{t('Example')}
|
{t('Example')}
|
||||||
</div>
|
</div>
|
||||||
<code className='bg-muted block rounded-md px-2 py-1.5 font-mono text-[11px] break-all'>
|
<code className='bg-muted block rounded-md px-2 py-1.5 font-mono text-xs break-all'>
|
||||||
NODE_NAME=new-api-master-1
|
NODE_NAME=new-api-master-1
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,7 +339,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
|
|||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground truncate font-mono text-[11px]'>
|
<div className='text-muted-foreground truncate font-mono text-xs'>
|
||||||
{instance.info?.host?.hostname || '-'}
|
{instance.info?.host?.hostname || '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,26 +53,23 @@ const STATUS_VARIANT: Record<SystemTaskStatus, 'secondary' | 'destructive'> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_CLASS_NAME: Record<SystemTaskStatus, string> = {
|
const STATUS_CLASS_NAME: Record<SystemTaskStatus, string> = {
|
||||||
pending:
|
pending: 'bg-warning/10 text-warning',
|
||||||
'bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300',
|
running: 'bg-info/10 text-info [&_span]:bg-info',
|
||||||
running:
|
succeeded: 'bg-success/10 text-success',
|
||||||
'bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300 [&_span]:bg-sky-500',
|
|
||||||
succeeded:
|
|
||||||
'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300',
|
|
||||||
failed: '',
|
failed: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_DOT_CLASS_NAME: Record<SystemTaskStatus, string> = {
|
const STATUS_DOT_CLASS_NAME: Record<SystemTaskStatus, string> = {
|
||||||
pending: 'bg-amber-500',
|
pending: 'bg-warning',
|
||||||
running: 'bg-sky-500',
|
running: 'bg-info',
|
||||||
succeeded: 'bg-emerald-500',
|
succeeded: 'bg-success',
|
||||||
failed: 'bg-destructive',
|
failed: 'bg-destructive',
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROGRESS_BAR_CLASS_NAME: Record<SystemTaskStatus, string> = {
|
const PROGRESS_BAR_CLASS_NAME: Record<SystemTaskStatus, string> = {
|
||||||
pending: '[&_[data-slot=progress-indicator]]:bg-amber-500',
|
pending: '[&_[data-slot=progress-indicator]]:bg-warning',
|
||||||
running: '[&_[data-slot=progress-indicator]]:bg-sky-500',
|
running: '[&_[data-slot=progress-indicator]]:bg-info',
|
||||||
succeeded: '[&_[data-slot=progress-indicator]]:bg-emerald-500',
|
succeeded: '[&_[data-slot=progress-indicator]]:bg-success',
|
||||||
failed: '[&_[data-slot=progress-indicator]]:bg-destructive',
|
failed: '[&_[data-slot=progress-indicator]]:bg-destructive',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +139,7 @@ function SystemTasksTable(props: SystemTasksTableProps) {
|
|||||||
<div className='font-medium'>
|
<div className='font-medium'>
|
||||||
{t(TYPE_LABEL[task.type] ?? task.type)}
|
{t(TYPE_LABEL[task.type] ?? task.type)}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground font-mono text-[11px]'>
|
<div className='text-muted-foreground font-mono text-xs'>
|
||||||
{TYPE_DISPLAY_ID[task.type] ?? task.type}
|
{TYPE_DISPLAY_ID[task.type] ?? task.type}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,7 +253,7 @@ export function SystemTasksPanel() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'size-1.5 rounded-full',
|
'size-1.5 rounded-full',
|
||||||
hasActiveTasks ? 'bg-emerald-500' : 'bg-muted-foreground/40'
|
hasActiveTasks ? 'bg-success' : 'bg-muted-foreground/40'
|
||||||
)}
|
)}
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
/>
|
/>
|
||||||
|
|||||||
+2
-2
@@ -42,8 +42,8 @@ export function FormDirtyIndicator({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsPageTitleStatusPortal>
|
<SettingsPageTitleStatusPortal>
|
||||||
<span className='inline-flex h-5 items-center gap-1.5 rounded-full bg-amber-500/10 px-2 text-[11px] font-medium whitespace-nowrap text-amber-700 ring-1 ring-amber-500/20 ring-inset dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/20'>
|
<span className='bg-warning/10 text-warning ring-warning/25 inline-flex h-5 items-center gap-1.5 rounded-full px-2 text-xs font-medium whitespace-nowrap ring-1 ring-inset'>
|
||||||
<span className='size-1.5 rounded-full bg-amber-500 dark:bg-amber-300' />
|
<span className='bg-warning size-1.5 rounded-full' />
|
||||||
{message ? t(message) : t('Unsaved changes')}
|
{message ? t(message) : t('Unsaved changes')}
|
||||||
</span>
|
</span>
|
||||||
</SettingsPageTitleStatusPortal>
|
</SettingsPageTitleStatusPortal>
|
||||||
|
|||||||
+1
-1
@@ -532,7 +532,7 @@ export function WaffoPancakeSettingsSection({
|
|||||||
<>
|
<>
|
||||||
<div className='relative flex items-center py-1'>
|
<div className='relative flex items-center py-1'>
|
||||||
<div className='flex-1 border-t' />
|
<div className='flex-1 border-t' />
|
||||||
<span className='text-muted-foreground px-3 text-[10px] font-medium tracking-[0.2em] uppercase'>
|
<span className='text-muted-foreground px-3 text-xs font-medium tracking-[0.2em] uppercase'>
|
||||||
{t('or pick existing')}
|
{t('or pick existing')}
|
||||||
</span>
|
</span>
|
||||||
<div className='flex-1 border-t' />
|
<div className='flex-1 border-t' />
|
||||||
|
|||||||
@@ -404,7 +404,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
|
|||||||
|
|
||||||
<div className={sideDrawerFormClassName('gap-5')}>
|
<div className={sideDrawerFormClassName('gap-5')}>
|
||||||
<section className='space-y-2'>
|
<section className='space-y-2'>
|
||||||
<h3 className='text-sm font-semibold'>{t('The two roles of a group')}</h3>
|
<h3 className='text-sm font-semibold'>
|
||||||
|
{t('The two roles of a group')}
|
||||||
|
</h3>
|
||||||
<div className='text-muted-foreground space-y-2 text-sm leading-6'>
|
<div className='text-muted-foreground space-y-2 text-sm leading-6'>
|
||||||
<p>
|
<p>
|
||||||
{t(
|
{t(
|
||||||
@@ -416,7 +418,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
|
|||||||
{t('Token group')}
|
{t('Token group')}
|
||||||
</span>
|
</span>
|
||||||
{': '}
|
{': '}
|
||||||
{t('decides which channels are used and which base ratio applies.')}
|
{t(
|
||||||
|
'decides which channels are used and which base ratio applies.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className='text-foreground font-medium'>
|
<span className='text-foreground font-medium'>
|
||||||
@@ -431,7 +435,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className='space-y-2'>
|
<section className='space-y-2'>
|
||||||
<h3 className='text-sm font-semibold'>{t('How a call is priced')}</h3>
|
<h3 className='text-sm font-semibold'>
|
||||||
|
{t('How a call is priced')}
|
||||||
|
</h3>
|
||||||
<ol className='text-muted-foreground list-decimal space-y-2 pl-5 text-sm leading-6'>
|
<ol className='text-muted-foreground list-decimal space-y-2 pl-5 text-sm leading-6'>
|
||||||
<li>
|
<li>
|
||||||
<span className='text-foreground font-medium'>
|
<span className='text-foreground font-medium'>
|
||||||
@@ -453,7 +459,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
|
|||||||
<span className='text-foreground font-medium'>
|
<span className='text-foreground font-medium'>
|
||||||
{t('Charge.')}
|
{t('Charge.')}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{t('Cost = model price × that one ratio. Nothing else from the group settings enters the formula.')}
|
{t(
|
||||||
|
'Cost = model price × that one ratio. Nothing else from the group settings enters the formula.'
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p className='text-muted-foreground text-sm leading-6'>
|
<p className='text-muted-foreground text-sm leading-6'>
|
||||||
@@ -466,7 +474,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
|
|||||||
<section className='space-y-3'>
|
<section className='space-y-3'>
|
||||||
<h3 className='text-sm font-semibold'>{t('Worked example')}</h3>
|
<h3 className='text-sm font-semibold'>{t('Worked example')}</h3>
|
||||||
<p className='text-muted-foreground text-sm leading-6'>
|
<p className='text-muted-foreground text-sm leading-6'>
|
||||||
{t('The admin configured three groups and one special ratio rule:')}
|
{t(
|
||||||
|
'The admin configured three groups and one special ratio rule:'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='overflow-hidden rounded-lg border'>
|
<div className='overflow-hidden rounded-lg border'>
|
||||||
@@ -529,7 +539,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className='space-y-2 p-3'>
|
<div className='space-y-2 p-3'>
|
||||||
<GuideStepRow chip='1'>
|
<GuideStepRow chip='1'>
|
||||||
{t('Billing group = premium (the token has a group, so use it)')}
|
{t(
|
||||||
|
'Billing group = premium (the token has a group, so use it)'
|
||||||
|
)}
|
||||||
</GuideStepRow>
|
</GuideStepRow>
|
||||||
<GuideStepRow chip='2'>
|
<GuideStepRow chip='2'>
|
||||||
{t(
|
{t(
|
||||||
@@ -550,7 +562,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className='space-y-2 p-3'>
|
<div className='space-y-2 p-3'>
|
||||||
<GuideStepRow chip='1'>
|
<GuideStepRow chip='1'>
|
||||||
{t('Billing group = default (the token has a group, so use it)')}
|
{t(
|
||||||
|
'Billing group = default (the token has a group, so use it)'
|
||||||
|
)}
|
||||||
</GuideStepRow>
|
</GuideStepRow>
|
||||||
<GuideStepRow chip='2'>
|
<GuideStepRow chip='2'>
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
+8
-5
@@ -29,6 +29,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
import { StaticDataTable } from '@/components/data-table/static/static-data-table'
|
import { StaticDataTable } from '@/components/data-table/static/static-data-table'
|
||||||
import { StaticRowActions } from '@/components/data-table/static/static-row-actions'
|
import { StaticRowActions } from '@/components/data-table/static/static-row-actions'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import {
|
import {
|
||||||
sideDrawerContentClassName,
|
sideDrawerContentClassName,
|
||||||
sideDrawerFormClassName,
|
sideDrawerFormClassName,
|
||||||
@@ -49,7 +50,6 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import { Dialog } from '@/components/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
@@ -1100,10 +1100,13 @@ function GroupOverrideDialog({
|
|||||||
<p className='text-muted-foreground text-xs'>
|
<p className='text-muted-foreground text-xs'>
|
||||||
{baseRatio !== undefined
|
{baseRatio !== undefined
|
||||||
? t('(instead of {{ratio}})', { ratio: baseRatio })
|
? t('(instead of {{ratio}})', { ratio: baseRatio })
|
||||||
: t('Multiplier applied when {{userGroup}} uses {{targetGroup}}', {
|
: t(
|
||||||
userGroup: userGroup || t('this user group'),
|
'Multiplier applied when {{userGroup}} uses {{targetGroup}}',
|
||||||
targetGroup: targetGroup || t('this token group'),
|
{
|
||||||
})}
|
userGroup: userGroup || t('this user group'),
|
||||||
|
targetGroup: targetGroup || t('this token group'),
|
||||||
|
}
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-3
@@ -83,7 +83,7 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<AlertTriangle className='h-3.5 w-3.5 shrink-0 text-amber-500' />
|
<AlertTriangle className='text-warning h-3.5 w-3.5 shrink-0' />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
@@ -199,7 +199,7 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
{displayName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
{selectableCount > 0 && (
|
{selectableCount > 0 && (
|
||||||
<span className='bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-[11px] leading-none font-normal tabular-nums'>
|
<span className='bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-xs leading-none font-normal tabular-nums'>
|
||||||
{selectedCount}/{selectableCount}
|
{selectedCount}/{selectableCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -361,7 +361,7 @@ function renderUpstreamValue(args: RenderUpstreamValueArgs) {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<AlertTriangle className='h-3.5 w-3.5 shrink-0 text-amber-500' />
|
<AlertTriangle className='text-warning h-3.5 w-3.5 shrink-0' />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t('This data may be unreliable, use with caution')}</p>
|
<p>{t('This data may be unreliable, use with caution')}</p>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Zap } from 'lucide-react'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { DataTableColumnHeader } from '@/components/data-table'
|
import { DataTableColumnHeader } from '@/components/data-table'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge, tintedBadgeClassMap } from '@/components/status-badge'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -90,7 +90,7 @@ export function createTimestampColumn<T>(config: {
|
|||||||
return <span className='text-muted-foreground/60 text-xs'>-</span>
|
return <span className='text-muted-foreground/60 text-xs'>-</span>
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className='font-mono text-xs tabular-nums'>
|
<span className='text-xs tabular-nums'>
|
||||||
{formatTimestampToDate(timestamp, unit)}
|
{formatTimestampToDate(timestamp, unit)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -137,22 +137,16 @@ export function createDurationColumn<T>(config: {
|
|||||||
const variant =
|
const variant =
|
||||||
duration.durationSec > warningThresholdSec ? 'danger' : 'success'
|
duration.durationSec > warningThresholdSec ? 'danger' : 'success'
|
||||||
|
|
||||||
const durationBgMap: Record<string, string> = {
|
|
||||||
success:
|
|
||||||
'border border-emerald-200/40 bg-emerald-50/35 !text-emerald-600 dark:border-emerald-900/40 dark:bg-emerald-950/15 dark:!text-emerald-400',
|
|
||||||
warning:
|
|
||||||
'border border-amber-200/45 bg-amber-50/35 !text-amber-600 dark:border-amber-900/40 dark:bg-amber-950/15 dark:!text-amber-400',
|
|
||||||
danger:
|
|
||||||
'border border-rose-200/50 bg-rose-50/35 !text-red-600 dark:border-rose-900/40 dark:bg-rose-950/15 dark:!text-red-400',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
label={`${duration.durationSec.toFixed(1)}s`}
|
label={`${duration.durationSec.toFixed(1)}s`}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className={cn('rounded-md font-mono', durationBgMap[variant])}
|
className={cn(
|
||||||
|
'rounded-md tabular-nums',
|
||||||
|
tintedBadgeClassMap[variant]
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -225,7 +219,7 @@ export function createFailReasonColumn<T>(config: {
|
|||||||
onClick={() => setDialogOpen(true)}
|
onClick={() => setDialogOpen(true)}
|
||||||
title={cellTitle}
|
title={cellTitle}
|
||||||
>
|
>
|
||||||
<span className='truncate leading-snug text-red-600 group-hover:underline dark:text-red-400'>
|
<span className='text-destructive truncate leading-snug group-hover:underline'>
|
||||||
{failReason}
|
{failReason}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+38
-30
@@ -21,7 +21,11 @@ import { CircleAlert, GitBranch, Sparkles, KeyRound } from 'lucide-react'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
import {
|
||||||
|
StatusBadge,
|
||||||
|
tintedBadgeClassMap,
|
||||||
|
type StatusBadgeProps,
|
||||||
|
} from '@/components/status-badge'
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -304,7 +308,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-w-0 flex-col gap-0.5'>
|
<div className='flex min-w-0 flex-col gap-0.5'>
|
||||||
<span className='truncate font-mono text-xs tabular-nums'>
|
<span className='truncate text-xs tabular-nums'>
|
||||||
{formatTimestampToDate(timestamp)}
|
{formatTimestampToDate(timestamp)}
|
||||||
</span>
|
</span>
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
@@ -312,7 +316,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
variant={config.color as StatusBadgeProps['variant']}
|
variant={config.color as StatusBadgeProps['variant']}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className='!text-xs [&_span]:!text-xs'
|
className='text-xs [&_span]:text-xs'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -402,7 +406,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GitBranch
|
<GitBranch
|
||||||
className='size-3.5 text-amber-500'
|
className='text-warning size-3.5'
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -423,7 +427,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
{affinity && (
|
{affinity && (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='absolute -top-1 -right-1 leading-none text-amber-500'
|
className='text-warning absolute -top-1 -right-1 leading-none'
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setAffinityTarget({
|
setAffinityTarget({
|
||||||
@@ -443,7 +447,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{log.channel_name && (
|
{log.channel_name && (
|
||||||
<span className='text-muted-foreground/70 truncate [font-family:var(--font-body)] !text-xs'>
|
<span className='text-muted-foreground/70 truncate text-xs'>
|
||||||
{channelName}
|
{channelName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -510,7 +514,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[11px] font-semibold',
|
'text-xs font-semibold',
|
||||||
!sensitiveVisible && 'bg-muted text-muted-foreground'
|
!sensitiveVisible && 'bg-muted text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
@@ -577,7 +581,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
copyText={sensitiveVisible ? tokenName : undefined}
|
copyText={sensitiveVisible ? tokenName : undefined}
|
||||||
size='sm'
|
size='sm'
|
||||||
showDot={false}
|
showDot={false}
|
||||||
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-full gap-1.5 overflow-hidden rounded-md border px-2 py-0.5 [font-family:var(--font-body)]'
|
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-full gap-1.5 overflow-hidden rounded-md border px-2 py-0.5'
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{sensitiveVisible && tokenName.length > 16 && (
|
{sensitiveVisible && tokenName.length > 16 && (
|
||||||
@@ -588,7 +592,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
{metaParts.length > 0 && (
|
{metaParts.length > 0 && (
|
||||||
<span className='text-muted-foreground/60 truncate [font-family:var(--font-body)] !text-xs'>
|
<span className='text-muted-foreground/60 truncate text-xs'>
|
||||||
{metaParts.join(' · ')}
|
{metaParts.join(' · ')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -637,16 +641,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
? getFirstResponseTimeColor(frt / 1000)
|
? getFirstResponseTimeColor(frt / 1000)
|
||||||
: 'neutral'
|
: 'neutral'
|
||||||
|
|
||||||
const timingBgMap: Record<string, string> = {
|
const timingClass = (variant: string) =>
|
||||||
success:
|
tintedBadgeClassMap[
|
||||||
'border border-emerald-200/40 bg-emerald-50/35 !text-emerald-600 dark:border-emerald-900/40 dark:bg-emerald-950/15 dark:!text-emerald-400',
|
(variant in tintedBadgeClassMap
|
||||||
warning:
|
? variant
|
||||||
'border border-amber-200/45 bg-amber-50/35 !text-amber-600 dark:border-amber-900/40 dark:bg-amber-950/15 dark:!text-amber-400',
|
: 'neutral') as keyof typeof tintedBadgeClassMap
|
||||||
danger:
|
]
|
||||||
'border border-rose-200/50 bg-rose-50/35 !text-red-600 dark:border-rose-900/40 dark:bg-rose-950/15 dark:!text-red-400',
|
|
||||||
neutral:
|
|
||||||
'border border-border/60 bg-muted/30 dark:border-border/40 dark:bg-muted/20',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
@@ -656,7 +656,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
variant={timeVariant as StatusBadgeProps['variant']}
|
variant={timeVariant as StatusBadgeProps['variant']}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className={cn('rounded-md font-mono', timingBgMap[timeVariant])}
|
className={cn(
|
||||||
|
'rounded-md tabular-nums',
|
||||||
|
timingClass(timeVariant)
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{log.is_stream &&
|
{log.is_stream &&
|
||||||
(frt != null && frt > 0 ? (
|
(frt != null && frt > 0 ? (
|
||||||
@@ -667,8 +670,8 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
showDot={false}
|
showDot={false}
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md font-mono',
|
'rounded-md tabular-nums',
|
||||||
timingBgMap[frtVariant]
|
timingClass(frtVariant)
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -678,12 +681,15 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
size='sm'
|
size='sm'
|
||||||
showDot={false}
|
showDot={false}
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className={cn('rounded-md font-mono', timingBgMap.neutral)}
|
className={cn(
|
||||||
|
'rounded-md tabular-nums',
|
||||||
|
tintedBadgeClassMap.neutral
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-1 [font-family:var(--font-body)] !text-xs leading-none'>
|
<div className='flex items-center gap-1 text-xs leading-none'>
|
||||||
<span className='text-muted-foreground/60 [font-family:var(--font-body)] !text-xs leading-none'>
|
<span className='text-muted-foreground/60 text-xs leading-none'>
|
||||||
{log.is_stream ? t('Stream') : t('Non-stream')}
|
{log.is_stream ? t('Stream') : t('Non-stream')}
|
||||||
{tokensPerSecond != null && (
|
{tokensPerSecond != null && (
|
||||||
<>
|
<>
|
||||||
@@ -701,7 +707,9 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={<CircleAlert className='size-3 text-red-500' />}
|
render={
|
||||||
|
<CircleAlert className='text-destructive size-3' />
|
||||||
|
}
|
||||||
></TooltipTrigger>
|
></TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className='space-y-0.5 text-xs'>
|
<div className='space-y-0.5 text-xs'>
|
||||||
@@ -751,12 +759,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-0.5'>
|
<div className='flex flex-col gap-0.5'>
|
||||||
<span className='font-mono text-xs font-medium tabular-nums'>
|
<span className='text-xs font-medium tabular-nums'>
|
||||||
{promptTokens.toLocaleString()} /{' '}
|
{promptTokens.toLocaleString()} /{' '}
|
||||||
{completionTokens.toLocaleString()}
|
{completionTokens.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
{(cacheReadTokens > 0 || cacheWriteTokens > 0) && (
|
{(cacheReadTokens > 0 || cacheWriteTokens > 0) && (
|
||||||
<div className='flex items-center gap-1 text-[11px]'>
|
<div className='flex items-center gap-1 text-xs'>
|
||||||
{cacheReadTokens > 0 && (
|
{cacheReadTokens > 0 && (
|
||||||
<span className='text-muted-foreground/60'>
|
<span className='text-muted-foreground/60'>
|
||||||
{t('Cache')}↓ {cacheReadTokens.toLocaleString()}
|
{t('Cache')}↓ {cacheReadTokens.toLocaleString()}
|
||||||
@@ -815,7 +823,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-0.5'>
|
<div className='flex flex-col gap-0.5'>
|
||||||
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 [font-family:var(--font-body)] text-sm leading-none font-semibold tabular-nums'>
|
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 text-sm leading-none font-semibold tabular-nums'>
|
||||||
{quotaDisplay.prefix && (
|
{quotaDisplay.prefix && (
|
||||||
<span className='mr-1'>{quotaDisplay.prefix}</span>
|
<span className='mr-1'>{quotaDisplay.prefix}</span>
|
||||||
)}
|
)}
|
||||||
@@ -853,7 +861,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
primary.muted
|
primary.muted
|
||||||
? 'text-muted-foreground/60'
|
? 'text-muted-foreground/60'
|
||||||
: primary.danger
|
: primary.danger
|
||||||
? 'text-red-600 dark:text-red-400'
|
? 'text-destructive'
|
||||||
: 'text-foreground'
|
: 'text-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-2
@@ -95,7 +95,7 @@ export function useDrawingLogsColumns(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-w-0 flex-col gap-0.5'>
|
<div className='flex min-w-0 flex-col gap-0.5'>
|
||||||
<span className='truncate font-mono text-xs tabular-nums'>
|
<span className='truncate text-xs tabular-nums'>
|
||||||
{formatTimestampToDate(submitTime, 'milliseconds')}
|
{formatTimestampToDate(submitTime, 'milliseconds')}
|
||||||
</span>
|
</span>
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
@@ -151,7 +151,7 @@ export function useDrawingLogsColumns(
|
|||||||
copyText={mjId}
|
copyText={mjId}
|
||||||
variant='neutral'
|
variant='neutral'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='border-border/60 bg-muted/30 !text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
|
className='border-border/60 bg-muted/30 text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-6
@@ -102,15 +102,15 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-w-0 flex-col gap-0.5'>
|
<div className='flex min-w-0 flex-col gap-0.5'>
|
||||||
<span className='truncate font-mono text-xs tabular-nums'>
|
<span className='truncate text-xs tabular-nums'>
|
||||||
{formatTimestampToDate(submitTime, 'seconds')}
|
{formatTimestampToDate(submitTime, 'seconds')}
|
||||||
</span>
|
</span>
|
||||||
{log.finish_time ? (
|
{log.finish_time ? (
|
||||||
<span className='text-muted-foreground/60 truncate font-mono text-[11px] tabular-nums'>
|
<span className='text-muted-foreground/60 truncate text-xs tabular-nums'>
|
||||||
{formatTimestampToDate(log.finish_time, 'seconds')}
|
{formatTimestampToDate(log.finish_time, 'seconds')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className='text-muted-foreground/50 text-[11px]'>-</span>
|
<span className='text-muted-foreground/50 text-xs'>-</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -143,7 +143,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
|||||||
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[11px] font-semibold',
|
'text-xs font-semibold',
|
||||||
!sensitiveVisible && 'bg-muted text-muted-foreground'
|
!sensitiveVisible && 'bg-muted text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
@@ -179,9 +179,9 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
|||||||
copyText={taskId}
|
copyText={taskId}
|
||||||
variant='neutral'
|
variant='neutral'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='border-border/60 bg-muted/30 !text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
|
className='border-border/60 bg-muted/30 text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
|
||||||
/>
|
/>
|
||||||
<span className='text-muted-foreground/60 truncate text-[11px]'>
|
<span className='text-muted-foreground/60 truncate text-xs'>
|
||||||
{t(log.platform)} · {t(taskActionMapper.getLabel(log.action))}
|
{t(log.platform)} · {t(taskActionMapper.getLabel(log.action))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function StatBadge(props: {
|
|||||||
<span className='border-border/60 bg-muted/25 inline-flex h-7 items-center gap-2 rounded-md border px-2.5 text-xs shadow-xs'>
|
<span className='border-border/60 bg-muted/25 inline-flex h-7 items-center gap-2 rounded-md border px-2.5 text-xs shadow-xs'>
|
||||||
<span className={cn('h-3.5 w-0.5 rounded-full', props.accent)} />
|
<span className={cn('h-3.5 w-0.5 rounded-full', props.accent)} />
|
||||||
<span className='text-muted-foreground'>{props.label}</span>
|
<span className='text-muted-foreground'>{props.label}</span>
|
||||||
<span className='text-foreground/85 font-mono font-semibold tabular-nums'>
|
<span className='text-foreground/85 font-semibold tabular-nums'>
|
||||||
{props.value}
|
{props.value}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -91,17 +91,17 @@ export function CommonLogsStats() {
|
|||||||
<StatBadge
|
<StatBadge
|
||||||
label={t('Usage')}
|
label={t('Usage')}
|
||||||
value={sensitiveVisible ? formatLogQuota(stats?.quota || 0) : '••••'}
|
value={sensitiveVisible ? formatLogQuota(stats?.quota || 0) : '••••'}
|
||||||
accent='bg-sky-500/70'
|
accent='bg-chart-1/70'
|
||||||
/>
|
/>
|
||||||
<StatBadge
|
<StatBadge
|
||||||
label={t('RPM')}
|
label={t('RPM')}
|
||||||
value={stats?.rpm || 0}
|
value={stats?.rpm || 0}
|
||||||
accent='bg-rose-500/65'
|
accent='bg-chart-5/70'
|
||||||
/>
|
/>
|
||||||
<StatBadge
|
<StatBadge
|
||||||
label={t('TPM')}
|
label={t('TPM')}
|
||||||
value={stats?.tpm || 0}
|
value={stats?.tpm || 0}
|
||||||
accent='bg-slate-400/70'
|
accent='bg-muted-foreground/50'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ const CHANNEL_FIELD_LABELS: Record<string, string> = {
|
|||||||
function timingTextColorClass(
|
function timingTextColorClass(
|
||||||
variant: 'success' | 'warning' | 'danger'
|
variant: 'success' | 'warning' | 'danger'
|
||||||
): string {
|
): string {
|
||||||
if (variant === 'success') return 'text-emerald-600'
|
if (variant === 'success') return 'text-success'
|
||||||
if (variant === 'warning') return 'text-amber-600'
|
if (variant === 'warning') return 'text-warning'
|
||||||
return 'text-rose-600'
|
return 'text-destructive'
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailRow(props: {
|
function DetailRow(props: {
|
||||||
@@ -628,7 +628,10 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
label={t('IP Address')}
|
label={t('IP Address')}
|
||||||
value={
|
value={
|
||||||
<span className='flex items-center gap-1'>
|
<span className='flex items-center gap-1'>
|
||||||
<Globe className='size-3 text-amber-500' aria-hidden='true' />
|
<Globe
|
||||||
|
className='text-muted-foreground size-3'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
{props.log.ip}
|
{props.log.ip}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -814,7 +817,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{showLegacyTopupWarning && (
|
{showLegacyTopupWarning && (
|
||||||
<div className='flex items-start gap-1.5 text-xs text-amber-600 dark:text-amber-400'>
|
<div className='text-warning flex items-start gap-1.5 text-xs'>
|
||||||
<Info className='mt-0.5 size-3.5 shrink-0' aria-hidden='true' />
|
<Info className='mt-0.5 size-3.5 shrink-0' aria-hidden='true' />
|
||||||
<span>
|
<span>
|
||||||
{t(
|
{t(
|
||||||
@@ -1030,9 +1033,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
value={
|
value={
|
||||||
<span className='flex items-center gap-1'>
|
<span className='flex items-center gap-1'>
|
||||||
{other.admin_info.local_count_tokens ? (
|
{other.admin_info.local_count_tokens ? (
|
||||||
<Monitor className='size-3 text-blue-500' />
|
<Monitor className='text-muted-foreground size-3' />
|
||||||
) : (
|
) : (
|
||||||
<Cloud className='size-3 text-emerald-500' />
|
<Cloud className='text-muted-foreground size-3' />
|
||||||
)}
|
)}
|
||||||
<span className='text-xs'>
|
<span className='text-xs'>
|
||||||
{other.admin_info.local_count_tokens
|
{other.admin_info.local_count_tokens
|
||||||
@@ -1080,7 +1083,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
)}
|
)}
|
||||||
{Array.isArray(other.stream_status.errors) &&
|
{Array.isArray(other.stream_status.errors) &&
|
||||||
other.stream_status.errors.length > 0 && (
|
other.stream_status.errors.length > 0 && (
|
||||||
<pre className='bg-background/60 mt-1 max-h-32 overflow-y-auto rounded border p-2 font-mono text-[11px] leading-relaxed wrap-break-word whitespace-pre-wrap'>
|
<pre className='bg-background/60 mt-1 max-h-32 overflow-y-auto rounded border p-2 font-mono text-xs leading-relaxed wrap-break-word whitespace-pre-wrap'>
|
||||||
{other.stream_status.errors.join('\n')}
|
{other.stream_status.errors.join('\n')}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@@ -1155,7 +1158,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
className='shrink-0 font-medium'
|
className='shrink-0 font-medium'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
/>
|
/>
|
||||||
<span className='min-w-0 font-mono text-[11px] leading-relaxed break-all sm:wrap-break-word'>
|
<span className='min-w-0 font-mono text-xs leading-relaxed break-all sm:wrap-break-word'>
|
||||||
{parsed.content}
|
{parsed.content}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export function LogsFilterToolbar<TData>(props: LogsFilterToolbarProps<TData>) {
|
|||||||
>
|
>
|
||||||
{advancedOpen ? t('Collapse') : t('Expand')}
|
{advancedOpen ? t('Collapse') : t('Expand')}
|
||||||
{activeAdvancedCount > 0 && (
|
{activeAdvancedCount > 0 && (
|
||||||
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
|
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
|
||||||
{activeAdvancedCount}
|
{activeAdvancedCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -157,7 +157,7 @@ export function LogsFilterToolbar<TData>(props: LogsFilterToolbarProps<TData>) {
|
|||||||
>
|
>
|
||||||
{t('Filter')}
|
{t('Filter')}
|
||||||
{activeMobileFilterCount > 0 && (
|
{activeMobileFilterCount > 0 && (
|
||||||
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
|
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
|
||||||
{activeMobileFilterCount}
|
{activeMobileFilterCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ function ModelBadgeContent(props: ModelBadgeProps) {
|
|||||||
showDot={!provider}
|
showDot={!provider}
|
||||||
autoColor={provider ? undefined : props.modelName}
|
autoColor={provider ? undefined : props.modelName}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-border/60 bg-muted/30 h-6 max-w-none gap-1.5 rounded-md border px-2 [font-family:var(--font-body)]',
|
'border-border/60 bg-muted/30 h-6 max-w-none gap-1.5 rounded-md border px-2',
|
||||||
provider && 'text-foreground',
|
provider && 'text-foreground',
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,10 +41,8 @@ import { getLogTypeConfig } from '../lib/utils'
|
|||||||
import type { LogCategory } from '../types'
|
import type { LogCategory } from '../types'
|
||||||
|
|
||||||
const logTypeRowTint: Record<number, string> = {
|
const logTypeRowTint: Record<number, string> = {
|
||||||
[LOG_TYPE_ENUM.ERROR]:
|
[LOG_TYPE_ENUM.ERROR]: 'bg-destructive/5 border-destructive/20',
|
||||||
'bg-rose-50/40 dark:bg-rose-950/20 border-rose-200/50 dark:border-rose-900/30',
|
[LOG_TYPE_ENUM.REFUND]: 'bg-info/5 border-info/20',
|
||||||
[LOG_TYPE_ENUM.REFUND]:
|
|
||||||
'bg-blue-50/30 dark:bg-blue-950/15 border-blue-200/50 dark:border-blue-900/30',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UsageLogsMobileListProps<TData> {
|
interface UsageLogsMobileListProps<TData> {
|
||||||
@@ -129,7 +127,7 @@ function SummaryField<TData>({
|
|||||||
<div
|
<div
|
||||||
className={cn('bg-muted/20 min-w-0 rounded-md px-2 py-1.5', className)}
|
className={cn('bg-muted/20 min-w-0 rounded-md px-2 py-1.5', className)}
|
||||||
>
|
>
|
||||||
<div className='text-muted-foreground mb-1 text-[11px] leading-none font-medium select-none'>
|
<div className='text-muted-foreground mb-1 text-xs leading-none font-medium select-none'>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<CompactCell
|
<CompactCell
|
||||||
@@ -156,7 +154,7 @@ function MobileLogTimeStatus({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<div className='font-mono text-xs leading-tight tabular-nums'>
|
<div className='text-xs leading-tight tabular-nums'>
|
||||||
{formatTimestampToDate(timestamp)}
|
{formatTimestampToDate(timestamp)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -200,7 +198,7 @@ function CommonLogsCard<TData>({
|
|||||||
|
|
||||||
<div className='grid grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)] gap-1.5'>
|
<div className='grid grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)] gap-1.5'>
|
||||||
<div className='bg-muted/20 min-w-0 rounded-md px-2 py-1.5'>
|
<div className='bg-muted/20 min-w-0 rounded-md px-2 py-1.5'>
|
||||||
<div className='text-muted-foreground mb-1 text-[11px] leading-none font-medium select-none'>
|
<div className='text-muted-foreground mb-1 text-xs leading-none font-medium select-none'>
|
||||||
{t('Time')}
|
{t('Time')}
|
||||||
</div>
|
</div>
|
||||||
<MobileLogTimeStatus
|
<MobileLogTimeStatus
|
||||||
@@ -217,7 +215,7 @@ function CommonLogsCard<TData>({
|
|||||||
<SummaryField
|
<SummaryField
|
||||||
label={t('Token')}
|
label={t('Token')}
|
||||||
cell={cells.get('token_name')}
|
cell={cells.get('token_name')}
|
||||||
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-[11px] [&_.flex-col>*:not(:first-child)]:leading-none'
|
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-xs [&_.flex-col>*:not(:first-child)]:leading-none'
|
||||||
/>
|
/>
|
||||||
<SummaryField
|
<SummaryField
|
||||||
label={t('Timing')}
|
label={t('Timing')}
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ import { UsageLogsMobileList } from './usage-logs-mobile-card'
|
|||||||
const route = getRouteApi('/_authenticated/usage-logs/$section')
|
const route = getRouteApi('/_authenticated/usage-logs/$section')
|
||||||
|
|
||||||
const logTypeRowTint: Record<number, string> = {
|
const logTypeRowTint: Record<number, string> = {
|
||||||
[LOG_TYPE_ENUM.ERROR]: 'bg-rose-50/40 dark:bg-rose-950/20',
|
[LOG_TYPE_ENUM.ERROR]: 'bg-destructive/5',
|
||||||
[LOG_TYPE_ENUM.REFUND]: 'bg-blue-50/30 dark:bg-blue-950/15',
|
[LOG_TYPE_ENUM.REFUND]: 'bg-info/5',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning tint for logs where a quota conversion saturated (admin-only marker).
|
// Warning tint for logs where a quota conversion saturated (admin-only marker).
|
||||||
// Takes precedence over the per-type tint since it flags a billing anomaly.
|
// Takes precedence over the per-type tint since it flags a billing anomaly.
|
||||||
const quotaSaturationRowTint = 'bg-amber-50/60 dark:bg-amber-950/25'
|
const quotaSaturationRowTint = 'bg-warning/10'
|
||||||
|
|
||||||
function getColumnVisibilityStorageKey(
|
function getColumnVisibilityStorageKey(
|
||||||
logCategory: LogCategory,
|
logCategory: LogCategory,
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ import type { User } from '../types'
|
|||||||
import { DataTableRowActions } from './data-table-row-actions'
|
import { DataTableRowActions } from './data-table-row-actions'
|
||||||
|
|
||||||
function getQuotaProgressColor(percentage: number): string {
|
function getQuotaProgressColor(percentage: number): string {
|
||||||
if (percentage <= 10) return '[&_[data-slot=progress-indicator]]:bg-rose-500'
|
if (percentage <= 10)
|
||||||
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-amber-500'
|
return '[&_[data-slot=progress-indicator]]:bg-destructive'
|
||||||
return '[&_[data-slot=progress-indicator]]:bg-emerald-500'
|
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-warning'
|
||||||
|
return '[&_[data-slot=progress-indicator]]:bg-success'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUsersColumns(): ColumnDef<User>[] {
|
export function useUsersColumns(): ColumnDef<User>[] {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function AffiliateRewardsCard({
|
|||||||
[t('Invites'), String(user?.aff_count ?? 0)],
|
[t('Invites'), String(user?.aff_count ?? 0)],
|
||||||
].map(([label, value]) => (
|
].map(([label, value]) => (
|
||||||
<div key={label}>
|
<div key={label}>
|
||||||
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
|
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-0.5 truncate text-sm font-semibold tabular-nums'>
|
<div className='mt-0.5 truncate text-sm font-semibold tabular-nums'>
|
||||||
|
|||||||
@@ -50,9 +50,7 @@ export function TransferDialog({
|
|||||||
transferring,
|
transferring,
|
||||||
}: TransferDialogProps) {
|
}: TransferDialogProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const currencyConfig = useSystemConfigStore(
|
const currencyConfig = useSystemConfigStore((state) => state.config.currency)
|
||||||
(state) => state.config.currency
|
|
||||||
)
|
|
||||||
const minimumQuota = Math.ceil(
|
const minimumQuota = Math.ceil(
|
||||||
currencyConfig.quotaPerUnit > 0
|
currencyConfig.quotaPerUnit > 0
|
||||||
? currencyConfig.quotaPerUnit
|
? currencyConfig.quotaPerUnit
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ export function RechargeFormCard({
|
|||||||
{method.name}
|
{method.name}
|
||||||
</span>
|
</span>
|
||||||
{disabledLabel && (
|
{disabledLabel && (
|
||||||
<span className='text-muted-foreground max-w-full truncate text-[11px] leading-4 font-normal'>
|
<span className='text-muted-foreground max-w-full truncate text-xs leading-4 font-normal'>
|
||||||
{disabledLabel}
|
{disabledLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -435,7 +435,7 @@ export function RechargeFormCard({
|
|||||||
{method.name}
|
{method.name}
|
||||||
</span>
|
</span>
|
||||||
{disabledLabel && (
|
{disabledLabel && (
|
||||||
<span className='text-muted-foreground max-w-full truncate text-[11px] leading-4 font-normal'>
|
<span className='text-muted-foreground max-w-full truncate text-xs leading-4 font-normal'>
|
||||||
{disabledLabel}
|
{disabledLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-foreground mt-1.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:mt-2 sm:text-2xl'>
|
<div className='text-foreground mt-1.5 text-base font-bold tracking-tight break-all tabular-nums sm:mt-2 sm:text-2xl'>
|
||||||
{item.value}
|
{item.value}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
|
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
|
||||||
|
|||||||
Vendored
+16
-3
@@ -20,14 +20,14 @@ import i18n from 'i18next'
|
|||||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
import { initReactI18next } from 'react-i18next'
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
|
||||||
import { convertDetectedLanguage } from './languages'
|
import { convertDetectedLanguage, toIntlLocale } from './languages'
|
||||||
import en from './locales/en.json'
|
import en from './locales/en.json'
|
||||||
import fr from './locales/fr.json'
|
import fr from './locales/fr.json'
|
||||||
import ja from './locales/ja.json'
|
import ja from './locales/ja.json'
|
||||||
import ru from './locales/ru.json'
|
import ru from './locales/ru.json'
|
||||||
import vi from './locales/vi.json'
|
import vi from './locales/vi.json'
|
||||||
import zhCN from './locales/zh.json'
|
|
||||||
import zhTW from './locales/zh-TW.json'
|
import zhTW from './locales/zh-TW.json'
|
||||||
|
import zhCN from './locales/zh.json'
|
||||||
|
|
||||||
export const resources = {
|
export const resources = {
|
||||||
en,
|
en,
|
||||||
@@ -36,7 +36,7 @@ export const resources = {
|
|||||||
ru,
|
ru,
|
||||||
ja,
|
ja,
|
||||||
vi,
|
vi,
|
||||||
zhTW
|
zhTW,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
@@ -61,4 +61,17 @@ i18n
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Keep <html lang> in sync with the active UI language. Han unification means
|
||||||
|
// zh-CN / zh-TW / ja share code points but need different glyphs; without a
|
||||||
|
// correct lang attribute browsers guess (index.html ships lang="en") and CJK
|
||||||
|
// text renders with the wrong regional font.
|
||||||
|
function syncDocumentLanguage(language?: string) {
|
||||||
|
if (typeof document === 'undefined') return
|
||||||
|
const locale = toIntlLocale(language ?? i18n.resolvedLanguage)
|
||||||
|
if (locale) document.documentElement.lang = locale
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n.on('languageChanged', syncDocumentLanguage)
|
||||||
|
syncDocumentLanguage(i18n.resolvedLanguage)
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
|
|||||||
Vendored
+8
-3
@@ -24,7 +24,7 @@ export const INTERFACE_LANGUAGE_OPTIONS = [
|
|||||||
{ code: 'ru', label: 'Русский' },
|
{ code: 'ru', label: 'Русский' },
|
||||||
{ code: 'ja', label: '日本語' },
|
{ code: 'ja', label: '日本語' },
|
||||||
{ code: 'vi', label: 'Tiếng Việt' },
|
{ code: 'vi', label: 'Tiếng Việt' },
|
||||||
{ code: 'zhTW', label: '繁體中文' }
|
{ code: 'zhTW', label: '繁體中文' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type InterfaceLanguageCode =
|
export type InterfaceLanguageCode =
|
||||||
@@ -34,10 +34,15 @@ export function normalizeInterfaceLanguage(value?: string | null): string {
|
|||||||
if (!value) return 'en'
|
if (!value) return 'en'
|
||||||
|
|
||||||
let normalized = value.trim().replaceAll('_', '-').toLowerCase()
|
let normalized = value.trim().replaceAll('_', '-').toLowerCase()
|
||||||
if (value === 'zh-TW' || value === 'zh-HK' || value === 'zh-MO' || value === 'zhTW') {
|
if (
|
||||||
|
value === 'zh-TW' ||
|
||||||
|
value === 'zh-HK' ||
|
||||||
|
value === 'zh-MO' ||
|
||||||
|
value === 'zhTW'
|
||||||
|
) {
|
||||||
normalized = 'zhTW'
|
normalized = 'zhTW'
|
||||||
}
|
}
|
||||||
if (value === 'zh-CN' || value === 'zh-Hans' || value === "zhCN") {
|
if (value === 'zh-CN' || value === 'zh-Hans' || value === 'zhCN') {
|
||||||
normalized = 'zhCN'
|
normalized = 'zhCN'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+5
-9
@@ -163,17 +163,13 @@ const TAG_COLORS = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert string to a stable semantic color
|
* Convert string to a stable semantic color.
|
||||||
* Used for model tags, group badges, user avatars, etc.
|
* Only for avatar identity tinting (via `getAvatarColorClass`), where a subtle
|
||||||
* Same string always returns the same color
|
* per-user hue aids recognition. Do NOT use it to color badges/tags — text
|
||||||
|
* badges speak only the five semantic voices (see `components/status-badge`).
|
||||||
*
|
*
|
||||||
* @param str - Input string (model name, group name, username, etc.)
|
* @param str - Input string (username, etc.)
|
||||||
* @returns Semantic color name from TAG_COLORS
|
* @returns Semantic color name from TAG_COLORS
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* stringToColor('gpt-4') // 'blue'
|
|
||||||
* stringToColor('claude-3') // 'purple'
|
|
||||||
* stringToColor('default') // 'green'
|
|
||||||
*/
|
*/
|
||||||
export function stringToColor(str: string): SemanticColor {
|
export function stringToColor(str: string): SemanticColor {
|
||||||
let sum = 0
|
let sum = 0
|
||||||
|
|||||||
Vendored
+6
-72
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import type { Transition, Variants } from 'motion/react'
|
import type { Transition } from 'motion/react'
|
||||||
|
|
||||||
const EASE_OUT_CUBIC = [0.33, 1, 0.68, 1] as const
|
const EASE_OUT_CUBIC = [0.33, 1, 0.68, 1] as const
|
||||||
|
|
||||||
@@ -36,87 +36,21 @@ export const MOTION_TRANSITION: Record<string, Transition> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MOTION_VARIANTS = {
|
export const MOTION_VARIANTS = {
|
||||||
|
// Admin surfaces keep page transitions to a bare opacity fade: no vertical
|
||||||
|
// shift, no blur. Anything more reads as lag on high-frequency workflows.
|
||||||
pageEnter: {
|
pageEnter: {
|
||||||
initial: { opacity: 0, y: 8, filter: 'blur(4px)' },
|
initial: { opacity: 0 },
|
||||||
animate: { opacity: 1, y: 0, filter: 'blur(0px)' },
|
animate: { opacity: 1 },
|
||||||
exit: { opacity: 0, y: -4, filter: 'blur(2px)' },
|
exit: { opacity: 0 },
|
||||||
},
|
},
|
||||||
fadeIn: {
|
fadeIn: {
|
||||||
initial: { opacity: 0 },
|
initial: { opacity: 0 },
|
||||||
animate: { opacity: 1 },
|
animate: { opacity: 1 },
|
||||||
exit: { opacity: 0 },
|
exit: { opacity: 0 },
|
||||||
},
|
},
|
||||||
scaleIn: {
|
|
||||||
initial: { opacity: 0, scale: 0.96 },
|
|
||||||
animate: { opacity: 1, scale: 1 },
|
|
||||||
exit: { opacity: 0, scale: 0.96 },
|
|
||||||
},
|
|
||||||
slideUp: {
|
|
||||||
initial: { opacity: 0, y: 16 },
|
|
||||||
animate: { opacity: 1, y: 0 },
|
|
||||||
exit: { opacity: 0, y: 16 },
|
|
||||||
},
|
|
||||||
slideDown: {
|
|
||||||
initial: { opacity: 0, y: -16 },
|
|
||||||
animate: { opacity: 1, y: 0 },
|
|
||||||
exit: { opacity: 0, y: -16 },
|
|
||||||
},
|
|
||||||
tableRow: {
|
|
||||||
initial: { opacity: 0, y: 4 },
|
|
||||||
animate: { opacity: 1, y: 0 },
|
|
||||||
},
|
|
||||||
cardItem: {
|
|
||||||
initial: { opacity: 0, y: 12, scale: 0.98 },
|
|
||||||
animate: { opacity: 1, y: 0, scale: 1 },
|
|
||||||
},
|
|
||||||
sidebarSlide: {
|
sidebarSlide: {
|
||||||
initial: { opacity: 0, x: -8 },
|
initial: { opacity: 0, x: -8 },
|
||||||
animate: { opacity: 1, x: 0 },
|
animate: { opacity: 1, x: 0 },
|
||||||
exit: { opacity: 0, x: -8 },
|
exit: { opacity: 0, x: -8 },
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const STAGGER_VARIANTS: Variants = {
|
|
||||||
initial: {},
|
|
||||||
animate: { transition: { staggerChildren: 0.04 } },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const STAGGER_ITEM_VARIANTS: Variants = {
|
|
||||||
initial: { opacity: 0, y: 8 },
|
|
||||||
animate: { opacity: 1, y: 0, transition: MOTION_TRANSITION.default },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TABLE_STAGGER_VARIANTS: Variants = {
|
|
||||||
initial: {},
|
|
||||||
animate: { transition: { staggerChildren: 0.03 } },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TABLE_ROW_VARIANTS: Variants = {
|
|
||||||
initial: { opacity: 0, y: 4 },
|
|
||||||
animate: { opacity: 1, y: 0, transition: MOTION_TRANSITION.fast },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CARD_STAGGER_VARIANTS: Variants = {
|
|
||||||
initial: {},
|
|
||||||
animate: { transition: { staggerChildren: 0.05 } },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CARD_ITEM_VARIANTS: Variants = {
|
|
||||||
initial: { opacity: 0, y: 12, scale: 0.98 },
|
|
||||||
animate: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
scale: 1,
|
|
||||||
transition: MOTION_TRANSITION.default,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SIDEBAR_STAGGER_VARIANTS: Variants = {
|
|
||||||
initial: {},
|
|
||||||
animate: { transition: { staggerChildren: 0.03, delayChildren: 0.05 } },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SIDEBAR_ITEM_VARIANTS: Variants = {
|
|
||||||
initial: { opacity: 0, x: -8 },
|
|
||||||
animate: { opacity: 1, x: 0, transition: MOTION_TRANSITION.fast },
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
+3
-6
@@ -37,7 +37,6 @@ import { handleServerError } from '@/lib/handle-server-error'
|
|||||||
import { useAuthStore } from '@/stores/auth-store'
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
|
|
||||||
import { DirectionProvider } from './context/direction-provider'
|
import { DirectionProvider } from './context/direction-provider'
|
||||||
import { FontProvider } from './context/font-provider'
|
|
||||||
import { ThemeProvider } from './context/theme-provider'
|
import { ThemeProvider } from './context/theme-provider'
|
||||||
import './i18n/config'
|
import './i18n/config'
|
||||||
// Generated Routes
|
// Generated Routes
|
||||||
@@ -165,11 +164,9 @@ if (!rootElement.innerHTML) {
|
|||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<FontProvider>
|
<DirectionProvider>
|
||||||
<DirectionProvider>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
</DirectionProvider>
|
||||||
</DirectionProvider>
|
|
||||||
</FontProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|||||||
Vendored
+7
-75
@@ -396,8 +396,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
.animate-appear,
|
.animate-appear,
|
||||||
.animate-appear-zoom,
|
.animate-appear-zoom,
|
||||||
.animate-scroll-up,
|
.animate-scroll-up,
|
||||||
.animate-scroll-down,
|
.animate-scroll-down {
|
||||||
[data-slot='table'] tbody tr {
|
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
@@ -482,79 +481,12 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Micro-interactions — Vercel-style subtle hover/active feedback */
|
/* Micro-interactions — admin surfaces stay calm: color transitions only.
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
* No entrance animations, no hover lift/shadow, no press scaling. High-
|
||||||
[data-slot='card']:not([data-card-hover='false']) {
|
* frequency workflows (scanning tables, toggling filters) read as instant;
|
||||||
transition:
|
* decorative motion belongs to the landing page (landing-* utilities). */
|
||||||
transform 150ms ease,
|
[data-slot='table'] tr {
|
||||||
box-shadow 150ms ease;
|
transition: background-color 120ms ease;
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
|
||||||
[data-slot='card']:not([data-card-hover='false']):hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark [data-slot='card']:not([data-card-hover='false']):hover {
|
|
||||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button:not(:disabled):active,
|
|
||||||
[role='button']:not(:disabled):active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot='table'] tr {
|
|
||||||
transition: background-color 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table row stagger-in animation */
|
|
||||||
[data-slot='table'] tbody tr {
|
|
||||||
animation: tableRowEnter 0.2s cubic-bezier(0.33, 1, 0.68, 1) both;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(2) {
|
|
||||||
animation-delay: 25ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(3) {
|
|
||||||
animation-delay: 50ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(4) {
|
|
||||||
animation-delay: 75ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(5) {
|
|
||||||
animation-delay: 100ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(6) {
|
|
||||||
animation-delay: 125ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(7) {
|
|
||||||
animation-delay: 150ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(8) {
|
|
||||||
animation-delay: 175ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(9) {
|
|
||||||
animation-delay: 200ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(10) {
|
|
||||||
animation-delay: 225ms;
|
|
||||||
}
|
|
||||||
[data-slot='table'] tbody tr:nth-child(n + 11) {
|
|
||||||
animation-delay: 250ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes tableRowEnter {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(4px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabular numbers for data-heavy contexts */
|
/* Tabular numbers for data-heavy contexts */
|
||||||
|
|||||||
+2
-2
@@ -419,8 +419,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
* cards/borders rather than tinting them with the clay accent).
|
* cards/borders rather than tinting them with the clay accent).
|
||||||
* - `simple-large`: keeps intentionally neutral, high-contrast surfaces. */
|
* - `simple-large`: keeps intentionally neutral, high-contrast surfaces. */
|
||||||
[data-theme-preset]:not([data-theme-preset='default']):not(
|
[data-theme-preset]:not([data-theme-preset='default']):not(
|
||||||
[data-theme-preset='anthropic']
|
[data-theme-preset='anthropic']
|
||||||
):not([data-theme-preset='simple-large']) {
|
):not([data-theme-preset='simple-large']) {
|
||||||
--card: color-mix(in oklch, var(--primary) 3%, var(--background));
|
--card: color-mix(in oklch, var(--primary) 3%, var(--background));
|
||||||
--popover: color-mix(in oklch, var(--primary) 5%, var(--background));
|
--popover: color-mix(in oklch, var(--primary) 5%, var(--background));
|
||||||
--muted: color-mix(in oklch, var(--primary) 7%, var(--background));
|
--muted: color-mix(in oklch, var(--primary) 7%, var(--background));
|
||||||
|
|||||||
Vendored
+16
-8
@@ -19,7 +19,17 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: 'Public Sans', sans-serif;
|
/* Primary UI face. `@fontsource-variable/public-sans` registers the family
|
||||||
|
* as 'Public Sans Variable' (NOT 'Public Sans'), so that name must lead the
|
||||||
|
* stack or the webfont silently never applies. Public Sans carries no CJK
|
||||||
|
* glyphs, so explicit CJK UI fonts (PingFang / YaHei UI / Noto Sans CJK)
|
||||||
|
* sit BEFORE the western system fallbacks — otherwise Windows browsers pick
|
||||||
|
* a legacy serif-ish CJK default and Latin/CJK baselines drift apart. */
|
||||||
|
--font-sans:
|
||||||
|
'Public Sans Variable', 'Public Sans', -apple-system, BlinkMacSystemFont,
|
||||||
|
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei',
|
||||||
|
'Noto Sans SC', 'Noto Sans CJK SC', 'Segoe UI', 'Helvetica Neue', Arial,
|
||||||
|
sans-serif;
|
||||||
/* Editorial serif token — the body face used by the `serif` font axis
|
/* Editorial serif token — the body face used by the `serif` font axis
|
||||||
* and by presets that opt in (currently Anthropic). Lora carries only
|
* and by presets that opt in (currently Anthropic). Lora carries only
|
||||||
* Latin glyphs, so the stack walks through CJK serifs (Noto / Source Han
|
* Latin glyphs, so the stack walks through CJK serifs (Noto / Source Han
|
||||||
@@ -33,12 +43,6 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
'Songti SC', 'STSong', 'STSongti-SC-Regular', 'PingFang SC', 'SimSun',
|
'Songti SC', 'STSong', 'STSongti-SC-Regular', 'PingFang SC', 'SimSun',
|
||||||
'NSimSun', '宋体', 'FangSong', '仿宋', 'KaiTi', '楷体', Georgia,
|
'NSimSun', '宋体', 'FangSong', '仿宋', 'KaiTi', '楷体', Georgia,
|
||||||
'Times New Roman', Cambria, 'Liberation Serif', serif;
|
'Times New Roman', Cambria, 'Liberation Serif', serif;
|
||||||
--font-inter:
|
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
|
||||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
--font-manrope:
|
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
|
||||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
|
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
@@ -91,7 +95,11 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 1rem;
|
/* Single radius source. 0.625rem (10px) is the shadcn default for admin
|
||||||
|
* density; larger values ripple through the derived scale above and make
|
||||||
|
* data-dense tables/cards read "bubbly". User can still pick rounder axes
|
||||||
|
* via data-theme-radius. */
|
||||||
|
--radius: 0.625rem;
|
||||||
--app-header-height: 3rem;
|
--app-header-height: 3rem;
|
||||||
/* Static build-channel fallback consumed when JS hasn't booted yet. */
|
/* Static build-channel fallback consumed when JS hasn't booted yet. */
|
||||||
--app-rev: '2k6e8r7p';
|
--app-rev: '2k6e8r7p';
|
||||||
|
|||||||
Reference in New Issue
Block a user