picoclaw/web/frontend/src/components/credentials/anthropic-credential-card.tsx
wenjie 6f8c91f8c3 feat(credentials): add multi-provider OAuth credential management
- add backend `/api/oauth/*` endpoints for provider status, browser/device-code/token login, flow query/polling, and logout
- extend API handler with OAuth flow/state tracking and route registration, plus OAuth unit tests
- implement frontend credentials page/components for OpenAI, Anthropic, and Google Antigravity login/logout
- add OAuth API client and `useCredentialsPage` hook, with new EN/ZH i18n strings
2026-03-09 19:24:44 +08:00

102 lines
3.2 KiB
TypeScript

import { IconKey, IconLoader2, IconPlayerStopFilled } from "@tabler/icons-react"
import { useTranslation } from "react-i18next"
import type { OAuthProviderStatus } from "@/api/oauth"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { CredentialCard } from "./credential-card"
interface AnthropicCredentialCardProps {
status?: OAuthProviderStatus
activeAction: string
token: string
onTokenChange: (value: string) => void
onStopLoading: () => void
onSaveToken: () => void
onAskLogout: () => void
}
export function AnthropicCredentialCard({
status,
activeAction,
token,
onTokenChange,
onStopLoading,
onSaveToken,
onAskLogout,
}: AnthropicCredentialCardProps) {
const { t } = useTranslation()
const actionBusy = activeAction !== ""
const tokenLoading = activeAction === "anthropic:token"
const stopLabel = t("credentials.actions.stopLoading", "Stop Loading")
return (
<CredentialCard
title="Anthropic"
description={t(
"credentials.providers.anthropic.description",
"Uses token login for Claude access.",
)}
status={status?.status ?? "not_logged_in"}
authMethod={status?.auth_method}
actions={
<div className="border-muted flex h-[120px] flex-col justify-center rounded-lg border p-3">
<div className="flex h-full flex-col gap-3">
<div className="flex h-full items-center gap-2">
<Input
value={token}
onChange={(e) => onTokenChange(e.target.value)}
type="password"
placeholder={t(
"credentials.fields.anthropicToken",
"Anthropic token",
)}
/>
<Button
size="sm"
className="w-fit"
disabled={actionBusy || !token.trim()}
onClick={onSaveToken}
>
{tokenLoading && (
<IconLoader2 className="size-4 animate-spin" />
)}
<IconKey className="size-4" />
{t("credentials.actions.saveToken", "Save")}
</Button>
{tokenLoading && (
<Button
size="icon-sm"
variant="ghost"
onClick={onStopLoading}
aria-label={stopLabel}
title={stopLabel}
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
>
<IconPlayerStopFilled className="size-4" />
</Button>
)}
</div>
</div>
</div>
}
footer={
status?.logged_in ? (
<Button
variant="ghost"
size="sm"
disabled={actionBusy}
onClick={onAskLogout}
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
>
{activeAction === "anthropic:logout" && (
<IconLoader2 className="size-4 animate-spin" />
)}
{t("credentials.actions.logout", "Logout")}
</Button>
) : null
}
/>
)
}