mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 01:21:43 +00:00
fix: use grid/cell components
This commit is contained in:
parent
923539a46b
commit
60fd4b7d16
12 changed files with 434 additions and 181 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { Typography } from "@nous-research/ui/ui/components/typography/index";
|
||||
import { useI18n } from "@/i18n/context";
|
||||
|
||||
/**
|
||||
|
|
@ -19,9 +20,12 @@ export function LanguageSwitcher() {
|
|||
>
|
||||
{/* Show the *current* language's flag — tooltip advertises the click action */}
|
||||
<span className="text-base leading-none">{locale === "en" ? "🇬🇧" : "🇨🇳"}</span>
|
||||
<span className="hidden sm:inline font-mondwest tracking-wide uppercase text-[0.65rem]">
|
||||
<Typography
|
||||
mondwest
|
||||
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
|
||||
>
|
||||
{locale === "en" ? "EN" : "中文"}
|
||||
</span>
|
||||
</Typography>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { ExternalLink, Copy, X, Check, Loader2 } from "lucide-react";
|
||||
import { Typography } from "@nous-research/ui/ui/components/typography/index";
|
||||
import { api, type OAuthProvider, type OAuthStartResponse } from "@/lib/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -12,9 +13,21 @@ interface Props {
|
|||
onError: (msg: string) => void;
|
||||
}
|
||||
|
||||
type Phase = "idle" | "starting" | "awaiting_user" | "submitting" | "polling" | "approved" | "error";
|
||||
type Phase =
|
||||
| "idle"
|
||||
| "starting"
|
||||
| "awaiting_user"
|
||||
| "submitting"
|
||||
| "polling"
|
||||
| "approved"
|
||||
| "error";
|
||||
|
||||
export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props) {
|
||||
export function OAuthLoginModal({
|
||||
provider,
|
||||
onClose,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: Props) {
|
||||
const [phase, setPhase] = useState<Phase>("starting");
|
||||
const [start, setStart] = useState<OAuthStartResponse | null>(null);
|
||||
const [pkceCode, setPkceCode] = useState("");
|
||||
|
|
@ -81,13 +94,15 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
|||
if (!isMounted.current) return;
|
||||
if (resp.status === "approved") {
|
||||
setPhase("approved");
|
||||
if (pollTimer.current !== null) window.clearInterval(pollTimer.current);
|
||||
if (pollTimer.current !== null)
|
||||
window.clearInterval(pollTimer.current);
|
||||
onSuccess(`${provider.name} connected`);
|
||||
window.setTimeout(() => isMounted.current && onClose(), 1500);
|
||||
} else if (resp.status !== "pending") {
|
||||
setPhase("error");
|
||||
setErrorMsg(resp.error_message || `Login ${resp.status}`);
|
||||
if (pollTimer.current !== null) window.clearInterval(pollTimer.current);
|
||||
if (pollTimer.current !== null)
|
||||
window.clearInterval(pollTimer.current);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isMounted.current) return;
|
||||
|
|
@ -107,7 +122,11 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
|||
setPhase("submitting");
|
||||
setErrorMsg(null);
|
||||
try {
|
||||
const resp = await api.submitOAuthCode(provider.id, start.session_id, pkceCode.trim());
|
||||
const resp = await api.submitOAuthCode(
|
||||
provider.id,
|
||||
start.session_id,
|
||||
pkceCode.trim(),
|
||||
);
|
||||
if (!isMounted.current) return;
|
||||
if (resp.ok && resp.status === "approved") {
|
||||
setPhase("approved");
|
||||
|
|
@ -175,14 +194,24 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
|||
</button>
|
||||
<div className="p-6 flex flex-col gap-4">
|
||||
<div>
|
||||
<h2 id="oauth-modal-title" className="font-mondwest text-base tracking-wider uppercase">
|
||||
<Typography
|
||||
as="h2"
|
||||
mondwest
|
||||
id="oauth-modal-title"
|
||||
className="text-base tracking-wider uppercase"
|
||||
>
|
||||
{t.oauth.connect} {provider.name}
|
||||
</h2>
|
||||
{secondsLeft !== null && phase !== "approved" && phase !== "error" && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t.oauth.sessionExpires.replace("{time}", fmtTime(secondsLeft))}
|
||||
</p>
|
||||
)}
|
||||
</Typography>
|
||||
{secondsLeft !== null &&
|
||||
phase !== "approved" &&
|
||||
phase !== "error" && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t.oauth.sessionExpires.replace(
|
||||
"{time}",
|
||||
fmtTime(secondsLeft),
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── starting ───────────────────────────────────── */}
|
||||
|
|
@ -211,7 +240,10 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
|||
/>
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<a
|
||||
href={(start as Extract<OAuthStartResponse, { flow: "pkce" }>).auth_url}
|
||||
href={
|
||||
(start as Extract<OAuthStartResponse, { flow: "pkce" }>)
|
||||
.auth_url
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
|
||||
|
|
@ -219,7 +251,11 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
|||
<ExternalLink className="h-3 w-3" />
|
||||
{t.oauth.reOpenAuth}
|
||||
</a>
|
||||
<Button onClick={handleSubmitPkceCode} disabled={!pkceCode.trim()} size="sm">
|
||||
<Button
|
||||
onClick={handleSubmitPkceCode}
|
||||
disabled={!pkceCode.trim()}
|
||||
size="sm"
|
||||
>
|
||||
{t.oauth.submitCode}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -243,23 +279,46 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
|||
</p>
|
||||
<div className="flex items-center justify-between gap-2 border border-border bg-secondary/30 p-4">
|
||||
<code className="font-mono-ui text-2xl tracking-widest text-foreground">
|
||||
{(start as Extract<OAuthStartResponse, { flow: "device_code" }>).user_code}
|
||||
{
|
||||
(
|
||||
start as Extract<
|
||||
OAuthStartResponse,
|
||||
{ flow: "device_code" }
|
||||
>
|
||||
).user_code
|
||||
}
|
||||
</code>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleCopyUserCode(
|
||||
(start as Extract<OAuthStartResponse, { flow: "device_code" }>).user_code,
|
||||
(
|
||||
start as Extract<
|
||||
OAuthStartResponse,
|
||||
{ flow: "device_code" }
|
||||
>
|
||||
).user_code,
|
||||
)
|
||||
}
|
||||
className="text-xs"
|
||||
>
|
||||
{codeCopied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
||||
{codeCopied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<a
|
||||
href={(start as Extract<OAuthStartResponse, { flow: "device_code" }>).verification_url}
|
||||
href={
|
||||
(
|
||||
start as Extract<
|
||||
OAuthStartResponse,
|
||||
{ flow: "device_code" }
|
||||
>
|
||||
).verification_url
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
|
||||
|
|
@ -302,21 +361,36 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
|||
setStart(null);
|
||||
setPkceCode("");
|
||||
setPhase("starting");
|
||||
api.startOAuthLogin(provider.id).then((resp) => {
|
||||
if (!isMounted.current) return;
|
||||
setStart(resp);
|
||||
setSecondsLeft(resp.expires_in);
|
||||
setPhase(resp.flow === "device_code" ? "polling" : "awaiting_user");
|
||||
if (resp.flow === "pkce") {
|
||||
window.open(resp.auth_url, "_blank", "noopener,noreferrer");
|
||||
} else {
|
||||
window.open(resp.verification_url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (!isMounted.current) return;
|
||||
setPhase("error");
|
||||
setErrorMsg(`${t.common.retry} failed: ${e}`);
|
||||
});
|
||||
api
|
||||
.startOAuthLogin(provider.id)
|
||||
.then((resp) => {
|
||||
if (!isMounted.current) return;
|
||||
setStart(resp);
|
||||
setSecondsLeft(resp.expires_in);
|
||||
setPhase(
|
||||
resp.flow === "device_code"
|
||||
? "polling"
|
||||
: "awaiting_user",
|
||||
);
|
||||
if (resp.flow === "pkce") {
|
||||
window.open(
|
||||
resp.auth_url,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
} else {
|
||||
window.open(
|
||||
resp.verification_url,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!isMounted.current) return;
|
||||
setPhase("error");
|
||||
setErrorMsg(`${t.common.retry} failed: ${e}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t.common.retry}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Palette, Check } from "lucide-react";
|
||||
import { Typography } from "@nous-research/ui/ui/components/typography/index";
|
||||
import { BUILTIN_THEMES, useTheme } from "@/themes";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -56,9 +57,12 @@ export function ThemeSwitcher() {
|
|||
aria-haspopup="listbox"
|
||||
>
|
||||
<Palette className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline font-mondwest tracking-wide uppercase text-[0.65rem]">
|
||||
<Typography
|
||||
mondwest
|
||||
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</Typography>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
|
|
@ -72,9 +76,12 @@ export function ThemeSwitcher() {
|
|||
)}
|
||||
>
|
||||
<div className="border-b border-current/20 px-3 py-2">
|
||||
<span className="font-mondwest text-[0.65rem] tracking-[0.15em] uppercase text-midground/70">
|
||||
<Typography
|
||||
mondwest
|
||||
className="text-[0.65rem] tracking-[0.15em] uppercase text-midground/70"
|
||||
>
|
||||
{t.theme?.title ?? "Theme"}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{availableThemes.map((th) => {
|
||||
|
|
@ -100,13 +107,16 @@ export function ThemeSwitcher() {
|
|||
{preset ? <ThemeSwatch theme={preset.name} /> : <PlaceholderSwatch />}
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="truncate font-mondwest text-[0.75rem] tracking-wide uppercase">
|
||||
<Typography
|
||||
mondwest
|
||||
className="truncate text-[0.75rem] tracking-wide uppercase"
|
||||
>
|
||||
{th.label}
|
||||
</span>
|
||||
</Typography>
|
||||
{th.description && (
|
||||
<span className="truncate font-sans text-[0.65rem] normal-case tracking-normal text-midground/50">
|
||||
<Typography className="truncate text-[0.65rem] normal-case tracking-normal text-midground/50">
|
||||
{th.description}
|
||||
</span>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue