feat(tinyplace): public profile viewer for arbitrary handles (#4931) (#4997)

This commit is contained in:
Mega Mind
2026-07-18 06:44:18 +03:00
committed by GitHub
parent 2114ad3320
commit f1de40cafa
17 changed files with 971 additions and 3 deletions
+9 -3
View File
@@ -25,6 +25,7 @@ import LedgerSection from './LedgerSection';
import MarketplaceSection from './MarketplaceSection';
import MessagingSection from './MessagingSection';
import ProfilesSection from './ProfilesSection';
import ProfileViewer from './ProfileViewer';
import WelcomeSection from './WelcomeSection';
import WorldSection from './WorldSection';
@@ -112,9 +113,12 @@ export default function AgentWorld() {
// Derive the active slug from the current sub-path
// e.g. /agent-world/explore → 'explore'
const pathParts = location.pathname.split('/');
const activeSlug = pathParts[pathParts.length - 1] || 'welcome';
const activeSection = activeSlug === 'agent-world' ? 'welcome' : activeSlug;
// Take the FIRST segment after `/agent-world`, not the last — nested routes
// like `/agent-world/profiles/:username` must still resolve to the `profiles`
// section (keeping the sidebar selection) rather than the trailing param.
const pathParts = location.pathname.split('/').filter(Boolean);
const awIndex = pathParts.indexOf('agent-world');
const activeSection = (awIndex >= 0 ? pathParts[awIndex + 1] : undefined) || 'welcome';
const isWorld = activeSection === 'world';
// The welcome landing sits flush (no framed card) so its centered pitch reads
// as part of the page, not boxed inside a panel.
@@ -174,6 +178,8 @@ export default function AgentWorld() {
{/* === AGENT-WORLD SECTION ROUTES (append one per section) === */}
<Route path="directory" element={<DirectorySection />} />
<Route path="profiles" element={<ProfilesSection />} />
{/* Public viewer for an arbitrary handle (own/other users/agents) — #4931 */}
<Route path="profiles/:username" element={<ProfileViewer />} />
<Route path="identities" element={<IdentitiesSection />} />
<Route path="marketplace" element={<MarketplaceSection />} />
<Route path="messaging" element={<MessagingSection />} />
@@ -0,0 +1,182 @@
/**
* Tests for ProfileViewer — the Agent World public profile viewer (#4931).
*
* The viewer renders an ARBITRARY handle's profile via `graphql.profile`, with a
* follow/unfollow button (`follows.follow`/`unfollow`, follow-state from
* `follows.following`) and a copy-link affordance. apiClient + wallet are mocked;
* all handles/ids are generic placeholders. These behaviours do not exist before
* this change (the route + component are new), so the file is the regression.
*/
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { type GqlProfile } from '../../lib/agentworld/invokeApiClient';
import { fetchWalletStatus } from '../../services/walletApi';
import { apiClient } from '../AgentWorldShell';
import ProfileViewer from './ProfileViewer';
vi.mock('../AgentWorldShell', () => ({
apiClient: {
graphql: { profile: vi.fn(), agentCard: vi.fn() },
follows: { follow: vi.fn(), unfollow: vi.fn(), stats: vi.fn() },
},
}));
vi.mock('../../services/walletApi', () => ({ fetchWalletStatus: vi.fn() }));
const graphqlProfile = vi.mocked(apiClient.graphql.profile);
const graphqlAgentCard = vi.mocked(apiClient.graphql.agentCard);
const followsFollow = vi.mocked(apiClient.follows.follow);
const followsUnfollow = vi.mocked(apiClient.follows.unfollow);
const followsStats = vi.mocked(apiClient.follows.stats);
const walletStatus = vi.mocked(fetchWalletStatus);
/** Agent card carrying the signer-aware follow flag the viewer reads. */
function agentCardFollowing(viewerIsFollowing: boolean) {
return { agentId: PROFILE_ADDR, viewerIsFollowing };
}
const PROFILE_ADDR = 'ProfiLeSoLanaAddr00000000001';
const VIEWER_ADDR = 'ViewerSoLanaAddr00000000002';
function makeProfile(overrides: Partial<GqlProfile> = {}): GqlProfile {
return {
cryptoId: PROFILE_ADDR,
actorType: 'agent',
displayName: 'Alice Agent',
bio: 'An autonomous test agent.',
private: false,
createdAt: '2026-01-02T00:00:00Z',
updatedAt: '2026-01-02T00:00:00Z',
verified: true,
attestations: [],
agentCard: null,
identities: [
{
username: 'alice',
cryptoId: PROFILE_ADDR,
publicKey: 'pk',
registeredAt: '2026-01-02T00:00:00Z',
expiresAt: '2027-01-02T00:00:00Z',
status: 'active',
updatedAt: '2026-01-02T00:00:00Z',
primary: true,
},
],
...overrides,
};
}
function walletWith(address: string | null) {
return { accounts: address ? [{ chain: 'solana', address }] : [] } as unknown as Awaited<
ReturnType<typeof fetchWalletStatus>
>;
}
function renderViewer(username = 'alice') {
return render(
<MemoryRouter initialEntries={[`/agent-world/profiles/${username}`]}>
<Routes>
<Route path="/agent-world/profiles/:username" element={<ProfileViewer />} />
</Routes>
</MemoryRouter>
);
}
beforeEach(() => {
vi.clearAllMocks();
graphqlProfile.mockResolvedValue(makeProfile());
// Default: viewer does not follow this agent yet.
graphqlAgentCard.mockResolvedValue(agentCardFollowing(false));
followsFollow.mockResolvedValue({ follower: VIEWER_ADDR, followee: PROFILE_ADDR, createdAt: '' });
followsUnfollow.mockResolvedValue(undefined);
followsStats.mockResolvedValue({ agentId: PROFILE_ADDR, followerCount: 3, followingCount: 5 });
walletStatus.mockResolvedValue(walletWith(VIEWER_ADDR));
});
describe('ProfileViewer', () => {
test('renders an arbitrary handle profile (not the wallet owner)', async () => {
renderViewer('alice');
// Looked up by the route param, not the wallet.
await waitFor(() => expect(graphqlProfile).toHaveBeenCalledWith('alice'));
// '@alice' shows in the header and again in the owned-handles list; assert
// it renders at all (not a specific count) so the query stays unambiguous.
expect((await screen.findAllByText('@alice')).length).toBeGreaterThan(0);
expect(screen.getByText('An autonomous test agent.')).toBeInTheDocument();
// Follower stats load in their own effect (a second async tick after the
// profile), so await rather than assert synchronously.
expect(await screen.findByText('3')).toBeInTheDocument();
});
test('shows a not-found state when the profile does not exist', async () => {
graphqlProfile.mockResolvedValue(null);
renderViewer('ghost');
expect(await screen.findByText(/profile not found/i)).toBeInTheDocument();
});
test('follow button follows then unfollows another user', async () => {
const user = userEvent.setup();
renderViewer('alice');
// Button appears once the wallet resolves and follow-state loads.
const followBtn = await screen.findByRole('button', { name: 'Follow' });
await waitFor(() => expect(followBtn).toBeEnabled());
// Follower count starts at 3 (from follows.stats).
expect(await screen.findByText('3')).toBeInTheDocument();
await user.click(followBtn);
expect(followsFollow).toHaveBeenCalledWith(PROFILE_ADDR);
const followingBtn = await screen.findByRole('button', { name: 'Following' });
// Count tracks the follow optimistically: 3 -> 4.
expect(await screen.findByText('4')).toBeInTheDocument();
await user.click(followingBtn);
expect(followsUnfollow).toHaveBeenCalledWith(PROFILE_ADDR);
expect(await screen.findByRole('button', { name: 'Follow' })).toBeInTheDocument();
// ...and back to 3 on unfollow.
expect(await screen.findByText('3')).toBeInTheDocument();
});
test('pre-selects the following state from the agent card follow flag', async () => {
graphqlAgentCard.mockResolvedValue(agentCardFollowing(true));
renderViewer('alice');
// viewerIsFollowing:true → button shows Following without a click.
expect(await screen.findByRole('button', { name: 'Following' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Follow' })).not.toBeInTheDocument();
});
test('hides the follow button when the follow relationship is unknown', async () => {
// No agent card (e.g. a non-agent profile) → no follow flag → the button is
// hidden rather than defaulting to an inferred state.
graphqlAgentCard.mockResolvedValue(null);
renderViewer('alice');
await screen.findByTestId('profile-copy-link');
expect(screen.queryByRole('button', { name: 'Follow' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Following' })).not.toBeInTheDocument();
});
test('hides the follow button and marks self when viewing own profile', async () => {
walletStatus.mockResolvedValue(walletWith(PROFILE_ADDR));
renderViewer('alice');
expect(await screen.findByText(/this is your profile/i)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Follow' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Following' })).not.toBeInTheDocument();
});
test('copy-link affordance copies the shareable deep link', async () => {
// Install a clipboard spy and drive the click with fireEvent — NOT
// userEvent, whose setup() replaces navigator.clipboard with its own stub
// and would shadow this spy.
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
renderViewer('alice');
const copyBtn = await screen.findByTestId('profile-copy-link');
fireEvent.click(copyBtn);
await waitFor(() => expect(writeText).toHaveBeenCalled());
expect(writeText.mock.calls[0][0] as string).toContain('#/agent-world/profiles/alice');
});
});
+508
View File
@@ -0,0 +1,508 @@
/**
* ProfileViewer — Agent World public profile viewer for an ARBITRARY handle.
*
* Route: `/agent-world/profiles/:username`. Where `ProfilesSection` is hard-wired
* to the connected wallet, this viewer renders any user's or agent's profile via
* the already-plumbed read handlers: `graphql.profile(username)` (a rich
* `GqlProfile`) and `graphql.agentCard(cryptoId)` (the agent card + the
* signer-aware `viewerIsFollowing` flag), plus `follows.stats`. It adds a
* follow / unfollow button (reusing `follows.follow` / `follows.unfollow`) and a
* copy-link affordance so a profile is shareable.
*
* Read-only. Profile EDITING lives in `ProfilesSection` and is tracked
* separately (#4930); this component never mutates the viewed profile.
*/
import debug from 'debug';
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import PanelScaffold from '../../components/layout/PanelScaffold';
import Button from '../../components/ui/Button';
import {
type AgentCard,
type FollowStats,
type GqlAttestation,
type GqlProfile,
type Identity,
PaymentRequiredError,
} from '../../lib/agentworld/invokeApiClient';
import { useT } from '../../lib/i18n/I18nContext';
import { fetchWalletStatus } from '../../services/walletApi';
import { apiClient } from '../AgentWorldShell';
const log = debug('agentworld:profileviewer');
// ── Helpers ─────────────────────────────────────────────────────────────────────
/** Sanitized error label for diagnostics — an error NAME/kind only, never the
* raw message (which may carry backend internals / PII). */
function errorKind(err: unknown): string {
if (err instanceof PaymentRequiredError) return 'payment_required';
if (err instanceof Error) return err.name || 'Error';
return 'unknown';
}
function truncateCryptoId(cryptoId: string): string {
if (cryptoId.length <= 12) return cryptoId;
return `${cryptoId.slice(0, 6)}${cryptoId.slice(-4)}`;
}
/** Format an ISO date using the runtime locale (never a hard-coded language). */
function formatDate(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
}
/** Pick the primary identity, else the first. */
function pickPrimary<T extends { primary?: boolean }>(identities: T[]): T | undefined {
return identities.find(i => i.primary) ?? identities[0];
}
/** Read the signer-aware `viewerIsFollowing` flag off an agent card, if present. */
function readViewerFollows(card: AgentCard | null): boolean | null {
const value = card?.['viewerIsFollowing'];
return typeof value === 'boolean' ? value : null;
}
/** Resolve the wallet's Solana address (the viewer's tiny.place cryptoId). */
function useMyAgentId(): string | null {
const [agentId, setAgentId] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
log('[agentworld:profileviewer] resolving wallet agent id');
void fetchWalletStatus()
.then(status => {
if (cancelled) return;
const solana = (status.accounts ?? []).find(a => a.chain === 'solana');
if (solana?.address) setAgentId(solana.address);
else log('[agentworld:profileviewer] no solana wallet account; viewer is signed out');
})
.catch((err: unknown) => {
if (!cancelled) log('[agentworld:profileviewer] wallet resolve failed: %s', errorKind(err));
});
return () => {
cancelled = true;
};
}, []);
return agentId;
}
// ── Profile fetch ────────────────────────────────────────────────────────────────
type ViewerState =
| { status: 'loading' }
| { status: 'not_found' }
| { status: 'error' }
| { status: 'ok'; profile: GqlProfile };
/**
* Load the target handle's public profile, keyed by the normalized handle so a
* stale response for a previous handle can never surface the wrong profile. The
* effect never calls setState synchronously — the only state writes happen in
* the async resolve/reject, satisfying the repo's no-sync-setState-in-effect
* rule. `null` from the server → not found.
*/
function useProfile(handle: string): ViewerState {
const [entry, setEntry] = useState<{ key: string; state: ViewerState }>({
key: '',
state: { status: 'loading' },
});
useEffect(() => {
if (!handle) return;
let cancelled = false;
log('[agentworld:profileviewer] loading profile');
void apiClient.graphql
.profile(handle)
.then(profile => {
if (cancelled) return;
log('[agentworld:profileviewer] profile %s', profile ? 'loaded' : 'not found');
setEntry({
key: handle,
state: profile ? { status: 'ok', profile } : { status: 'not_found' },
});
})
.catch((err: unknown) => {
if (cancelled) return;
log('[agentworld:profileviewer] profile load failed: %s', errorKind(err));
setEntry({ key: handle, state: { status: 'error' } });
});
return () => {
cancelled = true;
};
}, [handle]);
if (!handle) return { status: 'not_found' };
// Until THIS handle's response lands, show loading — prevents a previous
// handle's profile (and its follow/share actions) from flashing.
return entry.key === handle ? entry.state : { status: 'loading' };
}
// ── Agent card (also carries the signer-aware follow flag) ───────────────────────
/** Fetch the agent card for `cryptoId`. Returns `null` for non-agent profiles or
* on failure — callers degrade to "unknown". */
function useAgentCard(cryptoId: string): AgentCard | null {
const [card, setCard] = useState<AgentCard | null>(null);
useEffect(() => {
if (!cryptoId) return;
let cancelled = false;
log('[agentworld:profileviewer] loading agent card');
void apiClient.graphql
.agentCard(cryptoId)
.then(c => {
if (!cancelled) setCard(c);
})
.catch((err: unknown) => {
if (!cancelled)
log('[agentworld:profileviewer] agent card load failed: %s', errorKind(err));
});
return () => {
cancelled = true;
};
}, [cryptoId]);
return card;
}
// ── Follow control ─────────────────────────────────────────────────────────────
type FollowState = 'unknown' | 'following' | 'not_following';
/**
* Follow relationship for `targetCryptoId`, derived ONLY from the direct,
* signer-aware `viewerFollowsHint` (from the agent card). It never infers a
* relationship from partial or failed data: when the hint is absent it stays
* `'unknown'` and the button is hidden, rather than defaulting to "not
* following" and risking a follow() on an existing relationship. A user toggle
* takes optimistic precedence via `override`.
*/
function useFollow(
myAgentId: string | null,
targetCryptoId: string,
viewerFollowsHint: boolean | null,
onFollowChange?: (next: 'following' | 'not_following') => void
) {
const [override, setOverride] = useState<'following' | 'not_following' | null>(null);
const [busy, setBusy] = useState(false);
const isSelf = myAgentId != null && myAgentId === targetCryptoId;
const enabled = myAgentId != null && targetCryptoId !== '' && !isSelf;
const state: FollowState =
override ??
(viewerFollowsHint == null ? 'unknown' : viewerFollowsHint ? 'following' : 'not_following');
const toggle = useCallback(async () => {
if (busy || !enabled || state === 'unknown') return;
setBusy(true);
const next = state === 'following' ? 'not_following' : 'following';
try {
if (state === 'following') await apiClient.follows.unfollow(targetCryptoId);
else await apiClient.follows.follow(targetCryptoId);
setOverride(next);
// Keep the follower count in step with the button (it is fetched once and
// would otherwise lag behind after a follow/unfollow).
onFollowChange?.(next);
// No PII: only the resulting action, never the address/handle.
log('[agentworld:profileviewer] follow toggled -> %s', next);
} catch (err) {
log('[agentworld:profileviewer] follow toggle failed: %s', errorKind(err));
} finally {
setBusy(false);
}
}, [busy, enabled, state, targetCryptoId, onFollowChange]);
return { state, busy, isSelf, enabled, toggle };
}
// ── Follow stats sub-hook ────────────────────────────────────────────────────────
function useFollowStats(cryptoId: string): FollowStats | null {
const [stats, setStats] = useState<FollowStats | null>(null);
useEffect(() => {
if (!cryptoId) return;
let cancelled = false;
void apiClient.follows
.stats(cryptoId)
.then(s => {
if (!cancelled) setStats(s);
})
.catch((err: unknown) => {
if (!cancelled)
log('[agentworld:profileviewer] follow stats load failed: %s', errorKind(err));
});
return () => {
cancelled = true;
};
}, [cryptoId]);
return stats;
}
// ── Presentational bits ──────────────────────────────────────────────────────────
function StatusBlock({ tone, title, body }: { tone: string; title: string; body?: string }) {
return (
<div className="flex h-64 flex-col items-center justify-center gap-2 text-center">
<p className={`text-base font-medium ${tone}`}>{title}</p>
{body && <p className="max-w-md text-sm text-content-muted">{body}</p>}
</div>
);
}
// ── Main export ──────────────────────────────────────────────────────────────────
export default function ProfileViewer() {
const { username } = useParams<{ username: string }>();
const { t } = useT();
const routeHandle = (username ?? '').replace(/^@+/, '').trim();
const state = useProfile(routeHandle);
let body: React.ReactNode;
if (state.status === 'loading') {
body = (
<div className="flex h-64 items-center justify-center text-content-faint">
<span className="animate-pulse text-sm">{t('agentWorld.profileViewer.loading')}</span>
</div>
);
} else if (state.status === 'not_found') {
body = (
<StatusBlock
tone="text-content-secondary"
title={t('agentWorld.profileViewer.notFoundTitle')}
body={t('agentWorld.profileViewer.notFoundBody')}
/>
);
} else if (state.status === 'error') {
// Generic, translated copy only — never the raw external error string.
body = (
<StatusBlock
tone="text-red-600 dark:text-red-400"
title={t('agentWorld.profileViewer.errorTitle')}
/>
);
} else {
body = <ProfileCard profile={state.profile} routeHandle={routeHandle} />;
}
return (
<PanelScaffold description={t('agentWorld.profileViewer.description')}>{body}</PanelScaffold>
);
}
// ── Profile card ─────────────────────────────────────────────────────────────────
function ProfileCard({ profile, routeHandle }: { profile: GqlProfile; routeHandle: string }) {
const { t } = useT();
const myAgentId = useMyAgentId();
const cryptoId = profile.cryptoId;
const agentCard = useAgentCard(cryptoId);
// Optimistic follower-count adjustment so the count tracks the follow button
// (stats are fetched once and would otherwise lag after a toggle).
const [followerDelta, setFollowerDelta] = useState(0);
const follow = useFollow(myAgentId, cryptoId, readViewerFollows(agentCard), next =>
setFollowerDelta(d => d + (next === 'following' ? 1 : -1))
);
const followStats = useFollowStats(cryptoId);
const [copied, setCopied] = useState(false);
// Fall back to the initials monogram if the avatar image fails to load.
const [avatarBroken, setAvatarBroken] = useState(false);
const primaryIdentity = pickPrimary(profile.identities ?? []);
const primaryUsername = primaryIdentity?.username ?? null;
const hasHandle = primaryUsername !== null;
const usernameClean = (primaryUsername ?? profile.displayName ?? '').replace(/^@+/, '');
const handle = hasHandle ? `@${usernameClean}` : usernameClean;
const displayName = profile.displayName || usernameClean || '?';
const initials = displayName.slice(0, 2).toUpperCase();
const skills = profile.tags ?? [];
const attestations: GqlAttestation[] = profile.attestations ?? [];
const ownedIdentities: Identity[] = profile.identities ?? [];
const actorType = profile.actorType ?? '';
const isHuman = actorType.toLowerCase() === 'human';
const showFollow = follow.enabled && follow.state !== 'unknown';
// Read-only agent-card summary (only when it adds something over the profile).
const cardDescription = (agentCard?.description ?? '').trim();
const copyLink = useCallback(() => {
// Build the deep link from the ROUTE handle (what the user actually
// navigated to), not from `identities`/`displayName` — those may be null or
// differ from the selected route, producing a link that does not resolve.
const { origin, pathname } = window.location;
const link = `${origin}${pathname}#/agent-world/profiles/${encodeURIComponent(routeHandle)}`;
void navigator.clipboard
?.writeText(link)
.then(() => {
setCopied(true);
log('[agentworld:profileviewer] copied share link');
window.setTimeout(() => setCopied(false), 2000);
})
.catch((err: unknown) => {
log('[agentworld:profileviewer] copy link failed: %s', errorKind(err));
});
}, [routeHandle]);
return (
<div className="rounded-lg border border-line bg-surface p-4">
{/* Header row: identity + actions */}
<div className="flex items-start gap-4">
{profile.avatarUrl && !avatarBroken ? (
<img
src={profile.avatarUrl}
alt={displayName}
onError={() => setAvatarBroken(true)}
className="h-14 w-14 shrink-0 rounded-full object-cover"
/>
) : (
<div className="bg-primary-600 flex h-14 w-14 shrink-0 items-center justify-center rounded-full text-lg font-semibold text-content-inverted">
{initials}
</div>
)}
<div className="min-w-0 flex-1">
<h3 className="flex items-center gap-1.5 text-sm font-semibold text-content">
<span className="truncate">{handle}</span>
{profile.verified && (
<span className="text-xs text-blue-500" title="Verified">
&#10003;
</span>
)}
{actorType && (
<span
className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${
isHuman
? 'bg-emerald-50 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-300'
: 'bg-violet-50 text-violet-600 dark:bg-violet-900/30 dark:text-violet-300'
}`}>
{isHuman
? t('agentWorld.profileViewer.humanBadge')
: t('agentWorld.profileViewer.agentBadge')}
</span>
)}
</h3>
{cryptoId && (
<p className="mt-0.5 font-mono text-xs text-content-muted" title={cryptoId}>
{truncateCryptoId(cryptoId)}
</p>
)}
{profile.bio && (
<p className="mt-1.5 text-xs leading-relaxed text-content-secondary">{profile.bio}</p>
)}
</div>
{/* Actions */}
<div className="flex shrink-0 flex-col items-end gap-1.5">
{follow.isSelf ? (
<span className="text-[11px] text-content-faint">
{t('agentWorld.profileViewer.ownProfile')}
</span>
) : showFollow ? (
<Button
variant={follow.state === 'following' ? 'secondary' : 'primary'}
size="sm"
disabled={follow.busy}
onClick={() => void follow.toggle()}>
{follow.state === 'following'
? t('agentWorld.profileViewer.following')
: t('agentWorld.profileViewer.follow')}
</Button>
) : null}
<Button variant="tertiary" size="sm" onClick={copyLink} data-testid="profile-copy-link">
{copied
? t('agentWorld.profileViewer.linkCopied')
: t('agentWorld.profileViewer.copyLink')}
</Button>
</div>
</div>
{cardDescription && cardDescription !== (profile.bio ?? '').trim() && (
<div className="mt-4 border-t border-line pt-4">
<h4 className="mb-2 text-xs font-medium text-content">
{t('agentWorld.profileViewer.agentCard')}
</h4>
<p className="text-xs leading-relaxed text-content-secondary">{cardDescription}</p>
</div>
)}
{skills.length > 0 && (
<div className="mt-4 border-t border-line pt-4">
<h4 className="mb-2 text-xs font-medium text-content">
{t('agentWorld.profileViewer.skills')}
</h4>
<div className="flex flex-wrap gap-1.5">
{skills.map(skill => (
<span
key={skill}
className="rounded-full bg-surface-subtle px-2 py-0.5 text-xs text-content-secondary">
{skill}
</span>
))}
</div>
</div>
)}
{attestations.length > 0 && (
<div className="mt-4 border-t border-line pt-4">
<h4 className="mb-2 text-xs font-medium text-content">
{t('agentWorld.profileViewer.verifiedAccounts')}
</h4>
<div className="flex flex-wrap gap-2">
{attestations.map(a => (
<span
key={a.attestationId}
className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-0.5 text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
{a.platform}: {a.handle}
</span>
))}
</div>
</div>
)}
{ownedIdentities.length > 0 && (
<div className="mt-4 border-t border-line pt-4">
<h4 className="mb-2 text-xs font-medium text-content">
{t('agentWorld.profileViewer.handlesOwned')}
</h4>
<div className="flex flex-wrap gap-1.5">
{ownedIdentities.map(id => (
<span
key={id.username}
className="rounded-full border border-line px-2 py-0.5 text-xs text-content-secondary">
@{id.username.replace(/^@+/, '')}
</span>
))}
</div>
</div>
)}
{followStats && (
<div className="mt-4 border-t border-line pt-4">
<div className="flex gap-6">
<div>
<span className="text-sm font-semibold text-content">
{Math.max(0, followStats.followerCount + followerDelta)}
</span>
<span className="ml-1 text-xs text-content-muted">
{t('agentWorld.profileViewer.followers')}
</span>
</div>
<div>
<span className="text-sm font-semibold text-content">
{followStats.followingCount}
</span>
<span className="ml-1 text-xs text-content-muted">
{t('agentWorld.profileViewer.followingCount')}
</span>
</div>
</div>
</div>
)}
{profile.createdAt && (
<div className="mt-4 border-t border-line pt-4">
<span className="text-xs text-content-muted">
{t('agentWorld.profileViewer.joined')} {formatDate(profile.createdAt)}
</span>
</div>
)}
</div>
);
}
+19
View File
@@ -443,6 +443,25 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'جارٍ تحميل المزيد…',
'agentWorld.feed.loadMoreError': 'تعذّر تحميل المزيد من المنشورات. حاول مرة أخرى.',
'agentWorld.ledger': 'السجل',
'agentWorld.profileViewer.description': 'الملف الشخصي العام',
'agentWorld.profileViewer.agentCard': 'بطاقة الوكيل',
'agentWorld.profileViewer.loading': 'جارٍ تحميل الملف الشخصي…',
'agentWorld.profileViewer.notFoundTitle': 'الملف الشخصي غير موجود',
'agentWorld.profileViewer.notFoundBody': 'لا يوجد ملف شخصي منشور لهذا المعرّف حتى الآن.',
'agentWorld.profileViewer.errorTitle': 'تعذّر تحميل الملف الشخصي',
'agentWorld.profileViewer.follow': 'متابعة',
'agentWorld.profileViewer.following': 'تتابعه',
'agentWorld.profileViewer.copyLink': 'نسخ الرابط',
'agentWorld.profileViewer.linkCopied': 'تم نسخ الرابط',
'agentWorld.profileViewer.skills': 'المهارات',
'agentWorld.profileViewer.verifiedAccounts': 'الحسابات الموثّقة',
'agentWorld.profileViewer.handlesOwned': 'المعرّفات المملوكة',
'agentWorld.profileViewer.followers': 'متابِعون',
'agentWorld.profileViewer.followingCount': 'يتابع',
'agentWorld.profileViewer.joined': 'انضم في',
'agentWorld.profileViewer.ownProfile': 'هذا ملفك الشخصي',
'agentWorld.profileViewer.agentBadge': 'وكيل',
'agentWorld.profileViewer.humanBadge': 'إنسان',
'agentWorld.ledger.loadMore': 'تحميل المزيد',
'agentWorld.ledger.loadingMore': 'جارٍ تحميل المزيد…',
'agentWorld.ledger.loadMoreError': 'تعذّر تحميل المزيد من المعاملات. حاول مرة أخرى.',
+19
View File
@@ -459,6 +459,25 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'আরও লোড হচ্ছে…',
'agentWorld.feed.loadMoreError': 'আরও পোস্ট লোড করা যায়নি। আবার চেষ্টা করুন।',
'agentWorld.ledger': 'লেজার',
'agentWorld.profileViewer.description': 'সর্বজনীন প্রোফাইল',
'agentWorld.profileViewer.agentCard': 'এজেন্ট কার্ড',
'agentWorld.profileViewer.loading': 'প্রোফাইল লোড হচ্ছে…',
'agentWorld.profileViewer.notFoundTitle': 'প্রোফাইল পাওয়া যায়নি',
'agentWorld.profileViewer.notFoundBody': 'এই হ্যান্ডেলের জন্য এখনও কোনো প্রকাশিত প্রোফাইল নেই।',
'agentWorld.profileViewer.errorTitle': 'প্রোফাইল লোড করা যায়নি',
'agentWorld.profileViewer.follow': 'অনুসরণ',
'agentWorld.profileViewer.following': 'অনুসরণ করছেন',
'agentWorld.profileViewer.copyLink': 'লিঙ্ক কপি করুন',
'agentWorld.profileViewer.linkCopied': 'লিঙ্ক কপি হয়েছে',
'agentWorld.profileViewer.skills': 'দক্ষতা',
'agentWorld.profileViewer.verifiedAccounts': 'যাচাই করা অ্যাকাউন্ট',
'agentWorld.profileViewer.handlesOwned': 'মালিকানাধীন হ্যান্ডেল',
'agentWorld.profileViewer.followers': 'অনুসারী',
'agentWorld.profileViewer.followingCount': 'অনুসরণ করছেন',
'agentWorld.profileViewer.joined': 'যোগ দিয়েছেন',
'agentWorld.profileViewer.ownProfile': 'এটি আপনার প্রোফাইল',
'agentWorld.profileViewer.agentBadge': 'এজেন্ট',
'agentWorld.profileViewer.humanBadge': 'মানুষ',
'agentWorld.ledger.loadMore': 'আরও লোড করুন',
'agentWorld.ledger.loadingMore': 'আরও লোড হচ্ছে…',
'agentWorld.ledger.loadMoreError': 'আরও লেনদেন লোড করা যায়নি। আবার চেষ্টা করুন।',
+20
View File
@@ -484,6 +484,26 @@ const messages: TranslationMap = {
'agentWorld.feed.loadMoreError':
'Weitere Beiträge konnten nicht geladen werden. Bitte erneut versuchen.',
'agentWorld.ledger': 'Kontobuch',
'agentWorld.profileViewer.description': 'Öffentliches Profil',
'agentWorld.profileViewer.agentCard': 'Agentenkarte',
'agentWorld.profileViewer.loading': 'Profil wird geladen…',
'agentWorld.profileViewer.notFoundTitle': 'Profil nicht gefunden',
'agentWorld.profileViewer.notFoundBody':
'Für dieses Handle gibt es noch kein veröffentlichtes Profil.',
'agentWorld.profileViewer.errorTitle': 'Profil konnte nicht geladen werden',
'agentWorld.profileViewer.follow': 'Folgen',
'agentWorld.profileViewer.following': 'Folge ich',
'agentWorld.profileViewer.copyLink': 'Link kopieren',
'agentWorld.profileViewer.linkCopied': 'Link kopiert',
'agentWorld.profileViewer.skills': 'Fähigkeiten',
'agentWorld.profileViewer.verifiedAccounts': 'Verifizierte Konten',
'agentWorld.profileViewer.handlesOwned': 'Eigene Handles',
'agentWorld.profileViewer.followers': 'Follower',
'agentWorld.profileViewer.followingCount': 'folgt',
'agentWorld.profileViewer.joined': 'Beigetreten',
'agentWorld.profileViewer.ownProfile': 'Das ist dein Profil',
'agentWorld.profileViewer.agentBadge': 'Agent',
'agentWorld.profileViewer.humanBadge': 'Mensch',
'agentWorld.ledger.loadMore': 'Mehr laden',
'agentWorld.ledger.loadingMore': 'Wird geladen…',
'agentWorld.ledger.loadMoreError':
+19
View File
@@ -188,6 +188,25 @@ const en: TranslationMap = {
'agentWorld.feed.loadingMore': 'Loading more…',
'agentWorld.feed.loadMoreError': 'Could not load more posts. Try again.',
'agentWorld.ledger': 'Ledger',
'agentWorld.profileViewer.description': 'Public profile',
'agentWorld.profileViewer.agentCard': 'Agent card',
'agentWorld.profileViewer.loading': 'Loading profile…',
'agentWorld.profileViewer.notFoundTitle': 'Profile not found',
'agentWorld.profileViewer.notFoundBody': 'No published profile exists for this handle yet.',
'agentWorld.profileViewer.errorTitle': 'Failed to load profile',
'agentWorld.profileViewer.follow': 'Follow',
'agentWorld.profileViewer.following': 'Following',
'agentWorld.profileViewer.copyLink': 'Copy link',
'agentWorld.profileViewer.linkCopied': 'Link copied',
'agentWorld.profileViewer.skills': 'Skills',
'agentWorld.profileViewer.verifiedAccounts': 'Verified accounts',
'agentWorld.profileViewer.handlesOwned': 'Handles owned',
'agentWorld.profileViewer.followers': 'followers',
'agentWorld.profileViewer.followingCount': 'following',
'agentWorld.profileViewer.joined': 'Joined',
'agentWorld.profileViewer.ownProfile': 'This is your profile',
'agentWorld.profileViewer.agentBadge': 'Agent',
'agentWorld.profileViewer.humanBadge': 'Human',
'agentWorld.ledger.loadMore': 'Load more',
'agentWorld.ledger.loadingMore': 'Loading more…',
'agentWorld.ledger.loadMoreError': 'Could not load more transactions. Try again.',
+20
View File
@@ -469,6 +469,26 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'Cargando más…',
'agentWorld.feed.loadMoreError': 'No se pudieron cargar más publicaciones. Inténtalo de nuevo.',
'agentWorld.ledger': 'Libro mayor',
'agentWorld.profileViewer.description': 'Perfil público',
'agentWorld.profileViewer.agentCard': 'Tarjeta del agente',
'agentWorld.profileViewer.loading': 'Cargando perfil…',
'agentWorld.profileViewer.notFoundTitle': 'Perfil no encontrado',
'agentWorld.profileViewer.notFoundBody':
'Todavía no existe un perfil publicado para este identificador.',
'agentWorld.profileViewer.errorTitle': 'No se pudo cargar el perfil',
'agentWorld.profileViewer.follow': 'Seguir',
'agentWorld.profileViewer.following': 'Siguiendo',
'agentWorld.profileViewer.copyLink': 'Copiar enlace',
'agentWorld.profileViewer.linkCopied': 'Enlace copiado',
'agentWorld.profileViewer.skills': 'Habilidades',
'agentWorld.profileViewer.verifiedAccounts': 'Cuentas verificadas',
'agentWorld.profileViewer.handlesOwned': 'Identificadores en propiedad',
'agentWorld.profileViewer.followers': 'seguidores',
'agentWorld.profileViewer.followingCount': 'siguiendo',
'agentWorld.profileViewer.joined': 'Se unió',
'agentWorld.profileViewer.ownProfile': 'Este es tu perfil',
'agentWorld.profileViewer.agentBadge': 'Agente',
'agentWorld.profileViewer.humanBadge': 'Humano',
'agentWorld.ledger.loadMore': 'Cargar más',
'agentWorld.ledger.loadingMore': 'Cargando más…',
'agentWorld.ledger.loadMoreError': 'No se pudieron cargar más transacciones. Inténtalo de nuevo.',
+19
View File
@@ -479,6 +479,25 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'Chargement…',
'agentWorld.feed.loadMoreError': 'Impossible de charger plus de publications. Réessayez.',
'agentWorld.ledger': 'Grand livre',
'agentWorld.profileViewer.description': 'Profil public',
'agentWorld.profileViewer.agentCard': 'Fiche agent',
'agentWorld.profileViewer.loading': 'Chargement du profil…',
'agentWorld.profileViewer.notFoundTitle': 'Profil introuvable',
'agentWorld.profileViewer.notFoundBody': 'Aucun profil publié pour ce handle pour le moment.',
'agentWorld.profileViewer.errorTitle': 'Échec du chargement du profil',
'agentWorld.profileViewer.follow': 'Suivre',
'agentWorld.profileViewer.following': 'Suivi',
'agentWorld.profileViewer.copyLink': 'Copier le lien',
'agentWorld.profileViewer.linkCopied': 'Lien copié',
'agentWorld.profileViewer.skills': 'Compétences',
'agentWorld.profileViewer.verifiedAccounts': 'Comptes vérifiés',
'agentWorld.profileViewer.handlesOwned': 'Handles détenus',
'agentWorld.profileViewer.followers': 'abonnés',
'agentWorld.profileViewer.followingCount': 'abonnements',
'agentWorld.profileViewer.joined': 'Inscrit le',
'agentWorld.profileViewer.ownProfile': 'Ceci est votre profil',
'agentWorld.profileViewer.agentBadge': 'Agent',
'agentWorld.profileViewer.humanBadge': 'Humain',
'agentWorld.ledger.loadMore': 'Charger plus',
'agentWorld.ledger.loadingMore': 'Chargement…',
'agentWorld.ledger.loadMoreError': 'Impossible de charger plus de transactions. Réessayez.',
+19
View File
@@ -459,6 +459,25 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'और लोड हो रहा है…',
'agentWorld.feed.loadMoreError': 'अधिक पोस्ट लोड नहीं हो सके। फिर से प्रयास करें।',
'agentWorld.ledger': 'खाता बही',
'agentWorld.profileViewer.description': 'सार्वजनिक प्रोफ़ाइल',
'agentWorld.profileViewer.agentCard': 'एजेंट कार्ड',
'agentWorld.profileViewer.loading': 'प्रोफ़ाइल लोड हो रही है…',
'agentWorld.profileViewer.notFoundTitle': 'प्रोफ़ाइल नहीं मिली',
'agentWorld.profileViewer.notFoundBody': 'इस हैंडल के लिए अभी तक कोई प्रकाशित प्रोफ़ाइल नहीं है।',
'agentWorld.profileViewer.errorTitle': 'प्रोफ़ाइल लोड नहीं हो सकी',
'agentWorld.profileViewer.follow': 'फ़ॉलो करें',
'agentWorld.profileViewer.following': 'फ़ॉलो कर रहे हैं',
'agentWorld.profileViewer.copyLink': 'लिंक कॉपी करें',
'agentWorld.profileViewer.linkCopied': 'लिंक कॉपी हो गया',
'agentWorld.profileViewer.skills': 'कौशल',
'agentWorld.profileViewer.verifiedAccounts': 'सत्यापित खाते',
'agentWorld.profileViewer.handlesOwned': 'स्वामित्व वाले हैंडल',
'agentWorld.profileViewer.followers': 'फ़ॉलोअर्स',
'agentWorld.profileViewer.followingCount': 'फ़ॉलोइंग',
'agentWorld.profileViewer.joined': 'शामिल हुए',
'agentWorld.profileViewer.ownProfile': 'यह आपकी प्रोफ़ाइल है',
'agentWorld.profileViewer.agentBadge': 'एजेंट',
'agentWorld.profileViewer.humanBadge': 'मानव',
'agentWorld.ledger.loadMore': 'और लोड करें',
'agentWorld.ledger.loadingMore': 'और लोड हो रहा है…',
'agentWorld.ledger.loadMoreError': 'अधिक लेन-देन लोड नहीं हो सके। फिर से प्रयास करें।',
+19
View File
@@ -465,6 +465,25 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'Memuat lagi…',
'agentWorld.feed.loadMoreError': 'Tidak dapat memuat postingan lainnya. Coba lagi.',
'agentWorld.ledger': 'Buku Besar',
'agentWorld.profileViewer.description': 'Profil publik',
'agentWorld.profileViewer.agentCard': 'Kartu agen',
'agentWorld.profileViewer.loading': 'Memuat profil…',
'agentWorld.profileViewer.notFoundTitle': 'Profil tidak ditemukan',
'agentWorld.profileViewer.notFoundBody': 'Belum ada profil yang dipublikasikan untuk handel ini.',
'agentWorld.profileViewer.errorTitle': 'Gagal memuat profil',
'agentWorld.profileViewer.follow': 'Ikuti',
'agentWorld.profileViewer.following': 'Mengikuti',
'agentWorld.profileViewer.copyLink': 'Salin tautan',
'agentWorld.profileViewer.linkCopied': 'Tautan disalin',
'agentWorld.profileViewer.skills': 'Keterampilan',
'agentWorld.profileViewer.verifiedAccounts': 'Akun terverifikasi',
'agentWorld.profileViewer.handlesOwned': 'Handel yang dimiliki',
'agentWorld.profileViewer.followers': 'pengikut',
'agentWorld.profileViewer.followingCount': 'mengikuti',
'agentWorld.profileViewer.joined': 'Bergabung',
'agentWorld.profileViewer.ownProfile': 'Ini profil Anda',
'agentWorld.profileViewer.agentBadge': 'Agen',
'agentWorld.profileViewer.humanBadge': 'Manusia',
'agentWorld.ledger.loadMore': 'Muat lebih banyak',
'agentWorld.ledger.loadingMore': 'Memuat lagi…',
'agentWorld.ledger.loadMoreError': 'Tidak dapat memuat transaksi lainnya. Coba lagi.',
+20
View File
@@ -472,6 +472,26 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'Caricamento…',
'agentWorld.feed.loadMoreError': 'Impossibile caricare altri post. Riprova.',
'agentWorld.ledger': 'Registro',
'agentWorld.profileViewer.description': 'Profilo pubblico',
'agentWorld.profileViewer.agentCard': 'Scheda agente',
'agentWorld.profileViewer.loading': 'Caricamento profilo…',
'agentWorld.profileViewer.notFoundTitle': 'Profilo non trovato',
'agentWorld.profileViewer.notFoundBody':
'Non esiste ancora un profilo pubblicato per questo handle.',
'agentWorld.profileViewer.errorTitle': 'Impossibile caricare il profilo',
'agentWorld.profileViewer.follow': 'Segui',
'agentWorld.profileViewer.following': 'Stai già seguendo',
'agentWorld.profileViewer.copyLink': 'Copia link',
'agentWorld.profileViewer.linkCopied': 'Link copiato',
'agentWorld.profileViewer.skills': 'Competenze',
'agentWorld.profileViewer.verifiedAccounts': 'Account verificati',
'agentWorld.profileViewer.handlesOwned': 'Handle posseduti',
'agentWorld.profileViewer.followers': 'follower',
'agentWorld.profileViewer.followingCount': 'seguiti',
'agentWorld.profileViewer.joined': 'Iscritto',
'agentWorld.profileViewer.ownProfile': 'Questo è il tuo profilo',
'agentWorld.profileViewer.agentBadge': 'Agente',
'agentWorld.profileViewer.humanBadge': 'Umano',
'agentWorld.ledger.loadMore': 'Carica altro',
'agentWorld.ledger.loadingMore': 'Caricamento…',
'agentWorld.ledger.loadMoreError': 'Impossibile caricare altre transazioni. Riprova.',
+19
View File
@@ -452,6 +452,25 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': '더 불러오는 중…',
'agentWorld.feed.loadMoreError': '게시물을 더 불러오지 못했습니다. 다시 시도하세요.',
'agentWorld.ledger': '원장',
'agentWorld.profileViewer.description': '공개 프로필',
'agentWorld.profileViewer.agentCard': '에이전트 카드',
'agentWorld.profileViewer.loading': '프로필 불러오는 중…',
'agentWorld.profileViewer.notFoundTitle': '프로필을 찾을 수 없음',
'agentWorld.profileViewer.notFoundBody': '이 핸들에 대해 게시된 프로필이 아직 없습니다.',
'agentWorld.profileViewer.errorTitle': '프로필을 불러오지 못했습니다',
'agentWorld.profileViewer.follow': '팔로우',
'agentWorld.profileViewer.following': '팔로잉',
'agentWorld.profileViewer.copyLink': '링크 복사',
'agentWorld.profileViewer.linkCopied': '링크 복사됨',
'agentWorld.profileViewer.skills': '기술',
'agentWorld.profileViewer.verifiedAccounts': '인증된 계정',
'agentWorld.profileViewer.handlesOwned': '보유한 핸들',
'agentWorld.profileViewer.followers': '팔로워',
'agentWorld.profileViewer.followingCount': '팔로잉',
'agentWorld.profileViewer.joined': '가입일',
'agentWorld.profileViewer.ownProfile': '내 프로필입니다',
'agentWorld.profileViewer.agentBadge': '에이전트',
'agentWorld.profileViewer.humanBadge': '사람',
'agentWorld.ledger.loadMore': '더 보기',
'agentWorld.ledger.loadingMore': '더 불러오는 중…',
'agentWorld.ledger.loadMoreError': '거래를 더 불러오지 못했습니다. 다시 시도하세요.',
+20
View File
@@ -470,6 +470,26 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'Ładowanie…',
'agentWorld.feed.loadMoreError': 'Nie udało się załadować kolejnych postów. Spróbuj ponownie.',
'agentWorld.ledger': 'Księga',
'agentWorld.profileViewer.description': 'Profil publiczny',
'agentWorld.profileViewer.agentCard': 'Karta agenta',
'agentWorld.profileViewer.loading': 'Ładowanie profilu…',
'agentWorld.profileViewer.notFoundTitle': 'Nie znaleziono profilu',
'agentWorld.profileViewer.notFoundBody':
'Dla tej nazwy użytkownika nie ma jeszcze opublikowanego profilu.',
'agentWorld.profileViewer.errorTitle': 'Nie udało się załadować profilu',
'agentWorld.profileViewer.follow': 'Obserwuj',
'agentWorld.profileViewer.following': 'Obserwujesz',
'agentWorld.profileViewer.copyLink': 'Kopiuj link',
'agentWorld.profileViewer.linkCopied': 'Link skopiowany',
'agentWorld.profileViewer.skills': 'Umiejętności',
'agentWorld.profileViewer.verifiedAccounts': 'Zweryfikowane konta',
'agentWorld.profileViewer.handlesOwned': 'Posiadane nazwy użytkownika',
'agentWorld.profileViewer.followers': 'obserwujący',
'agentWorld.profileViewer.followingCount': 'obserwowani',
'agentWorld.profileViewer.joined': 'Dołączono',
'agentWorld.profileViewer.ownProfile': 'To jest Twój profil',
'agentWorld.profileViewer.agentBadge': 'Agent',
'agentWorld.profileViewer.humanBadge': 'Człowiek',
'agentWorld.ledger.loadMore': 'Załaduj więcej',
'agentWorld.ledger.loadingMore': 'Ładowanie…',
'agentWorld.ledger.loadMoreError':
+20
View File
@@ -464,6 +464,26 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'Carregando mais…',
'agentWorld.feed.loadMoreError': 'Não foi possível carregar mais publicações. Tente novamente.',
'agentWorld.ledger': 'Livro-razão',
'agentWorld.profileViewer.description': 'Perfil público',
'agentWorld.profileViewer.agentCard': 'Cartão do agente',
'agentWorld.profileViewer.loading': 'Carregando perfil…',
'agentWorld.profileViewer.notFoundTitle': 'Perfil não encontrado',
'agentWorld.profileViewer.notFoundBody':
'Ainda não existe um perfil publicado para este identificador.',
'agentWorld.profileViewer.errorTitle': 'Falha ao carregar o perfil',
'agentWorld.profileViewer.follow': 'Seguir',
'agentWorld.profileViewer.following': 'Seguindo',
'agentWorld.profileViewer.copyLink': 'Copiar link',
'agentWorld.profileViewer.linkCopied': 'Link copiado',
'agentWorld.profileViewer.skills': 'Habilidades',
'agentWorld.profileViewer.verifiedAccounts': 'Contas verificadas',
'agentWorld.profileViewer.handlesOwned': 'Handles próprios',
'agentWorld.profileViewer.followers': 'seguidores',
'agentWorld.profileViewer.followingCount': 'seguindo',
'agentWorld.profileViewer.joined': 'Entrou',
'agentWorld.profileViewer.ownProfile': 'Este é o seu perfil',
'agentWorld.profileViewer.agentBadge': 'Agente',
'agentWorld.profileViewer.humanBadge': 'Humano',
'agentWorld.ledger.loadMore': 'Carregar mais',
'agentWorld.ledger.loadingMore': 'Carregando mais…',
'agentWorld.ledger.loadMoreError': 'Não foi possível carregar mais transações. Tente novamente.',
+20
View File
@@ -464,6 +464,26 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': 'Загрузка…',
'agentWorld.feed.loadMoreError': 'Не удалось загрузить больше публикаций. Попробуйте ещё раз.',
'agentWorld.ledger': 'Реестр',
'agentWorld.profileViewer.description': 'Публичный профиль',
'agentWorld.profileViewer.agentCard': 'Карточка агента',
'agentWorld.profileViewer.loading': 'Загрузка профиля…',
'agentWorld.profileViewer.notFoundTitle': 'Профиль не найден',
'agentWorld.profileViewer.notFoundBody':
'Для этого идентификатора пока нет опубликованного профиля.',
'agentWorld.profileViewer.errorTitle': 'Не удалось загрузить профиль',
'agentWorld.profileViewer.follow': 'Подписаться',
'agentWorld.profileViewer.following': 'Вы подписаны',
'agentWorld.profileViewer.copyLink': 'Скопировать ссылку',
'agentWorld.profileViewer.linkCopied': 'Ссылка скопирована',
'agentWorld.profileViewer.skills': 'Навыки',
'agentWorld.profileViewer.verifiedAccounts': 'Подтверждённые аккаунты',
'agentWorld.profileViewer.handlesOwned': 'Собственные хэндлы',
'agentWorld.profileViewer.followers': 'подписчиков',
'agentWorld.profileViewer.followingCount': 'подписок',
'agentWorld.profileViewer.joined': 'Дата присоединения',
'agentWorld.profileViewer.ownProfile': 'Это ваш профиль',
'agentWorld.profileViewer.agentBadge': 'Агент',
'agentWorld.profileViewer.humanBadge': 'Человек',
'agentWorld.ledger.loadMore': 'Загрузить ещё',
'agentWorld.ledger.loadingMore': 'Загрузка…',
'agentWorld.ledger.loadMoreError': 'Не удалось загрузить больше транзакций. Попробуйте ещё раз.',
+19
View File
@@ -428,6 +428,25 @@ const messages: TranslationMap = {
'agentWorld.feed.loadingMore': '正在加载…',
'agentWorld.feed.loadMoreError': '无法加载更多帖子,请重试。',
'agentWorld.ledger': '账本',
'agentWorld.profileViewer.description': '公开资料',
'agentWorld.profileViewer.agentCard': '智能体卡片',
'agentWorld.profileViewer.loading': '正在加载资料…',
'agentWorld.profileViewer.notFoundTitle': '未找到资料',
'agentWorld.profileViewer.notFoundBody': '此账号句柄尚未发布资料。',
'agentWorld.profileViewer.errorTitle': '无法加载资料',
'agentWorld.profileViewer.follow': '关注',
'agentWorld.profileViewer.following': '已关注',
'agentWorld.profileViewer.copyLink': '复制链接',
'agentWorld.profileViewer.linkCopied': '链接已复制',
'agentWorld.profileViewer.skills': '技能',
'agentWorld.profileViewer.verifiedAccounts': '已验证账户',
'agentWorld.profileViewer.handlesOwned': '拥有的句柄',
'agentWorld.profileViewer.followers': '关注者',
'agentWorld.profileViewer.followingCount': '正在关注',
'agentWorld.profileViewer.joined': '加入于',
'agentWorld.profileViewer.ownProfile': '这是你的资料',
'agentWorld.profileViewer.agentBadge': '智能体',
'agentWorld.profileViewer.humanBadge': '人类',
'agentWorld.ledger.loadMore': '加载更多',
'agentWorld.ledger.loadingMore': '正在加载…',
'agentWorld.ledger.loadMoreError': '无法加载更多交易,请重试。',