mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
* container that is scaled to fill its parent, so the world stays crisp pixel
|
||||
* art at any size.
|
||||
*/
|
||||
import debugFactory from 'debug';
|
||||
import {
|
||||
Application,
|
||||
BitmapFont,
|
||||
@@ -35,6 +36,26 @@ import { ROOM_REGISTRY, type RoomEntry } from './rooms';
|
||||
import { TextureFactory } from './textures';
|
||||
import { type AgentState, type ChatMessage, TileCode, type WalkNode } from './types';
|
||||
|
||||
const debug = debugFactory('agentworld:gameworld');
|
||||
|
||||
// The CEF/Chromium renderer used by the desktop shell does not reliably expose a
|
||||
// WebGPU adapter — `Application.init({ preference: 'webgpu' })` can wait forever
|
||||
// on adapter acquisition and never resolve, leaving the world stuck on "Booting
|
||||
// renderer...". WebGL is the dependable path in Chromium/CEF, so we request it
|
||||
// directly instead of letting Pixi probe (and hang on) WebGPU first.
|
||||
const RENDERER_PREFERENCE = 'webgl' as const;
|
||||
// Backstop in case even WebGL context creation wedges: race init against this so
|
||||
// the caller can surface an error + retry rather than spinning indefinitely.
|
||||
const INIT_TIMEOUT_MS = 10_000;
|
||||
|
||||
/** Thrown when renderer init rejects or exceeds {@link INIT_TIMEOUT_MS}. */
|
||||
export class RendererInitError extends Error {
|
||||
public constructor(message: string, options?: { cause?: unknown }) {
|
||||
super(message, options);
|
||||
this.name = 'RendererInitError';
|
||||
}
|
||||
}
|
||||
|
||||
const NAMEPLATE_FONT = 'iso-body';
|
||||
const BUBBLE_FONT = 'iso-bubble';
|
||||
const FONT_CHARACTERS: Array<Array<string> | string> = [
|
||||
@@ -217,22 +238,36 @@ export class GameWorld {
|
||||
|
||||
public async init(parent: HTMLElement): Promise<void> {
|
||||
if (this.app) {
|
||||
debug('init skipped: renderer already initialized');
|
||||
return;
|
||||
}
|
||||
debug('init start preference=%s timeoutMs=%d', RENDERER_PREFERENCE, INIT_TIMEOUT_MS);
|
||||
TextureSource.defaultOptions.scaleMode = 'nearest';
|
||||
|
||||
// Build the Application locally first; only commit it to `this.app` once init
|
||||
// fully succeeds, so a timeout/failure leaves GameWorld in its pristine
|
||||
// pre-init state and `init()` can be retried cleanly.
|
||||
const app = new Application();
|
||||
await app.init({
|
||||
width: NATIVE_RESOLUTION,
|
||||
height: NATIVE_RESOLUTION,
|
||||
antialias: true,
|
||||
preference: 'webgpu',
|
||||
powerPreference: 'high-performance',
|
||||
resolution: globalThis.devicePixelRatio || 1,
|
||||
autoDensity: true,
|
||||
backgroundAlpha: 1,
|
||||
background: 0x0c0f16,
|
||||
});
|
||||
try {
|
||||
await this.runInitWithTimeout(app);
|
||||
debug('renderer init resolved preference=%s', RENDERER_PREFERENCE);
|
||||
} catch (error: unknown) {
|
||||
// Tear down any partially-created GPU/canvas resources so a retry starts
|
||||
// from a clean slate (and we don't leak a dangling renderer).
|
||||
try {
|
||||
app.destroy?.(true, { children: true, texture: true });
|
||||
} catch (destroyError: unknown) {
|
||||
debug('init cleanup: destroy after failure threw: %s', String(destroyError));
|
||||
}
|
||||
this.app = null;
|
||||
this.factory = null;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
debug('renderer init failed: %s', message);
|
||||
throw error instanceof RendererInitError
|
||||
? error
|
||||
: new RendererInitError(`renderer init failed: ${message}`, { cause: error });
|
||||
}
|
||||
|
||||
this.app = app;
|
||||
this.factory = new TextureFactory(app.renderer);
|
||||
|
||||
@@ -258,6 +293,41 @@ export class GameWorld {
|
||||
app.ticker.add(this.tick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Race `Application.init` against {@link INIT_TIMEOUT_MS}. Resolves when the
|
||||
* renderer is ready; rejects with a {@link RendererInitError} if init rejects
|
||||
* or the timeout fires first (the CEF WebGPU-adapter hang, primarily).
|
||||
*/
|
||||
private async runInitWithTimeout(app: Application): Promise<void> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
debug('renderer init timed out after %dms', INIT_TIMEOUT_MS);
|
||||
reject(new RendererInitError(`renderer init timed out after ${INIT_TIMEOUT_MS}ms`));
|
||||
}, INIT_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
const initialize = app.init({
|
||||
width: NATIVE_RESOLUTION,
|
||||
height: NATIVE_RESOLUTION,
|
||||
antialias: true,
|
||||
preference: RENDERER_PREFERENCE,
|
||||
powerPreference: 'high-performance',
|
||||
resolution: globalThis.devicePixelRatio || 1,
|
||||
autoDensity: true,
|
||||
backgroundAlpha: 1,
|
||||
background: 0x0c0f16,
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([initialize, timeout]);
|
||||
} finally {
|
||||
if (timer !== undefined) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private installFonts(): void {
|
||||
if (fontsInstalled) {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Public surface of the isometric agent world engine. */
|
||||
|
||||
export { GameWorld } from './GameWorld';
|
||||
export { GameWorld, RendererInitError } from './GameWorld';
|
||||
export type { AgentSummary } from './GameWorld';
|
||||
export { BaseRoom } from './BaseRoom';
|
||||
export {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Tests for WorldSection — the "Tiny Place" World tab renderer boot UX.
|
||||
*
|
||||
* Regression coverage for #4038: the PixiJS renderer init promise could reject
|
||||
* or hang forever (CEF WebGPU adapter never settling), leaving a permanent
|
||||
* silent "Booting renderer..." overlay with no error and no retry. These tests
|
||||
* assert that a rejected init surfaces an error overlay + Retry button, and that
|
||||
* Retry re-runs init (recovering to the ready state on a subsequent success).
|
||||
*
|
||||
* The iso engine is mocked at module level — no real WebGL/WebGPU is touched.
|
||||
*/
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import WorldSection from './WorldSection';
|
||||
|
||||
// Controllable GameWorld test double. `initImpl` is swapped per-test so we can
|
||||
// drive resolve / reject / never-settle behavior.
|
||||
let initImpl: (parent: HTMLElement) => Promise<void>;
|
||||
const initSpy = vi.fn((parent: HTMLElement) => initImpl(parent));
|
||||
const setChangeListener = vi.fn();
|
||||
const setRoom = vi.fn();
|
||||
const spawnAgents = vi.fn();
|
||||
const setAutonomous = vi.fn();
|
||||
const destroy = vi.fn();
|
||||
|
||||
vi.mock('../iso', () => {
|
||||
class FakeGameWorld {
|
||||
public currentRoomKey = 'outside';
|
||||
public init = (parent: HTMLElement): Promise<void> => initSpy(parent);
|
||||
public setChangeListener = setChangeListener;
|
||||
public setRoom = setRoom;
|
||||
public spawnAgents = spawnAgents;
|
||||
public setAutonomous = setAutonomous;
|
||||
public destroy = destroy;
|
||||
}
|
||||
return {
|
||||
GameWorld: FakeGameWorld,
|
||||
ROOM_REGISTRY: [
|
||||
{ key: 'outside', name: 'World', description: 'A large open plaza.' },
|
||||
{ key: 'poker', name: 'Poker', description: 'Eight seats around a felt table.' },
|
||||
],
|
||||
RendererInitError: class RendererInitError extends Error {},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
initImpl = () => Promise.resolve();
|
||||
});
|
||||
|
||||
describe('WorldSection renderer boot', () => {
|
||||
test('shows booting overlay while init is in flight', () => {
|
||||
initImpl = () => new Promise<void>(() => {}); // never settles
|
||||
render(<WorldSection />);
|
||||
expect(screen.getByText(/booting renderer/i)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('hides booting overlay and wires the world once init resolves', async () => {
|
||||
initImpl = () => Promise.resolve();
|
||||
render(<WorldSection />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/booting renderer/i)).not.toBeInTheDocument();
|
||||
});
|
||||
expect(setRoom).toHaveBeenCalledWith('outside');
|
||||
expect(spawnAgents).toHaveBeenCalled();
|
||||
expect(setAutonomous).toHaveBeenCalledWith(true);
|
||||
expect(screen.queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows error overlay + Retry when init rejects', async () => {
|
||||
initImpl = () => Promise.reject(new Error('webgpu adapter hung'));
|
||||
render(<WorldSection />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/could not start the world renderer/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
|
||||
// The dead-end booting overlay must NOT also be showing.
|
||||
expect(screen.queryByText(/booting renderer/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Retry re-invokes init and recovers to ready on success', async () => {
|
||||
const user = userEvent.setup();
|
||||
// First attempt fails, second succeeds.
|
||||
initImpl = () => Promise.reject(new Error('init failed'));
|
||||
render(<WorldSection />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
|
||||
});
|
||||
expect(initSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
initImpl = () => Promise.resolve();
|
||||
await user.click(screen.getByRole('button', { name: /retry/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(initSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/could not start the world renderer/i)).not.toBeInTheDocument();
|
||||
});
|
||||
expect(setRoom).toHaveBeenCalledWith('outside');
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,11 @@ const debug = debugFactory('agentworld:world');
|
||||
const WORLD_ROOM_KEY = 'outside';
|
||||
const WORLD_POPULATION = 100;
|
||||
const ROOM_POPULATION = 8;
|
||||
// Defense-in-depth backstop: even though GameWorld.init() already races its own
|
||||
// timeout, guard against the (rare) case where the init promise never settles at
|
||||
// all — flip to an error state so the user always gets a Retry instead of an
|
||||
// indefinite "Booting renderer..." overlay.
|
||||
const BOOT_TIMEOUT_MS = 15_000;
|
||||
|
||||
const populationFor = (key: string): number =>
|
||||
key === WORLD_ROOM_KEY ? WORLD_POPULATION : ROOM_POPULATION;
|
||||
@@ -25,7 +30,10 @@ export default function WorldSection() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const worldRef = useRef<GameWorld | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [roomKey, setRoomKey] = useState(WORLD_ROOM_KEY);
|
||||
// Bumped by the Retry button to re-run the init effect from scratch.
|
||||
const [retryNonce, setRetryNonce] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
@@ -34,14 +42,35 @@ export default function WorldSection() {
|
||||
return;
|
||||
}
|
||||
|
||||
debug('mounting pixi world');
|
||||
debug('mounting pixi world attempt=%d', retryNonce);
|
||||
// Reset visible state for this (possibly retried) boot attempt.
|
||||
setReady(false);
|
||||
setError(null);
|
||||
const world = new GameWorld();
|
||||
worldRef.current = world;
|
||||
let disposed = false;
|
||||
|
||||
// Component-level backstop in case init() never settles (belt-and-braces
|
||||
// over GameWorld's own internal timeout).
|
||||
let bootTimer: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
debug('renderer boot backstop fired after %dms', BOOT_TIMEOUT_MS);
|
||||
setError(t('agentWorld.world.initError', 'Could not start the world renderer.'));
|
||||
}, BOOT_TIMEOUT_MS);
|
||||
|
||||
const clearBootTimer = (): void => {
|
||||
if (bootTimer !== undefined) {
|
||||
clearTimeout(bootTimer);
|
||||
bootTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
void world
|
||||
.init(container)
|
||||
.then(() => {
|
||||
clearBootTimer();
|
||||
if (disposed) {
|
||||
debug('renderer initialized after unmount; destroying stale world');
|
||||
world.destroy();
|
||||
@@ -56,18 +85,31 @@ export default function WorldSection() {
|
||||
setReady(true);
|
||||
debug('renderer ready room=%s population=%d', WORLD_ROOM_KEY, WORLD_POPULATION);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
debug('renderer init failed: %s', String(error));
|
||||
.catch((initError: unknown) => {
|
||||
clearBootTimer();
|
||||
debug('renderer init failed: %s', String(initError));
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setError(t('agentWorld.world.initError', 'Could not start the world renderer.'));
|
||||
});
|
||||
|
||||
return () => {
|
||||
debug('unmounting pixi world');
|
||||
disposed = true;
|
||||
clearBootTimer();
|
||||
world.setChangeListener(null);
|
||||
world.destroy();
|
||||
worldRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
// `t` is stable enough for messaging; retryNonce re-runs the whole boot.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [retryNonce]);
|
||||
|
||||
const handleRetry = (): void => {
|
||||
debug('retry requested');
|
||||
setRetryNonce(nonce => nonce + 1);
|
||||
};
|
||||
|
||||
const handleRoom = (key: string): void => {
|
||||
const world = worldRef.current;
|
||||
@@ -88,7 +130,17 @@ export default function WorldSection() {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden bg-black">
|
||||
<div ref={containerRef} className="absolute inset-0" />
|
||||
{ready ? null : (
|
||||
{error ? (
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center gap-4 px-6 text-center">
|
||||
<p className="max-w-sm text-sm text-neutral-200">{error}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-primary-500 bg-primary-500 px-4 py-2 text-sm font-medium text-white transition hover:bg-primary-600 dark:border-primary-500 dark:bg-primary-600"
|
||||
onClick={handleRetry}>
|
||||
{t('agentWorld.world.retry', 'Retry')}
|
||||
</button>
|
||||
</div>
|
||||
) : ready ? null : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-sm text-neutral-300">
|
||||
{t('agentWorld.world.booting', 'Booting renderer...')}
|
||||
</div>
|
||||
|
||||
@@ -83,6 +83,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place شبكة اجتماعية لوكلاء الذكاء الاصطناعي. استخدم OpenHuman للتفاعل والعثور على الوظائف ونشرها والتداول والنمو معًا.',
|
||||
'agentWorld.world': 'العالم',
|
||||
'agentWorld.world.booting': 'جارٍ تشغيل العارض...',
|
||||
'agentWorld.world.initError': 'تعذّر تشغيل عارض العالم.',
|
||||
'agentWorld.world.retry': 'إعادة المحاولة',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'انضم إلى tiny.place ليتمكن وكيلك من التنسيق مع الوكلاء الآخرين: العثور على الوظائف ونشرها، والتداول، وتبادل الرسائل، والتعاون في المكافآت.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place হলো এআই এজেন্টদের জন্য একটি সোশ্যাল নেটওয়ার্ক। যোগাযোগ করতে, কাজ খুঁজতে ও পোস্ট করতে, লেনদেন করতে এবং একসাথে বেড়ে উঠতে OpenHuman ব্যবহার করুন।',
|
||||
'agentWorld.world': 'বিশ্ব',
|
||||
'agentWorld.world.booting': 'রেন্ডারার চালু হচ্ছে...',
|
||||
'agentWorld.world.initError': 'ওয়ার্ল্ড রেন্ডারার চালু করা যায়নি।',
|
||||
'agentWorld.world.retry': 'আবার চেষ্টা করুন',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'tiny.place-এ যোগ দিন যাতে আপনার এজেন্ট অন্য এজেন্টদের সাথে সমন্বয় করতে পারে — কাজ খুঁজে পাওয়া ও পোস্ট করা, লেনদেন, বার্তা পাঠানো এবং বাউন্টিতে একসাথে কাজ করা।',
|
||||
|
||||
@@ -85,6 +85,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place ist ein soziales Netzwerk für KI-Agenten. Nutze OpenHuman, um zu interagieren, Jobs zu finden und zu veröffentlichen, zu handeln und gemeinsam zu wachsen.',
|
||||
'agentWorld.world': 'Welt',
|
||||
'agentWorld.world.booting': 'Renderer wird gestartet...',
|
||||
'agentWorld.world.initError': 'Der Welt-Renderer konnte nicht gestartet werden.',
|
||||
'agentWorld.world.retry': 'Wiederholen',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Tritt tiny.place bei, damit dein Agent sich mit anderen Agenten abstimmen kann: Jobs finden und ausschreiben, handeln, Nachrichten senden und bei Bounties zusammenarbeiten.',
|
||||
|
||||
@@ -30,6 +30,8 @@ const en: TranslationMap = {
|
||||
'Tiny.Place is a social network for AI agents. Use OpenHuman to interact, find and post jobs, trade, and grow together.',
|
||||
'agentWorld.world': 'World',
|
||||
'agentWorld.world.booting': 'Booting renderer...',
|
||||
'agentWorld.world.initError': 'Could not start the world renderer.',
|
||||
'agentWorld.world.retry': 'Retry',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Join tiny.place so your agent can coordinate with other agents — find and post jobs, trade, message, and team up on bounties.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place es una red social para agentes de IA. Usa OpenHuman para interactuar, encontrar y publicar trabajos, comerciar y crecer juntos.',
|
||||
'agentWorld.world': 'Mundo',
|
||||
'agentWorld.world.booting': 'Iniciando renderizador...',
|
||||
'agentWorld.world.initError': 'No se pudo iniciar el renderizador del mundo.',
|
||||
'agentWorld.world.retry': 'Reintentar',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Únete a tiny.place para que tu agente coordine con otros agentes: encontrar y publicar trabajos, comerciar, enviar mensajes y colaborar en recompensas.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place est un réseau social pour les agents IA. Utilisez OpenHuman pour interagir, trouver et publier des missions, échanger et grandir ensemble.',
|
||||
'agentWorld.world': 'Monde',
|
||||
'agentWorld.world.booting': 'Démarrage du moteur de rendu...',
|
||||
'agentWorld.world.initError': 'Impossible de démarrer le moteur de rendu du monde.',
|
||||
'agentWorld.world.retry': 'Réessayer',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Rejoignez tiny.place pour que votre agent collabore avec d’autres agents : trouver et publier des missions, échanger, discuter et coopérer sur des primes.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place एआई एजेंट्स के लिए एक सोशल नेटवर्क है। बातचीत करने, काम खोजने और पोस्ट करने, व्यापार करने और साथ मिलकर आगे बढ़ने के लिए OpenHuman का उपयोग करें।',
|
||||
'agentWorld.world': 'दुनिया',
|
||||
'agentWorld.world.booting': 'रेंडरर शुरू हो रहा है...',
|
||||
'agentWorld.world.initError': 'वर्ल्ड रेंडरर शुरू नहीं किया जा सका।',
|
||||
'agentWorld.world.retry': 'पुनः प्रयास करें',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'tiny.place से जुड़ें ताकि आपका एजेंट दूसरे एजेंट्स के साथ तालमेल कर सके — काम ढूँढना और पोस्ट करना, व्यापार करना, संदेश भेजना और बाउंटी पर मिलकर काम करना।',
|
||||
|
||||
@@ -83,6 +83,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place adalah jejaring sosial untuk agen AI. Gunakan OpenHuman untuk berinteraksi, menemukan dan memasang pekerjaan, berdagang, serta tumbuh bersama.',
|
||||
'agentWorld.world': 'Dunia',
|
||||
'agentWorld.world.booting': 'Memulai perender...',
|
||||
'agentWorld.world.initError': 'Tidak dapat memulai perender dunia.',
|
||||
'agentWorld.world.retry': 'Coba lagi',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Bergabunglah dengan tiny.place agar agen Anda dapat berkoordinasi dengan agen lain: menemukan dan memposting pekerjaan, berdagang, berkirim pesan, dan bekerja sama dalam bounty.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place è un social network per agenti IA. Usa OpenHuman per interagire, trovare e pubblicare lavori, scambiare e crescere insieme.',
|
||||
'agentWorld.world': 'Mondo',
|
||||
'agentWorld.world.booting': 'Avvio del renderer...',
|
||||
'agentWorld.world.initError': 'Impossibile avviare il renderer del mondo.',
|
||||
'agentWorld.world.retry': 'Riprova',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Unisciti a tiny.place per far coordinare il tuo agente con altri agenti: trovare e pubblicare lavori, scambiare, messaggiare e collaborare alle taglie.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place는 AI 에이전트를 위한 소셜 네트워크입니다. OpenHuman을 사용해 소통하고, 일자리를 찾고 올리며, 거래하고 함께 성장하세요.',
|
||||
'agentWorld.world': '월드',
|
||||
'agentWorld.world.booting': '렌더러를 시작하는 중...',
|
||||
'agentWorld.world.initError': '월드 렌더러를 시작할 수 없습니다.',
|
||||
'agentWorld.world.retry': '다시 시도',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'tiny.place에 참여하면 에이전트가 다른 에이전트와 협업할 수 있습니다: 작업을 찾고 게시하고, 거래하고, 메시지를 주고받고, 바운티에 함께 참여하세요.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place to sieć społecznościowa dla agentów AI. Używaj OpenHuman, aby wchodzić w interakcje, znajdować i publikować zlecenia, handlować i wspólnie się rozwijać.',
|
||||
'agentWorld.world': 'Świat',
|
||||
'agentWorld.world.booting': 'Uruchamianie renderera...',
|
||||
'agentWorld.world.initError': 'Nie udało się uruchomić renderera świata.',
|
||||
'agentWorld.world.retry': 'Ponów',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Dołącz do tiny.place, aby Twój agent współpracował z innymi agentami: znajdował i publikował zlecenia, handlował, wysyłał wiadomości i działał przy nagrodach.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place é uma rede social para agentes de IA. Use o OpenHuman para interagir, encontrar e publicar trabalhos, negociar e crescer juntos.',
|
||||
'agentWorld.world': 'Mundo',
|
||||
'agentWorld.world.booting': 'Iniciando renderizador...',
|
||||
'agentWorld.world.initError': 'Não foi possível iniciar o renderizador do mundo.',
|
||||
'agentWorld.world.retry': 'Tentar novamente',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Junte-se ao tiny.place para que seu agente coordene com outros agentes: encontrar e publicar trabalhos, negociar, enviar mensagens e colaborar em recompensas.',
|
||||
|
||||
@@ -84,6 +84,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place — это социальная сеть для ИИ-агентов. Используйте OpenHuman, чтобы взаимодействовать, находить и публиковать задания, торговать и расти вместе.',
|
||||
'agentWorld.world': 'Мир',
|
||||
'agentWorld.world.booting': 'Запуск рендерера...',
|
||||
'agentWorld.world.initError': 'Не удалось запустить рендерер мира.',
|
||||
'agentWorld.world.retry': 'Повторить',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'Присоединяйтесь к tiny.place, чтобы ваш агент взаимодействовал с другими агентами: находил и публиковал задания, торговал, обменивался сообщениями и работал над наградами.',
|
||||
|
||||
@@ -83,6 +83,8 @@ const messages: TranslationMap = {
|
||||
'Tiny.Place 是面向 AI 智能体的社交网络。使用 OpenHuman 来互动、查找和发布工作、交易并共同成长。',
|
||||
'agentWorld.world': '世界',
|
||||
'agentWorld.world.booting': '正在启动渲染器...',
|
||||
'agentWorld.world.initError': '无法启动世界渲染器。',
|
||||
'agentWorld.world.retry': '重试',
|
||||
'agentWorld.world.title': 'Tiny Place',
|
||||
'agentWorld.world.description':
|
||||
'加入 tiny.place,让你的代理与其他代理协作:查找和发布任务、交易、收发消息以及共同完成悬赏。',
|
||||
|
||||
Reference in New Issue
Block a user