feat: improve agent management and speed up tests
This commit is contained in:
1 parent
134c06d605
commit
193fc3a95c
28 files changed
+419
-57
No files matched your search
+111
-1
@@ -1,5 +1,5 @@
|
||||
import { Check, Copy, Moon, Plug, Sun } from 'lucide-react'
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { Component, createContext, useCallback, useContext, useEffect, useRef, useState, type ErrorInfo, type ReactNode } from 'react'
|
||||
import { ApiError } from '../api/client'
|
||||
import { getBase, getToken, normalizeBase, setSource } from '../api/source'
|
||||
import { useTheme } from '../hooks'
|
||||
@@ -230,6 +230,116 @@ export function LoadingBar({ active }: { active: boolean }) {
|
||||
) : null
|
||||
}
|
||||
|
||||
export interface CommandTab {
|
||||
id: string
|
||||
label: string
|
||||
command: string
|
||||
}
|
||||
|
||||
/** 按浏览器所在的系统挑一个默认的标签(Windows / macOS / 其他当 Linux)。 */
|
||||
export function guessPlatform(): 'windows' | 'macos' | 'linux' {
|
||||
const text = `${navigator.platform} ${navigator.userAgent}`.toLowerCase()
|
||||
return text.includes('win') ? 'windows' : text.includes('mac') ? 'macos' : 'linux'
|
||||
}
|
||||
|
||||
/**
|
||||
* 带标签切换的命令框(curl / npm / bun 那样):上面一排下划线标签,下面是等宽的命令,右侧一个复制图标。
|
||||
* 记住上次选的标签(只是浏览器里的小方便)。
|
||||
*/
|
||||
export function CommandTabs({ tabs, initial, storageKey }: { tabs: CommandTab[]; initial?: string; storageKey?: string }) {
|
||||
const [active, setActive] = useState(() => {
|
||||
let saved: string | null = null
|
||||
try {
|
||||
saved = storageKey ? localStorage.getItem(storageKey) : null
|
||||
} catch {
|
||||
// 浏览器禁用存储时只是不记住
|
||||
}
|
||||
return [saved, initial].find((id) => id && tabs.some((t) => t.id === id)) ?? tabs[0]?.id ?? ''
|
||||
})
|
||||
const [done, setDone] = useState(false)
|
||||
const tab = tabs.find((t) => t.id === active) ?? tabs[0]
|
||||
if (!tab) return null
|
||||
const pick = (id: string) => {
|
||||
setActive(id)
|
||||
try {
|
||||
if (storageKey) localStorage.setItem(storageKey, id)
|
||||
} catch {
|
||||
// 同上
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="cmd-tabs">
|
||||
<div className="cmd-tablist" role="tablist">
|
||||
{tabs.map((t) => (
|
||||
<button key={t.id} type="button" role="tab" aria-selected={t.id === tab.id} className={t.id === tab.id ? 'on' : ''} onClick={() => pick(t.id)}>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="cmd-body">
|
||||
<code>{tab.command}</code>
|
||||
<button
|
||||
type="button"
|
||||
className="cmd-copy"
|
||||
aria-label="复制命令"
|
||||
title="复制命令"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(tab.command)
|
||||
setDone(true)
|
||||
window.setTimeout(() => setDone(false), 1500)
|
||||
} catch {
|
||||
// 没有剪贴板权限时用户可以手动选中
|
||||
}
|
||||
}}
|
||||
>
|
||||
{done ? <Check size={16} /> : <Copy size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面出错时显示提示,而不是整页白屏:说明出了什么错,可以重试(重新渲染这一块)或刷新整个页面。
|
||||
* 常见原因是前端和后端版本对不上(后端还没升级到新接口)。
|
||||
*/
|
||||
export class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
|
||||
state: { error: Error | null } = { error: null }
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error(error, info.componentStack)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error } = this.state
|
||||
if (!error) return this.props.children
|
||||
return (
|
||||
<div className="login">
|
||||
<div className="card stack" style={{ maxWidth: 520 }}>
|
||||
<div>
|
||||
<h1>页面出错了</h1>
|
||||
<p className="muted">这一页没能显示出来。如果刚升级过,多半是前端和后端的版本对不上:先把后端升级到对应的版本,再刷新。</p>
|
||||
</div>
|
||||
<pre className="code">{error.message}</pre>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<button type="button" className="btn primary" onClick={() => this.setState({ error: null })}>
|
||||
重试
|
||||
</button>
|
||||
<button type="button" className="btn" onClick={() => location.reload()}>
|
||||
刷新页面
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 数据源设置:默认连同源的后端;填了地址就连别的后端(跨域用 API 令牌,后端要放行这个前端的来源)。 */
|
||||
export function SourceButton({ tokenLabel = '只读令牌(站点私有时才需要)' }: { tokenLabel?: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
Reference in new issue
Block a user