From 56bbd4ba96a9b0c74cf54bb076ef1c97a4e0d80f Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:12:29 +0800 Subject: [PATCH] feat: fix video generation API and pagination controls, update configuration for upscale timing, and enhance UI for token management --- _public/static/admin/css/cache.css | 150 ++ _public/static/admin/css/token.css | 137 +- _public/static/admin/js/cache.js | 452 ++++- _public/static/admin/js/config.js | 9 +- _public/static/admin/js/token.js | 91 + _public/static/admin/pages/cache.html | 134 +- _public/static/admin/pages/config.html | 12 +- _public/static/admin/pages/login.html | 10 +- _public/static/admin/pages/token.html | 111 +- _public/static/common/js/footer.js | 2 +- _public/static/common/js/function-header.js | 2 +- _public/static/common/js/header.js | 2 +- _public/static/function/js/video.js | 25 + _public/static/function/pages/chat.html | 12 +- _public/static/function/pages/imagine.html | 12 +- _public/static/function/pages/login.html | 10 +- _public/static/function/pages/video.html | 12 +- _public/static/function/pages/voice.html | 12 +- _public/static/i18n/locales/en.json | 12 +- _public/static/i18n/locales/zh.json | 12 +- app/core/exceptions.py | 12 +- app/services/grok/services/video.py | 1672 +++++++++++-------- config.defaults.toml | 4 + docs/README.en.md | 52 +- pyproject.toml | 2 +- readme.md | 52 +- uv.lock | 2 +- 27 files changed, 2128 insertions(+), 887 deletions(-) diff --git a/_public/static/admin/css/cache.css b/_public/static/admin/css/cache.css index 2b3729533..99dc0557d 100644 --- a/_public/static/admin/css/cache.css +++ b/_public/static/admin/css/cache.css @@ -73,6 +73,156 @@ border: 1px solid var(--border); } +.cache-pagination { + margin: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + background: linear-gradient(180deg, rgba(250, 250, 251, 0.9) 0%, rgba(255, 255, 255, 0.95) 100%); +} + +.cache-table-shell { + border-radius: 10px; + overflow: hidden; + background: #fff; +} + +.cache-pagination-left { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.cache-pagination-info { + font-size: 12px; + color: var(--accents-5); + white-space: nowrap; +} + +.cache-pagination-right { + display: inline-flex; + align-items: center; + gap: 8px; + justify-content: flex-end; +} + +.cache-pagination-icon-btn { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + background: transparent; + color: var(--accents-5); + border-radius: 6px; + cursor: pointer; + transition: color 0.15s, background 0.15s; +} + +.cache-pagination-icon-btn:hover:not(:disabled) { + color: var(--accents-7); + background: #f3f4f6; +} + +.cache-pagination-icon-btn:disabled { + color: var(--accents-5); + opacity: 0.5; + cursor: not-allowed; + background: transparent; +} + +.cache-select-menu-wrap { + position: relative; +} + +.cache-pagination-action-btn { + height: 28px; + padding: 0 10px; + display: inline-flex; + align-items: center; + gap: 4px; + border: 1px solid var(--border); + border-radius: 8px; + background: #fff; + color: var(--accents-6); + font-size: 12px; + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} + +.cache-pagination-action-btn:hover { + border-color: var(--accents-3); + color: var(--accents-7); + background: #fafafa; +} + +.cache-pagination-action-btn.is-active { + border-color: #111827; + color: #111827; +} + +.cache-select-all-popover { + position: absolute; + right: 0; + top: calc(100% + 6px); + min-width: 116px; + border: 1px solid var(--border); + border-radius: 10px; + background: #fff; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); + padding: 4px; + z-index: 30; +} + +.cache-select-all-popover.hidden { + display: none; +} + +.cache-select-all-option { + width: 100%; + height: 30px; + border: 0; + border-radius: 7px; + background: transparent; + text-align: left; + padding: 0 8px; + font-size: 12px; + color: var(--accents-6); + cursor: pointer; +} + +.cache-select-all-option:hover { + background: #f3f4f6; + color: var(--accents-7); +} + +.cache-pagination-select { + min-width: 88px; + height: 28px; + border: 1px solid var(--border); + border-radius: 8px; + background: #fff; + font-size: 12px; + padding: 0 8px; + color: var(--accents-6); +} + +@media (max-width: 900px) { + .cache-pagination { + flex-wrap: wrap; + row-gap: 8px; + } + + .cache-pagination-right { + margin-left: auto; + flex-wrap: wrap; + } +} + #confirm-dialog.confirm-dialog { border: none; border-radius: 12px; diff --git a/_public/static/admin/css/token.css b/_public/static/admin/css/token.css index 8bf11b84f..b1fac4376 100644 --- a/_public/static/admin/css/token.css +++ b/_public/static/admin/css/token.css @@ -71,7 +71,142 @@ align-items: center; justify-content: space-between; gap: 12px; - padding: 8px 4px; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + background: linear-gradient(180deg, rgba(250, 250, 251, 0.9) 0%, rgba(255, 255, 255, 0.95) 100%); + } + + .pagination-left { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + } + + .pagination-right { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + } + + .pagination-info { + font-size: 12px; + color: var(--accents-5); + white-space: nowrap; + } + + .pagination-icon-btn { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + background: transparent; + color: var(--accents-5); + border-radius: 6px; + cursor: pointer; + transition: color 0.15s, background 0.15s; + } + + .pagination-icon-btn:hover:not(:disabled) { + color: var(--accents-7); + background: #f3f4f6; + } + + .pagination-icon-btn:disabled { + color: var(--accents-3); + cursor: not-allowed; + background: transparent; + } + + .select-menu-wrap { + position: relative; + } + + .pagination-action-btn { + height: 28px; + padding: 0 10px; + display: inline-flex; + align-items: center; + gap: 4px; + border: 1px solid var(--border); + border-radius: 8px; + background: #fff; + color: var(--accents-6); + font-size: 12px; + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; + } + + .pagination-action-btn:hover { + border-color: var(--accents-3); + color: var(--accents-7); + background: #fafafa; + } + + .pagination-action-btn.is-active { + border-color: #111827; + color: #111827; + } + + .select-all-popover { + position: absolute; + right: 0; + top: calc(100% + 6px); + min-width: 116px; + border: 1px solid var(--border); + border-radius: 10px; + background: #fff; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); + padding: 4px; + z-index: 30; + } + + .select-all-popover.hidden { + display: none; + } + + .select-all-option { + width: 100%; + height: 30px; + border: 0; + border-radius: 7px; + background: transparent; + text-align: left; + padding: 0 8px; + font-size: 12px; + color: var(--accents-6); + cursor: pointer; + } + + .select-all-option:hover { + background: #f3f4f6; + color: var(--accents-7); + } + + .pagination-size-select { + min-width: 88px; + height: 28px; + border: 1px solid var(--border); + border-radius: 8px; + background: #fff; + font-size: 12px; + padding: 0 8px; + color: var(--accents-6); + } + + @media (max-width: 900px) { + .pagination-bar { + flex-wrap: wrap; + row-gap: 8px; + } + + .pagination-right { + margin-left: auto; + flex-wrap: wrap; + } } /* Elegant Badges */ diff --git a/_public/static/admin/js/cache.js b/_public/static/admin/js/cache.js index 3b0ffc301..ca4f38739 100644 --- a/_public/static/admin/js/cache.js +++ b/_public/static/admin/js/cache.js @@ -8,6 +8,8 @@ const selectedLocal = { image: new Set(), video: new Set() }; +const LOCAL_PAGE_SIZE_OPTIONS = [50, 100, 200, 500]; +const LOCAL_PAGE_SIZE_DEFAULT = 100; const ui = {}; const byId = (id) => document.getElementById(id); const loadFailed = new Map(); @@ -16,8 +18,24 @@ let currentBatchAction = null; let lastBatchAction = null; let isLocalDeleting = false; const cacheListState = { - image: { loaded: false, visible: false, items: [] }, - video: { loaded: false, visible: false, items: [] } + image: { + loaded: false, + visible: false, + items: [], + total: 0, + page: 1, + pageSize: LOCAL_PAGE_SIZE_DEFAULT, + loading: false + }, + video: { + loaded: false, + visible: false, + items: [], + total: 0, + page: 1, + pageSize: LOCAL_PAGE_SIZE_DEFAULT, + loading: false + } }; const UI_MAP = { imgCount: 'img-count', @@ -30,8 +48,6 @@ const UI_MAP = { accountTableBody: 'account-table-body', accountEmpty: 'account-empty', selectAll: 'select-all', - localImageSelectAll: 'local-image-select-all', - localVideoSelectAll: 'local-video-select-all', selectedCount: 'selected-count', batchActions: 'batch-actions', loadBtn: 'btn-load-stats', @@ -41,6 +57,28 @@ const UI_MAP = { localVideoList: 'local-video-list', localImageBody: 'local-image-body', localVideoBody: 'local-video-body', + localImagePrev: 'local-image-prev', + localImageNext: 'local-image-next', + localImagePageInfo: 'local-image-page-info', + localImagePageSize: 'local-image-page-size', + localImageSelectWrap: 'local-image-select-wrap', + localImageSelectTrigger: 'local-image-select-trigger', + localImageSelectLabel: 'local-image-select-label', + localImageSelectCaret: 'local-image-select-caret', + localImageSelectPopover: 'local-image-select-popover', + localImageSelectPage: 'local-image-select-page', + localImageSelectAllBtn: 'local-image-select-all', + localVideoPrev: 'local-video-prev', + localVideoNext: 'local-video-next', + localVideoPageInfo: 'local-video-page-info', + localVideoPageSize: 'local-video-page-size', + localVideoSelectWrap: 'local-video-select-wrap', + localVideoSelectTrigger: 'local-video-select-trigger', + localVideoSelectLabel: 'local-video-select-label', + localVideoSelectCaret: 'local-video-select-caret', + localVideoSelectPopover: 'local-video-select-popover', + localVideoSelectPage: 'local-video-select-page', + localVideoSelectAllBtn: 'local-video-select-all', onlineAssetsTable: 'online-assets-table', batchProgress: 'batch-progress', batchProgressText: 'batch-progress-text', @@ -87,6 +125,7 @@ async function init() { apiKey = await ensureAdminKey(); if (apiKey === null) return; cacheUI(); + setupLocalPaginationControls(); setupCacheCards(); setupConfirmDialog(); setupFailureDialog(); @@ -112,6 +151,315 @@ function cacheUI() { ui.cacheCards = document.querySelectorAll('.cache-card'); } +function getLocalState(type) { + return cacheListState[type] || null; +} + +function getLocalPaginationRefs(type) { + if (type === 'image') { + return { + prev: ui.localImagePrev, + next: ui.localImageNext, + info: ui.localImagePageInfo, + size: ui.localImagePageSize, + wrap: ui.localImageSelectWrap, + trigger: ui.localImageSelectTrigger, + label: ui.localImageSelectLabel, + caret: ui.localImageSelectCaret, + popover: ui.localImageSelectPopover, + selectPage: ui.localImageSelectPage, + selectAll: ui.localImageSelectAllBtn + }; + } + return { + prev: ui.localVideoPrev, + next: ui.localVideoNext, + info: ui.localVideoPageInfo, + size: ui.localVideoPageSize, + wrap: ui.localVideoSelectWrap, + trigger: ui.localVideoSelectTrigger, + label: ui.localVideoSelectLabel, + caret: ui.localVideoSelectCaret, + popover: ui.localVideoSelectPopover, + selectPage: ui.localVideoSelectPage, + selectAll: ui.localVideoSelectAllBtn + }; +} + +function setupPageSizeOptions(select, selectedValue) { + if (!select) return; + const value = Number(selectedValue) || LOCAL_PAGE_SIZE_DEFAULT; + select.innerHTML = ''; + LOCAL_PAGE_SIZE_OPTIONS.forEach(size => { + const option = document.createElement('option'); + option.value = String(size); + option.textContent = t('cache.perPage', { size }); + option.selected = size === value; + select.appendChild(option); + }); +} + +function updateLocalPaginationUI(type) { + const state = getLocalState(type); + if (!state) return; + const refs = getLocalPaginationRefs(type); + const total = Math.max(0, Number(state.total) || 0); + const pageSize = Math.max(1, Number(state.pageSize) || LOCAL_PAGE_SIZE_DEFAULT); + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + const page = Math.min(Math.max(1, Number(state.page) || 1), totalPages); + state.page = page; + state.pageSize = pageSize; + + if (refs.info) { + refs.info.textContent = t('cache.pagination', { + current: page, + total: totalPages, + count: total + }); + } + + if (refs.prev) refs.prev.disabled = state.loading || page <= 1; + if (refs.next) refs.next.disabled = state.loading || page >= totalPages; + if (refs.size && String(refs.size.value) !== String(pageSize)) { + setupPageSizeOptions(refs.size, pageSize); + } +} + +function closeLocalSelectMenu(type) { + const refs = getLocalPaginationRefs(type); + if (refs.popover) refs.popover.classList.add('hidden'); +} + +function closeAllLocalSelectMenus() { + closeLocalSelectMenu('image'); + closeLocalSelectMenu('video'); +} + +function isLocalSelectMenuOpen(type) { + const refs = getLocalPaginationRefs(type); + return !!(refs.popover && !refs.popover.classList.contains('hidden')); +} + +function refreshLocalSelectControl(type) { + const refs = getLocalPaginationRefs(type); + const selectedCount = selectedLocal[type]?.size || 0; + if (refs.label) { + refs.label.textContent = selectedCount > 0 + ? t('cache.clearSelection') + : t('common.selectAll'); + } + if (refs.trigger) { + refs.trigger.classList.toggle('is-active', selectedCount > 0); + } + if (refs.caret) { + refs.caret.style.display = selectedCount > 0 ? 'none' : 'inline'; + } +} + +function selectLocalPage(type) { + const set = selectedLocal[type]; + if (!set) return; + const items = cacheListState[type]?.items || []; + items.forEach(item => { + if (item && item.name) set.add(item.name); + }); + syncLocalRowCheckboxes(type); + updateSelectedCount(); + closeLocalSelectMenu(type); +} + +async function fetchAllLocalNames(type) { + const names = []; + let page = 1; + const pageSize = 1000; + let total = 0; + + while (true) { + const params = new URLSearchParams({ + type, + page: String(page), + page_size: String(pageSize) + }); + const res = await fetch(`/v1/admin/cache/list?${params.toString()}`, { + headers: buildAuthHeaders(apiKey) + }); + if (!res.ok) { + throw new Error(t('common.loadFailed')); + } + const data = await res.json(); + const items = Array.isArray(data.items) ? data.items : []; + total = Math.max(total, Number(data.total) || 0); + items.forEach(item => { + if (item && item.name) names.push(item.name); + }); + if (names.length >= total || items.length < pageSize) break; + page += 1; + } + return names; +} + +async function selectLocalAll(type) { + try { + const names = await fetchAllLocalNames(type); + const set = selectedLocal[type]; + if (!set) return; + set.clear(); + names.forEach(name => set.add(name)); + syncLocalRowCheckboxes(type); + updateSelectedCount(); + } catch (e) { + showToast(t('common.requestFailed'), 'error'); + } finally { + closeLocalSelectMenu(type); + } +} + +function clearLocalSelection(type) { + const set = selectedLocal[type]; + if (!set) return; + if (set.size === 0) { + closeLocalSelectMenu(type); + return; + } + set.clear(); + syncLocalRowCheckboxes(type); + updateSelectedCount(); + closeLocalSelectMenu(type); +} + +function setupLocalPaginationControls() { + const imageState = getLocalState('image'); + const videoState = getLocalState('video'); + setupPageSizeOptions(ui.localImagePageSize, imageState?.pageSize); + setupPageSizeOptions(ui.localVideoPageSize, videoState?.pageSize); + + if (ui.localImagePrev) { + ui.localImagePrev.addEventListener('click', () => { + const state = getLocalState('image'); + if (!state || state.loading) return; + if (state.page <= 1) return; + closeAllLocalSelectMenus(); + loadLocalCacheList('image', { page: state.page - 1 }); + }); + } + if (ui.localImageNext) { + ui.localImageNext.addEventListener('click', () => { + const state = getLocalState('image'); + if (!state || state.loading) return; + const totalPages = Math.max(1, Math.ceil((state.total || 0) / state.pageSize)); + if (state.page >= totalPages) return; + closeAllLocalSelectMenus(); + loadLocalCacheList('image', { page: state.page + 1 }); + }); + } + if (ui.localVideoPrev) { + ui.localVideoPrev.addEventListener('click', () => { + const state = getLocalState('video'); + if (!state || state.loading) return; + if (state.page <= 1) return; + closeAllLocalSelectMenus(); + loadLocalCacheList('video', { page: state.page - 1 }); + }); + } + if (ui.localVideoNext) { + ui.localVideoNext.addEventListener('click', () => { + const state = getLocalState('video'); + if (!state || state.loading) return; + const totalPages = Math.max(1, Math.ceil((state.total || 0) / state.pageSize)); + if (state.page >= totalPages) return; + closeAllLocalSelectMenus(); + loadLocalCacheList('video', { page: state.page + 1 }); + }); + } + + if (ui.localImagePageSize) { + ui.localImagePageSize.addEventListener('change', (event) => { + const state = getLocalState('image'); + if (!state) return; + const size = parseInt(event.target.value, 10); + if (!Number.isFinite(size) || size <= 0) return; + state.pageSize = size; + state.page = 1; + closeAllLocalSelectMenus(); + loadLocalCacheList('image', { page: 1, pageSize: size }); + }); + } + if (ui.localVideoPageSize) { + ui.localVideoPageSize.addEventListener('change', (event) => { + const state = getLocalState('video'); + if (!state) return; + const size = parseInt(event.target.value, 10); + if (!Number.isFinite(size) || size <= 0) return; + state.pageSize = size; + state.page = 1; + closeAllLocalSelectMenus(); + loadLocalCacheList('video', { page: 1, pageSize: size }); + }); + } + + if (ui.localImageSelectTrigger) { + ui.localImageSelectTrigger.addEventListener('click', (event) => { + event.stopPropagation(); + if ((selectedLocal.image?.size || 0) > 0) { + clearLocalSelection('image'); + return; + } + if (isLocalSelectMenuOpen('image')) closeLocalSelectMenu('image'); + else { + closeLocalSelectMenu('video'); + const refs = getLocalPaginationRefs('image'); + if (refs.popover) refs.popover.classList.remove('hidden'); + } + }); + } + if (ui.localVideoSelectTrigger) { + ui.localVideoSelectTrigger.addEventListener('click', (event) => { + event.stopPropagation(); + if ((selectedLocal.video?.size || 0) > 0) { + clearLocalSelection('video'); + return; + } + if (isLocalSelectMenuOpen('video')) closeLocalSelectMenu('video'); + else { + closeLocalSelectMenu('image'); + const refs = getLocalPaginationRefs('video'); + if (refs.popover) refs.popover.classList.remove('hidden'); + } + }); + } + + if (ui.localImageSelectPage) { + ui.localImageSelectPage.addEventListener('click', () => selectLocalPage('image')); + } + if (ui.localImageSelectAllBtn) { + ui.localImageSelectAllBtn.addEventListener('click', () => selectLocalAll('image')); + } + if (ui.localVideoSelectPage) { + ui.localVideoSelectPage.addEventListener('click', () => selectLocalPage('video')); + } + if (ui.localVideoSelectAllBtn) { + ui.localVideoSelectAllBtn.addEventListener('click', () => selectLocalAll('video')); + } + + document.addEventListener('click', (event) => { + const imageWrap = ui.localImageSelectWrap; + const videoWrap = ui.localVideoSelectWrap; + if (imageWrap && imageWrap.contains(event.target)) return; + if (videoWrap && videoWrap.contains(event.target)) return; + closeAllLocalSelectMenus(); + }); + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + closeAllLocalSelectMenus(); + } + }); + + updateLocalPaginationUI('image'); + updateLocalPaginationUI('video'); + refreshLocalSelectControl('image'); + refreshLocalSelectControl('video'); +} + function ensureUI() { if (!ui.batchActions) cacheUI(); } @@ -461,13 +809,17 @@ async function clearCache(type) { const state = cacheListState[type]; if (state) { state.items = []; + state.total = 0; + state.page = 1; state.loaded = true; + state.loading = false; } if (selectedLocal[type]) selectedLocal[type].clear(); if (state && state.visible) { renderLocalCacheList(type, []); } else { syncLocalSelectAllState(type); + updateLocalPaginationUI(type); updateSelectedCount(); } loadStats(); @@ -520,18 +872,11 @@ function toggleLocalSelect(type, name, checkbox) { } function toggleLocalSelectAll(type, checkbox) { - const set = selectedLocal[type]; - if (!set) return; - const shouldSelect = checkbox && checkbox.checked; - set.clear(); - if (shouldSelect) { - const items = cacheListState[type]?.items || []; - items.forEach(item => { - if (item && item.name) set.add(item.name); - }); + if (checkbox && checkbox.checked) { + selectLocalPage(type); + } else { + clearLocalSelection(type); } - syncLocalRowCheckboxes(type); - updateSelectedCount(); } function syncLocalRowCheckboxes(type) { @@ -550,12 +895,7 @@ function syncLocalRowCheckboxes(type) { } function syncLocalSelectAllState(type) { - const selectAll = type === 'image' ? ui.localImageSelectAll : ui.localVideoSelectAll; - if (!selectAll) return; - const total = cacheListState[type]?.items?.length || 0; - const selected = selectedLocal[type]?.size || 0; - selectAll.checked = total > 0 && selected === total; - selectAll.indeterminate = selected > 0 && selected < total; + refreshLocalSelectControl(type); } function syncRowCheckboxes() { @@ -584,6 +924,8 @@ function updateSelectedCount() { const el = ui.selectedCount; const selected = getActiveSelectedSet().size; if (el) el.textContent = String(selected); + refreshLocalSelectControl('image'); + refreshLocalSelectControl('video'); setActionButtonsState(); updateBatchActionsVisibility(); } @@ -764,31 +1106,56 @@ async function toggleCacheList(type) { await showCacheSection(type); } -async function loadLocalCacheList(type) { +async function loadLocalCacheList(type, options = {}) { const body = type === 'image' ? ui.localImageBody : ui.localVideoBody; if (!body) return; + const state = getLocalState(type); + if (!state) return; + const pageSize = Math.max( + 1, + parseInt(options.pageSize ?? state.pageSize ?? LOCAL_PAGE_SIZE_DEFAULT, 10) || LOCAL_PAGE_SIZE_DEFAULT + ); + const targetPage = Math.max(1, parseInt(options.page ?? state.page ?? 1, 10) || 1); + state.loading = true; + state.pageSize = pageSize; + state.page = targetPage; + updateLocalPaginationUI(type); body.innerHTML = `