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
35 changes: 34 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@ function displayError(error: unknown, t: Translate) {
return apiErrorMessage(error instanceof ApiError ? error.code : undefined, t)
}

async function copyText(value: string): Promise<boolean> {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(value)
return true
} catch { /* Fall back when clipboard access is denied. */ }
}

const field = document.createElement("textarea")
field.value = value
field.readOnly = true
field.style.position = "fixed"
field.style.opacity = "0"
const focused = document.activeElement instanceof HTMLElement ? document.activeElement : null
document.body.appendChild(field)
field.select()
try {
return document.execCommand("copy")
} catch {
return false
} finally {
field.remove()
focused?.focus()
}
}

function loadAvailableModels(): Promise<{ models: Model[] }> {
return api("/internal/models")
}
Expand Down Expand Up @@ -142,6 +168,7 @@ function Keys({ csrf }: { csrf: string }) {
const [rawKey, setRawKey] = useState("")
const [showCreate, setShowCreate] = useState(false)
const [error, setError] = useState<unknown>(null)
const [copyError, setCopyError] = useState(false)
const [busy, setBusy] = useState(false)
const reload = () => api<ApiKey[]>("/internal/api-keys").then(setKeys).catch(setError)
useEffect(() => { void reload() }, [])
Expand All @@ -153,6 +180,7 @@ function Keys({ csrf }: { csrf: string }) {
try {
const created = await api<{ key: string }>("/internal/api-keys", { method: "POST", body: JSON.stringify({ name }) }, csrf)
setRawKey(created.key)
setCopyError(false)
setName("")
setShowCreate(false)
await reload()
Expand All @@ -173,10 +201,15 @@ function Keys({ csrf }: { csrf: string }) {
}
}

async function copyKey() {
setCopyError(false)
if (!await copyText(rawKey)) setCopyError(true)
}

return <>
<PageTitle title={t("apiKeys")} description={t("keysDescription")} action={<Button onClick={() => setShowCreate(true)}><Plus size={16} /> {t("createKey")}</Button>} />
{error ? <div className="form-error" role="alert">{displayError(error, t)}</div> : null}
{rawKey ? <div className="key-reveal"><div><strong>{t("saveKeyTitle")}</strong><p>{t("saveKeyDescription")}</p></div><div className="key-value"><code>{rawKey}</code><Button variant="outline" size="sm" onClick={() => navigator.clipboard.writeText(rawKey)}><Copy size={14} /> {t("copy")}</Button></div><button aria-label={t("closeKeyNotice")} onClick={() => setRawKey("")}><X size={18} /></button></div> : null}
{rawKey ? <div className="key-reveal"><div><strong>{t("saveKeyTitle")}</strong><p>{t("saveKeyDescription")}</p>{copyError ? <p className="form-error" role="alert">{t("copyManually")}</p> : null}</div><div className="key-value"><code>{rawKey}</code><Button variant="outline" size="sm" onClick={() => void copyKey()}><Copy size={14} /> {t("copy")}</Button></div><button aria-label={t("closeKeyNotice")} onClick={() => setRawKey("")}><X size={18} /></button></div> : null}
<div className="section-head"><h2>{t("keyList")}</h2><span>{t("keyCount", { count: keys.length })}</span></div>
{keys.length === 0 ? <div className="empty"><KeyRound size={23} /><h3>{t("noKey")}</h3><p>{t("noKeyDescription")}</p><Button variant="outline" onClick={() => setShowCreate(true)}>{t("createKey")}</Button></div> :
<div className="table-wrap"><table><thead><tr><th>{t("name")}</th><th>{t("key")}</th><th>{t("createdAt")}</th><th>{t("lastUsed")}</th><th>{t("status")}</th><th></th></tr></thead><tbody>{keys.map(key =>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ class DraftError extends Error {
constructor(readonly key: MessageKey, readonly values?: Record<string, string | number>) { super(key) }
}

let nextQuestionUid = 0

function makeQuestion(type: QuestionType = "noul"): Question {
return { uid: crypto.randomUUID(), id: "", type, instructions: "", choices: [{ label: "", description: "" }, { label: "", description: "" }], levels: ["", ""] }
return { uid: String(++nextQuestionUid), id: "", type, instructions: "", choices: [{ label: "", description: "" }, { label: "", description: "" }], levels: ["", ""] }
}

function parseDraft(value: string): Draft {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const zhCN = {
saveKeyTitle: "请立即保存这把密钥",
saveKeyDescription: "关闭后将无法再次查看完整密钥。",
copy: "复制",
copyManually: "浏览器无法自动复制,请手动选中并复制上方密钥。",
closeKeyNotice: "关闭密钥提示",
keyList: "密钥的列表",
keyCount: "{count} 把密钥",
Expand Down Expand Up @@ -176,6 +177,7 @@ const en: Record<MessageKey, string> = {
saveKeyTitle: "Save this key now",
saveKeyDescription: "You cannot view the full key again after closing this notice.",
copy: "Copy",
copyManually: "Your browser could not copy the key. Select and copy it manually above.",
closeKeyNotice: "Close key notice",
keyList: "Keys",
keyCount: "Total keys: {count}",
Expand Down Expand Up @@ -314,6 +316,7 @@ const zhTW: Record<MessageKey, string> = {
saveKeyTitle: "請立即儲存這把金鑰",
saveKeyDescription: "關閉提示後將無法再次查看完整金鑰。",
copy: "複製",
copyManually: "瀏覽器無法自動複製,請手動選取並複製上方金鑰。",
closeKeyNotice: "關閉金鑰提示",
keyList: "金鑰列表",
keyCount: "{count} 把金鑰",
Expand Down
Loading