mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
fix(components): refactor to use design system
This commit is contained in:
parent
663602f6b0
commit
1285172aca
20 changed files with 243 additions and 597 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import { Select, SelectOption } from "@nous-research/ui";
|
||||
import { Select, SelectOption, Switch } from "@nous-research/ui";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
function FieldHint({ schema, schemaKey }: { schema: Record<string, unknown>; schemaKey: string }) {
|
||||
const keyPath = schemaKey.includes(".") ? schemaKey : "";
|
||||
|
|
|
|||
|
|
@ -313,19 +313,21 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) {
|
|||
model
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
ghost
|
||||
size="sm"
|
||||
disabled={!canPickModel}
|
||||
onClick={() => setModelOpen(true)}
|
||||
className="flex items-center gap-1 truncate text-sm font-medium hover:underline disabled:cursor-not-allowed disabled:opacity-60 disabled:no-underline"
|
||||
suffix={
|
||||
canPickModel ? (
|
||||
<ChevronDown className="opacity-60" />
|
||||
) : undefined
|
||||
}
|
||||
className="self-start min-w-0 px-0 py-0 normal-case tracking-normal text-sm font-medium hover:underline disabled:no-underline"
|
||||
title={info.model ?? "switch model"}
|
||||
>
|
||||
<span className="truncate">{modelLabel}</span>
|
||||
|
||||
{canPickModel && (
|
||||
<ChevronDown className="h-3 w-3 shrink-0 opacity-60" />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Badge tone={STATE_TONE[state]}>{STATE_LABEL[state]}</Badge>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Typography } from "@nous-research/ui";
|
||||
import { Button, Typography } from "@nous-research/ui";
|
||||
import { useI18n } from "@/i18n/context";
|
||||
|
||||
/**
|
||||
|
|
@ -11,22 +11,25 @@ export function LanguageSwitcher() {
|
|||
const toggle = () => setLocale(locale === "en" ? "zh" : "en");
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
ghost
|
||||
onClick={toggle}
|
||||
className="group relative inline-flex items-center gap-1.5 px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
title={t.language.switchTo}
|
||||
aria-label={t.language.switchTo}
|
||||
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<span className="text-base leading-none">
|
||||
{locale === "en" ? "🇬🇧" : "🇨🇳"}
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="text-base leading-none">
|
||||
{locale === "en" ? "🇬🇧" : "🇨🇳"}
|
||||
</span>
|
||||
|
||||
<Typography
|
||||
mondwest
|
||||
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
|
||||
>
|
||||
{locale === "en" ? "EN" : "中文"}
|
||||
</Typography>
|
||||
</span>
|
||||
<Typography
|
||||
mondwest
|
||||
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
|
||||
>
|
||||
{locale === "en" ? "EN" : "中文"}
|
||||
</Typography>
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button } from "@nous-research/ui";
|
||||
import { Button, ListItem } from "@nous-research/ui";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { GatewayClient } from "@/lib/gatewayClient";
|
||||
import { Check, Loader2, Search, X } from "lucide-react";
|
||||
|
|
@ -280,14 +280,12 @@ function ProviderColumn({
|
|||
{providers.map((p) => {
|
||||
const active = p.slug === selectedSlug;
|
||||
return (
|
||||
<button
|
||||
<ListItem
|
||||
key={p.slug}
|
||||
type="button"
|
||||
active={active}
|
||||
onClick={() => onSelect(p.slug)}
|
||||
className={`w-full text-left px-3 py-2 text-xs border-l-2 transition-colors cursor-pointer flex items-start gap-2 ${
|
||||
active
|
||||
? "bg-primary/10 border-l-primary text-foreground"
|
||||
: "border-l-transparent text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
className={`items-start text-xs border-l-2 ${
|
||||
active ? "border-l-primary" : "border-l-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
@ -299,7 +297,7 @@ function ProviderColumn({
|
|||
{p.slug} · {p.total_models ?? p.models?.length ?? 0} models
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -360,23 +358,19 @@ function ModelColumn({
|
|||
m === currentModel && provider.slug === currentProviderSlug;
|
||||
|
||||
return (
|
||||
<button
|
||||
<ListItem
|
||||
key={m}
|
||||
type="button"
|
||||
active={active}
|
||||
onClick={() => onSelect(m)}
|
||||
onDoubleClick={() => onConfirm(m)}
|
||||
className={`w-full text-left px-3 py-1.5 text-xs font-mono transition-colors cursor-pointer flex items-center gap-2 ${
|
||||
active
|
||||
? "bg-primary/15 text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
}`}
|
||||
className="px-3 py-1.5 text-xs font-mono"
|
||||
>
|
||||
<Check
|
||||
className={`h-3 w-3 shrink-0 ${active ? "text-primary" : "text-transparent"}`}
|
||||
/>
|
||||
<span className="flex-1 truncate">{m}</span>
|
||||
{isCurrent && <CurrentTag />}
|
||||
</button>
|
||||
</ListItem>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { GatewayClient } from "@/lib/gatewayClient";
|
||||
import { ListItem } from "@nous-research/ui";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import {
|
||||
forwardRef,
|
||||
|
|
@ -139,18 +140,14 @@ export const SlashPopover = forwardRef<SlashPopoverHandle, Props>(
|
|||
const active = i === selected;
|
||||
|
||||
return (
|
||||
<button
|
||||
<ListItem
|
||||
key={`${it.text}-${i}`}
|
||||
type="button"
|
||||
active={active}
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
onMouseEnter={() => setSelected(i)}
|
||||
onClick={() => apply(it)}
|
||||
className={`w-full flex items-center gap-2 px-3 py-1.5 text-left cursor-pointer transition-colors ${
|
||||
active
|
||||
? "bg-primary/10 text-foreground"
|
||||
: "text-muted-foreground hover:bg-muted/60"
|
||||
}`}
|
||||
className="px-3 py-1.5"
|
||||
>
|
||||
<ChevronRight
|
||||
className={`h-3 w-3 shrink-0 ${active ? "text-primary" : "text-transparent"}`}
|
||||
|
|
@ -165,7 +162,7 @@ export const SlashPopover = forwardRef<SlashPopoverHandle, Props>(
|
|||
{it.meta}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Palette, Check } from "lucide-react";
|
||||
import { Typography } from "@nous-research/ui";
|
||||
import { Button, ListItem, Typography } from "@nous-research/ui";
|
||||
import { BUILTIN_THEMES, useTheme } from "@/themes";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -50,27 +50,26 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
|
|||
|
||||
return (
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
ghost
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className={cn(
|
||||
"group relative inline-flex items-center gap-1.5 px-2 py-1 text-xs",
|
||||
"text-muted-foreground hover:text-foreground transition-colors cursor-pointer",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
|
||||
)}
|
||||
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-muted-foreground hover:text-foreground"
|
||||
title={t.theme?.switchTheme ?? "Switch theme"}
|
||||
aria-label={t.theme?.switchTheme ?? "Switch theme"}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
>
|
||||
<Palette className="h-3.5 w-3.5" />
|
||||
<Typography
|
||||
mondwest
|
||||
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
</button>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Palette className="h-3.5 w-3.5" />
|
||||
|
||||
<Typography
|
||||
mondwest
|
||||
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
|
|
@ -97,20 +96,16 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
|
|||
const preset = BUILTIN_THEMES[th.name];
|
||||
|
||||
return (
|
||||
<button
|
||||
<ListItem
|
||||
key={th.name}
|
||||
type="button"
|
||||
active={isActive}
|
||||
role="option"
|
||||
aria-selected={isActive}
|
||||
onClick={() => {
|
||||
setTheme(th.name);
|
||||
close();
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 px-3 py-2 text-left transition-colors cursor-pointer",
|
||||
"hover:bg-midground/10",
|
||||
isActive ? "text-midground" : "text-midground/60",
|
||||
)}
|
||||
className="gap-3"
|
||||
>
|
||||
{preset ? (
|
||||
<ThemeSwatch theme={preset.name} />
|
||||
|
|
@ -138,7 +133,7 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
|
|||
isActive ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ListItem } from "@nous-research/ui";
|
||||
import {
|
||||
AlertCircle,
|
||||
Check,
|
||||
|
|
@ -87,12 +88,11 @@ export function ToolCall({ tool }: { tool: ToolEntry }) {
|
|||
<div
|
||||
className={`rounded-md border overflow-hidden ${STATUS_TONE[tool.status]}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<ListItem
|
||||
onClick={() => setUserOverride(!open)}
|
||||
disabled={!hasBody}
|
||||
aria-expanded={open}
|
||||
className="w-full flex items-center gap-2 px-2.5 py-1.5 text-left text-xs hover:bg-foreground/2 disabled:cursor-default cursor-pointer transition-colors"
|
||||
className="px-2.5 py-1.5 text-xs hover:bg-foreground/2 disabled:cursor-default"
|
||||
>
|
||||
{hasBody ? (
|
||||
<Chevron className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
|
|
@ -132,7 +132,7 @@ export function ToolCall({ tool }: { tool: ToolEntry }) {
|
|||
{elapsed}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</ListItem>
|
||||
|
||||
{open && hasBody && (
|
||||
<div className="border-t border-border/60 px-3 py-2 space-y-2 text-xs font-mono">
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Segmented<T extends string>({
|
||||
className,
|
||||
onChange,
|
||||
options,
|
||||
size = "sm",
|
||||
value,
|
||||
}: SegmentedProps<T>) {
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
className={cn(
|
||||
"inline-flex border border-border bg-background/30",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{options.map((opt) => {
|
||||
const active = opt.value === value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
onClick={() => onChange(opt.value)}
|
||||
className={cn(
|
||||
"font-mondwest tracking-[0.1em] uppercase",
|
||||
"transition-colors cursor-pointer whitespace-nowrap",
|
||||
"border-r border-border last:border-r-0",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30",
|
||||
size === "sm" && "h-7 px-2.5 text-[0.65rem]",
|
||||
size === "md" && "h-8 px-3 text-xs",
|
||||
active
|
||||
? "bg-foreground/90 text-background"
|
||||
: "text-muted-foreground hover:bg-foreground/10 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterGroup({
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
}: FilterGroupProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<span className="font-mondwest text-[0.65rem] tracking-[0.12em] uppercase text-muted-foreground/70">
|
||||
{label}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterGroupProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SegmentedOption<T extends string> {
|
||||
label: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
interface SegmentedProps<T extends string> {
|
||||
className?: string;
|
||||
onChange: (value: T) => void;
|
||||
options: SegmentedOption<T>[];
|
||||
size?: "sm" | "md";
|
||||
value: T;
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Switch({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
className,
|
||||
disabled,
|
||||
id,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onCheckedChange: (v: boolean) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
id?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
id={id}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center border border-border transition-colors",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
checked ? "bg-foreground/15 border-foreground/30" : "bg-background",
|
||||
className,
|
||||
)}
|
||||
onClick={() => onCheckedChange(!checked)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"pointer-events-none block h-3.5 w-3.5 transition-transform",
|
||||
checked ? "translate-x-4 bg-foreground" : "translate-x-0.5 bg-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Tabs({
|
||||
defaultValue,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
defaultValue: string;
|
||||
children: (active: string, setActive: (v: string) => void) => React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [active, setActive] = useState(defaultValue);
|
||||
return <div className={cn("flex flex-col gap-4", className)}>{children(active, setActive)}</div>;
|
||||
}
|
||||
|
||||
export function TabsList({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-start border-b border-border text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsTrigger({
|
||||
active,
|
||||
value,
|
||||
onClick,
|
||||
className,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { active: boolean; value: string }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 font-mondwest text-xs tracking-[0.1em] uppercase transition-all cursor-pointer",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
active
|
||||
? "text-foreground after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-foreground"
|
||||
: "hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue