Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog/unreleased/model-cooldowns-and-bulk-checkin-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
### English

- Add a model-cooldown button to every account card. It opens an inline list of models currently in cooldown with a live remaining-time countdown, and shows an empty state when nothing is cooling down.

- Show per-account check-in feedback after a one-click "check in all" run, not only after a single-account check-in. Each card reports success, already checked in, or the raw upstream error with a copy button.

- Remove `baseUrl` and `ignoreDeprecations` from the frontend TypeScript config so the console type-checks under both the workspace TypeScript 6.0 and the globally installed 5.9.

### 中文

- 每个账号卡片新增「模型冷却」按钮,点开可在卡片内查看当前处于冷却中的模型及实时剩余时间;没有模型冷却时显示空状态。

- 一键签到后每个账号卡片也会显示签到反馈(成功 / 已签到 / 失败并展示上游原始错误及复制按钮),此前只有单账号签到才有反馈。

- 移除前端 TypeScript 配置中的 `baseUrl` 与 `ignoreDeprecations`,让工作区 TypeScript 6.0 和全局 5.9 都能通过类型检查。

56 changes: 44 additions & 12 deletions frontend/src/components/account/AccountCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ListBullets,
PencilSimple,
ShieldCheck,
Timer,
TrashSimple,
WarningCircle,
XCircle,
Expand Down Expand Up @@ -115,6 +116,7 @@ export function AccountCard({
const chipRef = useRef<HTMLSpanElement>(null)
const authRef = useRef<HTMLElement>(null)
const [authCopied, setAuthCopied] = useState(false)
const [cooldownsOpen, setCooldownsOpen] = useState(false)
const lastStateRef = useRef<string | null>(null)
const [, refreshCooldowns] = useState(0)
const state = accountState(account)
Expand All @@ -131,18 +133,19 @@ export function AccountCard({
// Transient check-in feedback: a green check / yellow circle / red cross row
// that the page pushes after a manual check-in. It shows for three seconds
// and then disappears, regardless of whether the check-in succeeded.
const [feedback, setFeedback] = useState<CheckinFeedback | null>(null)
const [feedbackCopied, setFeedbackCopied] = useState(false)
// The page creates a fresh object per check-in, so reference equality is
// enough to know which notice has been dismissed. Both the visible notice
// and the copied flag are derived during render; only the timer callback
// and user events update state (never a synchronous setState in an effect).
const [dismissed, setDismissed] = useState<CheckinFeedback | null>(null)
const [copiedKey, setCopiedKey] = useState<CheckinFeedback | null>(null)
const feedbackTimer = useRef<number | null>(null)
const feedback: CheckinFeedback | null = checkinFeedback && checkinFeedback !== dismissed ? checkinFeedback : null
const feedbackCopied = Boolean(feedback) && copiedKey === feedback
useEffect(() => {
if (!checkinFeedback) {
setFeedback(null)
return
}
setFeedback(checkinFeedback)
setFeedbackCopied(false)
if (!checkinFeedback) return
if (feedbackTimer.current) window.clearTimeout(feedbackTimer.current)
feedbackTimer.current = window.setTimeout(() => setFeedback(null), 3000)
feedbackTimer.current = window.setTimeout(() => setDismissed(checkinFeedback), 3000)
return () => {
if (feedbackTimer.current) window.clearTimeout(feedbackTimer.current)
}
Expand Down Expand Up @@ -342,9 +345,9 @@ export function AccountCard({
className="h-6 min-w-0 gap-1 px-1.5 text-caption"
onPress={() => {
void copyText(feedback.message || '').then((ok) => {
if (!ok) return
setFeedbackCopied(true)
window.setTimeout(() => setFeedbackCopied(false), 1100)
if (!ok || !feedback) return
setCopiedKey(feedback)
window.setTimeout(() => setCopiedKey(null), 1100)
})
}}
>
Expand Down Expand Up @@ -502,6 +505,14 @@ export function AccountCard({
<Tooltip.Content>{t('checkinNow')}</Tooltip.Content>
</Tooltip>
) : null}
<Tooltip>
<Tooltip.Trigger>
<Button isIconOnly size="sm" variant={cooldownsOpen ? 'secondary' : 'ghost'} onPress={() => setCooldownsOpen((open) => !open)} aria-label={t('modelCooldowns')}>
<Timer size={15} className={modelCooldowns.length ? 'text-warning' : undefined} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>{t('modelCooldowns')}{modelCooldowns.length ? ` · ${modelCooldowns.length}` : ''}</Tooltip.Content>
</Tooltip>
<Dropdown>
<Dropdown.Trigger>
<Button isIconOnly size="sm" variant="secondary" aria-label={t('more')}>
Expand Down Expand Up @@ -533,6 +544,27 @@ export function AccountCard({
/>
</div>
</Card.Footer>

{cooldownsOpen ? (
<section className="border-t border-separator bg-surface-secondary/55 px-4 py-2.5">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="text-caption font-medium text-muted">{t('modelCooldownsTitle')}</span>
<span className="mono text-caption text-muted">{modelCooldowns.length}</span>
</div>
{modelCooldowns.length ? (
<div className="space-y-1">
{modelCooldowns.map(([model, until]) => (
<div key={model} className="flex items-center justify-between gap-3 rounded-lg bg-surface px-2 py-1.5 text-caption">
<span className="mono min-w-0 truncate" title={model}>{model}</span>
<span className="mono shrink-0 text-warning">{cooldownLabel(until)}</span>
</div>
))}
</div>
) : (
<p className="text-caption text-muted">{t('modelCooldownsEmpty')}</p>
)}
</section>
) : null}
</Card>
)
}
6 changes: 6 additions & 0 deletions frontend/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,9 @@ export const messages: Record<Lang, Dict> = {
clearFilters: 'Clear filters',
more: 'More actions',
partialCooling: 'Partial model cooldown',
modelCooldowns: 'Model cooldowns',
modelCooldownsTitle: 'Models in cooldown',
modelCooldownsEmpty: 'No models are in cooldown.',
runtimeState: 'Runtime state',
maxInflight: 'Max concurrency',
priority: 'Priority',
Expand Down Expand Up @@ -1602,6 +1605,9 @@ export const messages: Record<Lang, Dict> = {
clearFilters: '清除筛选',
more: '更多操作',
partialCooling: '部分模型冷却',
modelCooldowns: '模型冷却',
modelCooldownsTitle: '冷却中的模型',
modelCooldownsEmpty: '当前没有模型在冷却。',
runtimeState: '运行状态',
maxInflight: '最大并发',
priority: '优先级',
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/pages/AccountsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Plus,
UploadSimple,
} from '@phosphor-icons/react'
import { AccountCard, type AccountBusyKind } from '@/components/account/AccountCard'
import { AccountCard, type AccountBusyKind, type CheckinFeedback } from '@/components/account/AccountCard'
import { AccountCheckinRecordsModal } from '@/components/account/AccountCheckinRecordsModal'
import { AccountModelsModal } from '@/components/account/AccountModelsModal'
import { EditAccountModal } from '@/components/account/EditAccountModal'
Expand Down Expand Up @@ -83,7 +83,7 @@ export function AccountsPage() {
const [patById, setPatById] = useState<Record<string, string>>({})
const [callbackById, setCallbackById] = useState<Record<string, string>>({})
const [noteById, setNoteById] = useState<Record<string, string>>({})
const [checkinFeedbackById, setCheckinFeedbackById] = useState<Record<string, import('@/components/account/AccountCard').CheckinFeedback | null>>({})
const [checkinFeedbackById, setCheckinFeedbackById] = useState<Record<string, CheckinFeedback>>({})
const [urlById, setUrlById] = useState<Record<string, string>>({})
const [confirmId, setConfirmId] = useState<string | null>(null)
const [modelsId, setModelsId] = useState<string | null>(null)
Expand Down Expand Up @@ -491,10 +491,21 @@ export function AccountsPage() {
await new Promise((resolve) => setTimeout(resolve, 4000))
}
try {
await checkinAccount(pending[index].id)
const result = await checkinAccount(pending[index].id)
summary.checked += 1
} catch {
const status = result?.last_checkin_status
const message = result?.last_checkin_msg
const kind = status === 'already' ? 'already' : status === 'error' ? 'error' : 'success'
setCheckinFeedbackById((current) => ({
...current,
[pending[index].id]: { kind, message: kind === 'error' ? message || t('checkinRecordFailed') : message },
}))
} catch (error) {
summary.failed += 1
setCheckinFeedbackById((current) => ({
...current,
[pending[index].id]: { kind: 'error', message: error instanceof Error ? error.message : String(error) },
}))
}
if (index < pending.length - 1) {
toast({
Expand Down
2 changes: 0 additions & 2 deletions frontend/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"compilerOptions": {
"baseUrl": ".",
"ignoreDeprecations": "6.0",
"paths": {
"@/*": ["./src/*"]
},
Expand Down
Loading
Loading