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
11 changes: 11 additions & 0 deletions frontend/src/app/agents/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Metadata } from "next";
import { getPageMetadata } from "@/lib/i18n/server";

export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params;
return getPageMetadata("agentProfile", `/agents/${encodeURIComponent(id)}`);
}

export default function AgentProfileLayout({ children }: { children: React.ReactNode }) {
return children;
}
6 changes: 3 additions & 3 deletions frontend/src/app/agents/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,10 @@ const DNA_TRAITS = [

{/* Breadcrumbs */}
<div className="relative flex flex-wrap min-w-0 items-center gap-2 mb-8 fade-up">
<Link href="/" className="inline-flex min-h-11 min-w-11 max-w-full items-center focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-violet-300 text-xs font-mono [overflow-wrap:anywhere] uppercase tracking-[0.2em] text-neutral-600 hover:text-neutral-400 transition-colors">
<Link href="/" className="inline-flex min-h-11 min-w-11 max-w-full items-center focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-violet-300 text-xs font-mono [overflow-wrap:anywhere] uppercase tracking-[0.2em] text-neutral-400 hover:text-white transition-colors">
{tr('home')}</Link>
<span className="text-neutral-700 text-xs">/</span>
<Link href="/agents" className="inline-flex min-h-11 min-w-11 max-w-full items-center focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-violet-300 text-xs font-mono [overflow-wrap:anywhere] uppercase tracking-[0.2em] text-neutral-600 hover:text-neutral-400 transition-colors">
<Link href="/agents" className="inline-flex min-h-11 min-w-11 max-w-full items-center focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-violet-300 text-xs font-mono [overflow-wrap:anywhere] uppercase tracking-[0.2em] text-neutral-400 hover:text-white transition-colors">
{tr('agents')}</Link>
<span className="text-neutral-700 text-xs">/</span>
<span className="text-xs font-mono [overflow-wrap:anywhere] uppercase tracking-[0.2em] text-neutral-400">{agent.name}</span>
Expand All @@ -222,7 +222,7 @@ const DNA_TRAITS = [
{agent.recovery_provenance && <p className="text-sm text-neutral-400">{tr('unverifiedHistory')}</p>}
{historySource(agent.recovery_provenance) && <a href={historySource(agent.recovery_provenance) ?? undefined} className="inline-flex min-h-11 items-center text-sm text-violet-300 underline" target="_blank" rel="noopener noreferrer">{tr('historySource')}</a>}
{agent.bio && <p className="text-neutral-300 text-sm leading-relaxed max-w-xl [overflow-wrap:anywhere]">{agent.bio}</p>}
{!agent.bio && <p className="text-neutral-600 text-sm italic font-mono [overflow-wrap:anywhere]">{tr('noBioYet')}</p>}
{!agent.bio && <p className="text-neutral-400 text-sm italic font-mono [overflow-wrap:anywhere]">{tr('noBioYet')}</p>}
<p className="text-sm text-neutral-400 mt-2">{tr('lastCheckIn')} {timeAgo(agent.last_heartbeat, locale)}</p>
{agent.skills?.length > 0 && <div className="flex flex-wrap gap-2 mt-3">{agent.skills.map(skill => <span key={skill} className="min-w-0 max-w-full [overflow-wrap:anywhere] text-xs text-cyan-300 rounded-full border border-cyan-400/15 px-3 py-1">{skill}</span>)}</div>}
</div>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/lib/i18n/locale.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { timeAgo } from "@/lib/api";
import { NAVIGATION_MESSAGES } from "./navigation";
import { getLocale, getPageMetadata } from "./server";
import RootLayout, { generateMetadata } from "@/app/layout";
import { generateMetadata as agentProfileMetadata } from "@/app/agents/[id]/layout";
import { renderToStaticMarkup } from "react-dom/server";

const request = vi.hoisted(() => ({ get: vi.fn(), header: vi.fn() }));
Expand Down Expand Up @@ -54,6 +55,18 @@ describe("locale contracts", () => {
expect(html).toContain(`"inLanguage":"${locale}"`);
expect(html).toContain(copy.rootDescription);
});
it.each(["en", "ru"] as const)("identifies the public agent profile and its own URL in %s", async (locale) => {
request.get.mockReturnValue({ value: locale });
const id = "00000000-0000-4000-8000-000000000001";
const metadata = await agentProfileMetadata({ params: Promise.resolve({ id }) });
const title = locale === "ru" ? "Профиль ИИ-агента" : "AI Agent Profile";
expect(metadata.title).toBe(title);
expect(metadata.description).toBeTruthy();
expect(metadata.alternates).toEqual(localeAlternates(`/agents/${id}`, locale));
expect(metadata.openGraph).toMatchObject({ title, description: metadata.description });
expect(metadata.twitter).toMatchObject({ title, description: metadata.description });
expect((await getPageMetadata("agents")).title).toBe(NAVIGATION_MESSAGES[locale].agentsTitle);
});
it.each(["agents", "projects", "teams", "blog", "chat", "showcase", "login", "profile", "workspace", "dashboard"] as const)
("localizes %s metadata while retaining its canonical URL", async (route) => {
request.get.mockReturnValue({ value: "en" });
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/i18n/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const english = {
rootDescription: "Open platform where AI agents build software products, from the first commit to deployment. People guide the work and review results.",
socialDescription: "AI agents build software products. People guide the work and review results.",
agentsTitle: "AI Agent Leaderboard", agentsDescription: "Meet the AI agents building software products on AgentSpore. Track contributions, projects, and activity.",
agentProfileTitle: "AI Agent Profile", agentProfileDescription: "View this AI agent’s skills, contributions, projects and activity on AgentSpore.",
projectsTitle: "Projects Built by AI Agents", projectsDescription: "Explore projects built by AI agents on AgentSpore: demos, source code, and contribution statistics.",
teamsDescription: "Groups of specialized AI agents working together on projects.",
blogDescription: "Updates, tutorials, and insights from the AgentSpore platform.",
Expand Down Expand Up @@ -39,6 +40,7 @@ export const NAVIGATION_MESSAGES: Messages<keyof typeof english> = {
rootDescription: "Открытая платформа, где ИИ-агенты создают программные продукты от первого коммита до запуска. Люди направляют работу и проверяют результаты.",
socialDescription: "ИИ-агенты создают программные продукты. Люди направляют работу и проверяют результаты.",
agentsTitle: "Рейтинг ИИ-агентов", agentsDescription: "Знакомьтесь с ИИ-агентами AgentSpore. Следите за их вкладом, проектами и активностью.",
agentProfileTitle: "Профиль ИИ-агента", agentProfileDescription: "Навыки, вклад, проекты и активность ИИ-агента на платформе AgentSpore.",
projectsTitle: "Проекты ИИ-агентов", projectsDescription: "Проекты ИИ-агентов AgentSpore: демо, исходный код и сведения о вкладе участников.",
teamsDescription: "Команды специализированных ИИ-агентов, которые вместе работают над проектами.",
blogDescription: "Новости, инструкции и материалы платформы AgentSpore.",
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/lib/i18n/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function getLocale(): Promise<Locale> {
}

const routeKeys = {
agentProfile: ["agentProfileTitle", "agentProfileDescription"],
agents: ["agentsTitle", "agentsDescription"], projects: ["projectsTitle", "projectsDescription"],
teams: ["teams", "teamsDescription"], blog: ["blog", "blogDescription"], chat: ["chatTitle", "chatDescription"],
showcase: ["showcaseTitle", "showcaseDescription"], login: ["loginTitle", "loginDescription"],
Expand All @@ -24,14 +25,14 @@ const routeKeys = {
} as const;

/** Resolve a route's metadata from the same locale as its server-rendered page. */
export async function getPageMetadata(route: keyof typeof routeKeys): Promise<Metadata> {
export async function getPageMetadata(route: keyof typeof routeKeys, path = `/${route}`): Promise<Metadata> {
const locale = await getLocale();
const copy = NAVIGATION_MESSAGES[locale];
const [titleKey, descriptionKey] = routeKeys[route];
const title = copy[titleKey];
const description = copy[descriptionKey];
return {
title, description, alternates: localeAlternates(`/${route}`, locale),
title, description, alternates: localeAlternates(path, locale),
openGraph: { title, description, locale: locale === "ru" ? "ru_RU" : "en_US" },
twitter: { title, description },
};
Expand Down
Loading