Spaces:
Running
Running
| import { remainingLockSeconds } from './api.js'; | |
| export function retryAfterLabel(lock, nowMs = Date.now()) { | |
| const seconds = remainingLockSeconds(lock, nowMs); | |
| return seconds > 0 ? `请求过快,请 ${seconds} 秒后再试` : ''; | |
| } | |
| export const DEFAULT_REASONING_EFFORT = 'xhigh'; | |
| export const REASONING_DEFAULT_VERSION = 'xhigh-v1'; | |
| export const REASONING_DEFAULT_VERSION_KEY = 'codexmobile.reasoningDefaultVersion'; | |
| export const REASONING_EFFORT_KEY = 'codexmobile.reasoningEffort'; | |
| export const THEME_KEY = 'codexmobile.theme'; | |
| export const VOICE_MAX_RECORDING_MS = 90 * 1000; | |
| export const VOICE_MAX_UPLOAD_BYTES = 10 * 1024 * 1024; | |
| export const VOICE_MIME_CANDIDATES = ['audio/mp4', 'audio/webm;codecs=opus', 'audio/webm']; | |
| export const VOICE_DIALOG_SILENCE_MS = 900; | |
| export const VOICE_DIALOG_MIN_RECORDING_MS = 600; | |
| export const VOICE_DIALOG_LEVEL_THRESHOLD = 0.018; | |
| export const VOICE_DIALOG_SILENCE_AUDIO = | |
| 'data:audio/wav;base64,UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAAAAA=='; | |
| export const REALTIME_VOICE_SAMPLE_RATE = 24000; | |
| export const REALTIME_VOICE_BUFFER_SIZE = 2048; | |
| export const REALTIME_VOICE_MIN_TURN_MS = 500; | |
| export const REALTIME_VOICE_BARGE_IN_LEVEL_THRESHOLD = 0.026; | |
| export const REALTIME_VOICE_BARGE_IN_SUSTAIN_MS = 180; | |
| export async function copyTextToClipboard(text) { | |
| const value = String(text || ''); | |
| try { | |
| if (navigator.clipboard?.writeText) { | |
| await navigator.clipboard.writeText(value); | |
| return true; | |
| } | |
| } catch { | |
| // Fall back below for browsers that block Clipboard API in PWA/http contexts. | |
| } | |
| const textarea = document.createElement('textarea'); | |
| textarea.value = value; | |
| textarea.setAttribute('readonly', ''); | |
| textarea.style.position = 'fixed'; | |
| textarea.style.top = '-1000px'; | |
| textarea.style.left = '-1000px'; | |
| document.body.appendChild(textarea); | |
| textarea.select(); | |
| textarea.setSelectionRange(0, textarea.value.length); | |
| try { | |
| return document.execCommand('copy'); | |
| } finally { | |
| document.body.removeChild(textarea); | |
| } | |
| } | |
| export const PERMISSION_OPTIONS = [ | |
| { value: 'default', label: '默认权限' }, | |
| { value: 'acceptEdits', label: '自动接受编辑' }, | |
| { value: 'bypassPermissions', label: '完全访问', danger: true } | |
| ]; | |
| export const REASONING_OPTIONS = [ | |
| { value: 'low', label: '低' }, | |
| { value: 'medium', label: '中' }, | |
| { value: 'high', label: '高' }, | |
| { value: 'xhigh', label: '超高' } | |
| ]; | |
| export function formatTime(value) { | |
| if (!value) { | |
| return ''; | |
| } | |
| try { | |
| return new Intl.DateTimeFormat('zh-CN', { | |
| month: '2-digit', | |
| day: '2-digit', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }).format(new Date(value)); | |
| } catch { | |
| return ''; | |
| } | |
| } | |
| export function compactPath(value) { | |
| if (!value) { | |
| return ''; | |
| } | |
| const normalized = value.replaceAll('\\', '/'); | |
| const parts = normalized.split('/').filter(Boolean); | |
| return parts.length > 2 ? `${parts.at(-2)}/${parts.at(-1)}` : normalized; | |
| } | |
| export function formatBytes(value) { | |
| const size = Number(value) || 0; | |
| if (size < 1024) { | |
| return `${size} B`; | |
| } | |
| if (size < 1024 * 1024) { | |
| return `${Math.round(size / 102.4) / 10} KB`; | |
| } | |
| return `${Math.round(size / 1024 / 102.4) / 10} MB`; | |
| } | |
| export function shortModelName(model) { | |
| if (!model) { | |
| return '5.5'; | |
| } | |
| return model | |
| .replace(/^gpt-/i, '') | |
| .replace(/-codex.*$/i, '') | |
| .replace(/-mini$/i, ' mini'); | |
| } | |
| export function permissionLabel(value) { | |
| return PERMISSION_OPTIONS.find((option) => option.value === value)?.label || '默认权限'; | |
| } | |
| export function reasoningLabel(value) { | |
| return REASONING_OPTIONS.find((option) => option.value === value)?.label || '超高'; | |
| } | |
| export function imageUrlWithRetry(url, retryKey) { | |
| if (!retryKey) { | |
| return url; | |
| } | |
| const separator = url.includes('?') ? '&' : '?'; | |
| return `${url}${separator}r=${retryKey}`; | |
| } | |
| export function createClientTurnId() { | |
| return globalThis.crypto?.randomUUID?.() || `turn-${Date.now()}-${Math.random().toString(16).slice(2)}`; | |
| } | |
| export function createDraftSession(project) { | |
| const now = new Date().toISOString(); | |
| return { | |
| id: `draft-${project.id}-${Date.now()}`, | |
| projectId: project.id, | |
| title: '新对话', | |
| summary: '等待第一条消息', | |
| messageCount: 0, | |
| updatedAt: now, | |
| draft: true | |
| }; | |
| } | |
| export function isDraftSession(session) { | |
| const id = typeof session === 'string' ? session : session?.id; | |
| return Boolean(session?.draft || id?.startsWith('draft-')); | |
| } | |
| export function titleFromFirstMessage(message) { | |
| const value = String(message || '').trim().replace(/\s+/g, ' '); | |
| return value ? value.slice(0, 52) : '新对话'; | |
| } | |
| export function payloadRunKeys(payload) { | |
| return [payload?.turnId, payload?.sessionId, payload?.previousSessionId].filter(Boolean); | |
| } | |
| export function selectedRunKeys(session) { | |
| return [session?.id, session?.turnId].filter(Boolean); | |
| } | |
| export function hasRunningKey(runningById, keys) { | |
| return keys.some((key) => Boolean(runningById[key])); | |
| } | |
| export function hasVisibleAssistantForTurn(messages, payload) { | |
| const hasExactMatch = messages.some( | |
| (message) => | |
| message.role === 'assistant' && | |
| ((payload?.messageId && message.id === payload.messageId) || | |
| (payload?.turnId && message.turnId === payload.turnId)) && | |
| typeof message.content === 'string' && | |
| message.content.trim() | |
| ); | |
| if (hasExactMatch) { | |
| return true; | |
| } | |
| if (payload?.turnId || payload?.messageId) { | |
| return false; | |
| } | |
| const latestUserIndex = messages.reduce( | |
| (latest, message, index) => (message.role === 'user' ? index : latest), | |
| -1 | |
| ); | |
| return messages.some( | |
| (message, index) => | |
| message.role === 'assistant' && | |
| index > latestUserIndex && | |
| typeof message.content === 'string' && | |
| message.content.trim() | |
| ); | |
| } | |
| export function hasLatestAssistantAfterLatestUser(messages) { | |
| const latestUserIndex = messages.reduce( | |
| (latest, message, index) => (message.role === 'user' ? index : latest), | |
| -1 | |
| ); | |
| return messages.some( | |
| (message, index) => | |
| message.role === 'assistant' && | |
| index > latestUserIndex && | |
| typeof message.content === 'string' && | |
| message.content.trim() | |
| ); | |
| } | |
| export function hasAssistantResultForTurn(messages, payload) { | |
| if (hasVisibleAssistantForTurn(messages, payload)) { | |
| return true; | |
| } | |
| if (payload?.messageId) { | |
| return false; | |
| } | |
| if (payload?.turnId && !payload?.allowLatestAssistantFallback) { | |
| return false; | |
| } | |
| if (!payload?.hadAssistantText && payload?.status !== 'completed') { | |
| return false; | |
| } | |
| return hasLatestAssistantAfterLatestUser(messages); | |
| } | |
| export function upsertSessionInProject(current, projectId, session, replaceId = null) { | |
| if (!projectId || !session) { | |
| return current; | |
| } | |
| const existing = current[projectId] || []; | |
| const filtered = existing.filter((item) => item.id !== session.id && (!replaceId || item.id !== replaceId)); | |
| return { | |
| ...current, | |
| [projectId]: [session, ...filtered] | |
| }; | |
| } | |