Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

193 lines
5.5 KiB
TypeScript

"use client";
import type { CatalogProfile, ServiceName } from "./SettingsContext";
export const fieldControlClass =
"w-full rounded-lg border border-[var(--border)] px-3 py-2 text-[14px] text-[var(--foreground)] outline-none transition-colors focus:border-[var(--ring)]";
export const inputClass = `${fieldControlClass} bg-transparent placeholder:text-[var(--muted-foreground)]/40`;
export const nativeSelectClass = `${fieldControlClass} bg-[var(--background)] cursor-pointer disabled:cursor-not-allowed disabled:opacity-60`;
export const selectClass = `${nativeSelectClass} appearance-none`;
export const selectOptionClass =
"bg-[var(--background)] text-[var(--foreground)]";
export const supportedSearchProviders = [
"brave",
"tavily",
"jina",
"searxng",
"duckduckgo",
"perplexity",
] as const;
export const deprecatedSearchProviders = new Set([
"exa",
"serper",
"baidu",
"openrouter",
]);
export function stringifyExtraHeaders(
value: CatalogProfile["extra_headers"],
): string {
if (!value) return "";
if (typeof value === "string") return value;
try {
return JSON.stringify(value);
} catch {
return "";
}
}
export function statusDotClass(configured: boolean, hasError: boolean): string {
if (hasError) return "bg-red-400";
if (configured) return "bg-emerald-500";
return "bg-[var(--border)]";
}
export function formatContextWindowSource(
source: string | undefined,
t: (key: string) => string,
): string {
if (source === "manual") return t("Manual");
if (source === "metadata") return t("Auto");
if (source === "known_model") return t("Known");
if (source === "default") return t("Default");
return t("Unset");
}
export function formatContextWindowUpdatedAt(
value: string | undefined,
language: "en" | "zh",
): string {
if (!value) return "";
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return value;
return parsed.toLocaleString(language === "zh" ? "zh-CN" : "en-US", {
dateStyle: "medium",
timeStyle: "short",
});
}
export function activeProfileDetail(
profile: CatalogProfile | null,
service: ServiceName,
t: (key: string) => string,
): string {
if (!profile) return t("No profile");
if (service === "search") return profile.provider || t("No provider");
return profile.base_url || t("No endpoint");
}
export function activeModelDetail(
profile: CatalogProfile | null,
model: { model?: string; name?: string } | null,
service: ServiceName,
t: (key: string) => string,
): string {
if (service === "search") return profile?.provider || t("No provider");
return model?.model || model?.name || t("No model selected");
}
// Category-label typography. English looks good with uppercase + wide tracking;
// CJK glyphs are already square blocks so we drop both and bump size a hair.
export function labelClass(
size: "sm" | "md" | "lg",
language: "en" | "zh",
): string {
if (language === "zh") {
if (size === "sm") return "text-[10.5px] font-medium";
if (size === "lg") return "text-[12px] font-medium";
return "text-[11px] font-medium";
}
if (size === "sm")
return "text-[9.5px] font-semibold uppercase tracking-[0.16em]";
if (size === "lg") return "text-[11px] uppercase tracking-[0.16em]";
return "text-[10px] font-semibold uppercase tracking-[0.16em]";
}
// One-row settings group used on simple pages (Appearance, Status etc.).
// Title + optional description on the left, control flushed right. Matches
// the visual rhythm used on Codex-style preferences pages.
export function SettingRow({
title,
description,
control,
}: {
title: string;
description?: string;
control: React.ReactNode;
}) {
return (
<div className="flex items-start justify-between gap-6 border-t border-[var(--border)]/50 px-1 py-4 first:border-t-0">
<div className="min-w-0 flex-1">
<div className="text-[13.5px] font-medium text-[var(--foreground)]">
{title}
</div>
{description && (
<p className="mt-1 text-[12px] leading-relaxed text-[var(--muted-foreground)]">
{description}
</p>
)}
</div>
<div className="shrink-0">{control}</div>
</div>
);
}
// Page-level section group. Title + optional description, then children.
export function SettingSection({
title,
description,
children,
}: {
title: string;
description?: string;
children: React.ReactNode;
}) {
return (
<section className="mb-10">
<header className="mb-3">
<h2 className="text-[15px] font-semibold tracking-tight text-[var(--foreground)]">
{title}
</h2>
{description && (
<p className="mt-1 text-[12.5px] leading-relaxed text-[var(--muted-foreground)]">
{description}
</p>
)}
</header>
<div className="rounded-xl border border-[var(--border)]/60 bg-[var(--card)]/40 px-5">
{children}
</div>
</section>
);
}
// Page heading shared across settings sub-pages. The global Save Draft / Apply
// toolbar (which also shows where this module persists to) lives above this, so
// each page just owns its title row.
export function SettingsPageHeader({
title,
description,
}: {
title: string;
description?: string;
}) {
return (
<header className="mb-8">
<h1 className="font-serif text-[22px] font-semibold tracking-tight text-[var(--foreground)]">
{title}
</h1>
{description && (
<p className="mt-1.5 text-[13px] leading-relaxed text-[var(--muted-foreground)]">
{description}
</p>
)}
</header>
);
}