From e5434b9d262a10db41a69dc5a785c5e26317a623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreu=20Mart=C3=ADnez=20Vidal?= Date: Sat, 21 Mar 2026 16:16:26 +0100 Subject: [PATCH 1/7] Add Unity MCP Semantic Kernel integration Introduce a new Unity MCP Semantic Kernel integration package under backend/app/agents/unity_mcp_sk. Adds core contracts and value objects, a default tool-definition mapper and parser, an MCP client adapter, a semantic plugin runtime, and SK integration helpers (kernel factory and registration helpers supporting expanded and router modes). Includes unit tests for discovery, registration, and metadata fidelity, and documents usage/behavior in docs/UNITY_MCP_SK_INTEGRATION.md. Also bump project versions to 1.8.0 (root, backend, frontend) and extend .gitignore/.dockerignore entries to exclude virtualenvs, editor folders, and coverage artifacts. --- .dockerignore | 14 ++ .gitignore | 14 ++ backend/app/agents/unity_mcp_sk/__init__.py | 19 ++ .../app/agents/unity_mcp_sk/core/__init__.py | 2 + .../app/agents/unity_mcp_sk/core/contracts.py | 88 +++++++++ .../agents/unity_mcp_sk/core/tool_mapper.py | 98 ++++++++++ .../unity_mcp_sk/infrastructure/__init__.py | 2 + .../unity_mcp_sk/infrastructure/mcp_client.py | 40 ++++ .../agents/unity_mcp_sk/plugin/__init__.py | 2 + .../plugin/unity_mcp_semantic_plugin.py | 63 ++++++ .../unity_mcp_sk/sk_integration/__init__.py | 2 + .../sk_integration/kernel_factory.py | 52 +++++ .../sk_integration/kernel_registration.py | 184 ++++++++++++++++++ backend/package.json | 2 +- backend/pyproject.toml | 2 +- .../tests/test_unity_mcp_sk_integration.py | 136 +++++++++++++ docs/UNITY_MCP_SK_INTEGRATION.md | 97 +++++---- frontend/.dockerignore | 14 ++ frontend/package.json | 2 +- package.json | 2 +- 20 files changed, 793 insertions(+), 42 deletions(-) create mode 100644 backend/app/agents/unity_mcp_sk/__init__.py create mode 100644 backend/app/agents/unity_mcp_sk/core/__init__.py create mode 100644 backend/app/agents/unity_mcp_sk/core/contracts.py create mode 100644 backend/app/agents/unity_mcp_sk/core/tool_mapper.py create mode 100644 backend/app/agents/unity_mcp_sk/infrastructure/__init__.py create mode 100644 backend/app/agents/unity_mcp_sk/infrastructure/mcp_client.py create mode 100644 backend/app/agents/unity_mcp_sk/plugin/__init__.py create mode 100644 backend/app/agents/unity_mcp_sk/plugin/unity_mcp_semantic_plugin.py create mode 100644 backend/app/agents/unity_mcp_sk/sk_integration/__init__.py create mode 100644 backend/app/agents/unity_mcp_sk/sk_integration/kernel_factory.py create mode 100644 backend/app/agents/unity_mcp_sk/sk_integration/kernel_registration.py create mode 100644 backend/tests/test_unity_mcp_sk_integration.py diff --git a/.dockerignore b/.dockerignore index 7271739c..488f2014 100644 --- a/.dockerignore +++ b/.dockerignore @@ -55,3 +55,17 @@ temp_extracted_asar dist-electron/ dist-electron-build/ out/ + +.venv +.venv-local +.venv-build +.vscode +.idea +.pytest_cache +.coverage +.coverage.* +.coverage.*.* +.coverage.*.*.* +.coverage.*.*.*.* +.coverage.*.*.*.*.* +.coverage.*.*.*.*.*.* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1ade672d..802e8fd5 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,17 @@ BUILD_STATUS.md dist-electron/ dist-electron-build/ out/ + +.venv +.venv-local +.venv-build +.vscode +.idea +.pytest_cache +.coverage +.coverage.* +.coverage.*.* +.coverage.*.*.* +.coverage.*.*.*.* +.coverage.*.*.*.*.* +.coverage.*.*.*.*.*.* \ No newline at end of file diff --git a/backend/app/agents/unity_mcp_sk/__init__.py b/backend/app/agents/unity_mcp_sk/__init__.py new file mode 100644 index 00000000..e143fa94 --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/__init__.py @@ -0,0 +1,19 @@ +""" +Semantic Kernel integration for Unity MCP tools. + +This package supports two registration modes: +- Expanded mode: one SK function per MCP tool (recommended). +- Router mode: one generic SK function for tool routing. +""" + +from .plugin.unity_mcp_semantic_plugin import UnityMcpSemanticPlugin +from .sk_integration.kernel_factory import create_kernel_with_unity_async +from .sk_integration.kernel_registration import register_unity_router_function, register_unity_tools_as_functions + +__all__ = [ + "UnityMcpSemanticPlugin", + "create_kernel_with_unity_async", + "register_unity_router_function", + "register_unity_tools_as_functions", +] + diff --git a/backend/app/agents/unity_mcp_sk/core/__init__.py b/backend/app/agents/unity_mcp_sk/core/__init__.py new file mode 100644 index 00000000..95b4429a --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/core/__init__.py @@ -0,0 +1,2 @@ +"""Core contracts and mappers for Unity MCP SK integration.""" + diff --git a/backend/app/agents/unity_mcp_sk/core/contracts.py b/backend/app/agents/unity_mcp_sk/core/contracts.py new file mode 100644 index 00000000..47aaac88 --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/core/contracts.py @@ -0,0 +1,88 @@ +"""Core contracts and value objects for Unity MCP SK integration.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Protocol + + +MCP_JSON_TYPE_TO_SK_TYPE: dict[str, str] = { + "string": "string", + "number": "number", + "integer": "integer", + "boolean": "boolean", + "array": "array", + "object": "object", +} + + +@dataclass(frozen=True) +class ToolParameterDefinition: + """ + Represents an MCP tool parameter as a typed, SK-ready value object. + + Args: + name: Exact parameter name from MCP schema. + description: Human-readable parameter description. + json_type: MCP JSON schema type. + required: Whether the parameter is required. + default: Optional default value from MCP schema. + schema: Full original JSON schema fragment for this parameter. + """ + + name: str + description: str + json_type: str + required: bool + default: Any = None + schema: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class ToolDefinition: + """ + Represents one discovered MCP tool definition. + + Args: + name: MCP tool name. + description: Tool description exposed to SK planners/agents. + parameters: Parsed input parameters from MCP input schema. + return_schema: Optional output schema metadata when available. + raw_input_schema: Raw MCP input schema for metadata passthrough. + """ + + name: str + description: str + parameters: tuple[ToolParameterDefinition, ...] + return_schema: dict[str, Any] = field(default_factory=dict) + raw_input_schema: dict[str, Any] = field(default_factory=dict) + + +class ToolDefinitionMapper(Protocol): + """Maps/discovers MCP tool definitions and stores a deterministic registry.""" + + def initialize(self, tool_definitions: list[ToolDefinition]) -> None: + """Initialize mapper with tool definitions.""" + + def map_tool_definition(self, tool_definition: ToolDefinition) -> ToolDefinition: + """Normalize a tool definition for SK registration.""" + + def get_tool_by_name(self, name: str) -> ToolDefinition | None: + """Return a registered tool definition by name.""" + + def get_tool_names(self) -> list[str]: + """Return all registered tool names in deterministic order.""" + + def get_registered_tools(self) -> list[ToolDefinition]: + """Return all registered tool definitions in deterministic order.""" + + +class McpToolClient(Protocol): + """Client contract for listing and invoking MCP tools.""" + + async def list_tools(self) -> list[dict[str, Any]]: + """Return raw MCP tool definitions.""" + + async def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: + """Invoke one MCP tool with arguments.""" + diff --git a/backend/app/agents/unity_mcp_sk/core/tool_mapper.py b/backend/app/agents/unity_mcp_sk/core/tool_mapper.py new file mode 100644 index 00000000..f23be21b --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/core/tool_mapper.py @@ -0,0 +1,98 @@ +"""Default tool-definition mapper for Unity MCP SK integration.""" + +from __future__ import annotations + +from typing import Any + +from .contracts import MCP_JSON_TYPE_TO_SK_TYPE, ToolDefinition, ToolDefinitionMapper, ToolParameterDefinition + + +class DefaultToolDefinitionMapper(ToolDefinitionMapper): + """Stores normalized tool definitions and exposes deterministic lookup APIs.""" + + def __init__(self) -> None: + self._tools_by_name: dict[str, ToolDefinition] = {} + + def initialize(self, tool_definitions: list[ToolDefinition]) -> None: + self._tools_by_name = {tool.name: self.map_tool_definition(tool) for tool in tool_definitions} + + def map_tool_definition(self, tool_definition: ToolDefinition) -> ToolDefinition: + normalized_parameters = tuple( + ToolParameterDefinition( + name=parameter.name, + description=parameter.description, + json_type=parameter.json_type if parameter.json_type in MCP_JSON_TYPE_TO_SK_TYPE else "object", + required=parameter.required, + default=parameter.default, + schema=parameter.schema, + ) + for parameter in tool_definition.parameters + ) + return ToolDefinition( + name=tool_definition.name, + description=tool_definition.description, + parameters=normalized_parameters, + return_schema=tool_definition.return_schema, + raw_input_schema=tool_definition.raw_input_schema, + ) + + def get_tool_by_name(self, name: str) -> ToolDefinition | None: + return self._tools_by_name.get(name) + + def get_tool_names(self) -> list[str]: + return sorted(self._tools_by_name.keys()) + + def get_registered_tools(self) -> list[ToolDefinition]: + return [self._tools_by_name[name] for name in self.get_tool_names()] + + +def parse_mcp_tool_definition(raw_tool: dict[str, Any]) -> ToolDefinition: + """ + Parse one raw MCP tool definition into a typed value object. + + Args: + raw_tool: Raw MCP tool object with `name`, `description`, and `inputSchema`. + + Returns: + Parsed `ToolDefinition` with stable parameter extraction. + + Raises: + ValueError: If required fields are missing or malformed. + """ + name = raw_tool.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("MCP tool definition is missing a valid 'name'.") + + description = raw_tool.get("description") if isinstance(raw_tool.get("description"), str) else "" + input_schema = raw_tool.get("inputSchema") if isinstance(raw_tool.get("inputSchema"), dict) else {} + return_schema = raw_tool.get("outputSchema") if isinstance(raw_tool.get("outputSchema"), dict) else {} + required = input_schema.get("required") if isinstance(input_schema.get("required"), list) else [] + required_names = {item for item in required if isinstance(item, str)} + properties = input_schema.get("properties") if isinstance(input_schema.get("properties"), dict) else {} + + parameters: list[ToolParameterDefinition] = [] + for parameter_name in sorted(properties.keys()): + schema = properties.get(parameter_name) + if not isinstance(schema, dict): + continue + parameter_description = schema.get("description") if isinstance(schema.get("description"), str) else "" + json_type = schema.get("type") if isinstance(schema.get("type"), str) else "object" + parameters.append( + ToolParameterDefinition( + name=parameter_name, + description=parameter_description, + json_type=json_type, + required=parameter_name in required_names, + default=schema.get("default"), + schema=schema, + ) + ) + + return ToolDefinition( + name=name, + description=description, + parameters=tuple(parameters), + return_schema=return_schema, + raw_input_schema=input_schema, + ) + diff --git a/backend/app/agents/unity_mcp_sk/infrastructure/__init__.py b/backend/app/agents/unity_mcp_sk/infrastructure/__init__.py new file mode 100644 index 00000000..144e2c5d --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/infrastructure/__init__.py @@ -0,0 +1,2 @@ +"""Infrastructure adapters for Unity MCP SK integration.""" + diff --git a/backend/app/agents/unity_mcp_sk/infrastructure/mcp_client.py b/backend/app/agents/unity_mcp_sk/infrastructure/mcp_client.py new file mode 100644 index 00000000..69dd974e --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/infrastructure/mcp_client.py @@ -0,0 +1,40 @@ +"""Infrastructure MCP client adapter for Unity MCP SK integration.""" + +from __future__ import annotations + +from typing import Any + +from ..core.contracts import McpToolClient + + +class UnityMcpClientAdapter(McpToolClient): + """ + Minimal adapter around an MCP session-like object. + + The wrapped object must expose: + - `list_tools()` -> object with `.tools` or a dict containing `tools` + - `call_tool(tool_name, arguments)` -> tool result + """ + + def __init__(self, session: Any) -> None: + if session is None: + raise ValueError("session must not be None") + self._session = session + + async def list_tools(self) -> list[dict[str, Any]]: + raw = await self._session.list_tools() + if isinstance(raw, dict): + tools = raw.get("tools", []) + return [item for item in tools if isinstance(item, dict)] + tools = getattr(raw, "tools", []) + if not isinstance(tools, list): + return [] + return [item for item in tools if isinstance(item, dict)] + + async def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: + if not tool_name or not isinstance(tool_name, str): + raise ValueError("tool_name must be a non-empty string.") + if arguments is None or not isinstance(arguments, dict): + raise ValueError("arguments must be a dictionary.") + return await self._session.call_tool(tool_name, arguments) + diff --git a/backend/app/agents/unity_mcp_sk/plugin/__init__.py b/backend/app/agents/unity_mcp_sk/plugin/__init__.py new file mode 100644 index 00000000..08a84225 --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/plugin/__init__.py @@ -0,0 +1,2 @@ +"""Plugin runtime layer for Unity MCP SK integration.""" + diff --git a/backend/app/agents/unity_mcp_sk/plugin/unity_mcp_semantic_plugin.py b/backend/app/agents/unity_mcp_sk/plugin/unity_mcp_semantic_plugin.py new file mode 100644 index 00000000..36aee4cb --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/plugin/unity_mcp_semantic_plugin.py @@ -0,0 +1,63 @@ +"""Unity MCP semantic plugin runtime abstraction.""" + +from __future__ import annotations + +from typing import Any + +from ..core.contracts import McpToolClient, ToolDefinition, ToolDefinitionMapper +from ..core.tool_mapper import DefaultToolDefinitionMapper, parse_mcp_tool_definition + + +class UnityMcpSemanticPlugin: + """Owns MCP discovery state and tool invocation behavior for SK registration.""" + + def __init__(self, client: McpToolClient, mapper: ToolDefinitionMapper | None = None) -> None: + if client is None: + raise ValueError("client must not be None.") + self._client = client + self._mapper = mapper or DefaultToolDefinitionMapper() + self._is_initialized = False + + @property + def is_initialized(self) -> bool: + """Return whether discovery has completed at least once.""" + return self._is_initialized + + async def initialize(self) -> None: + """ + Discover MCP tools once and cache them in the mapper. + + Repeated calls are idempotent and do not re-query the MCP server. + """ + if self._is_initialized: + return + raw_tools = await self._client.list_tools() + parsed_tools = [parse_mcp_tool_definition(raw_tool) for raw_tool in raw_tools] + self._mapper.initialize(parsed_tools) + self._is_initialized = True + + async def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: + """ + Invoke an MCP tool through the configured client. + + Args: + tool_name: Name of the tool to invoke. + arguments: Tool arguments. + + Returns: + Raw MCP tool invocation result. + """ + return await self._client.invoke_tool(tool_name=tool_name, arguments=arguments) + + def get_tool_by_name(self, name: str) -> ToolDefinition | None: + """Return a single registered tool definition by name.""" + return self._mapper.get_tool_by_name(name) + + def get_tool_names(self) -> list[str]: + """Return all registered tool names in deterministic order.""" + return self._mapper.get_tool_names() + + def get_registered_tools(self) -> list[ToolDefinition]: + """Return all registered tool definitions in deterministic order.""" + return self._mapper.get_registered_tools() + diff --git a/backend/app/agents/unity_mcp_sk/sk_integration/__init__.py b/backend/app/agents/unity_mcp_sk/sk_integration/__init__.py new file mode 100644 index 00000000..531ee72f --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/sk_integration/__init__.py @@ -0,0 +1,2 @@ +"""Semantic Kernel registration/factory helpers for Unity MCP tools.""" + diff --git a/backend/app/agents/unity_mcp_sk/sk_integration/kernel_factory.py b/backend/app/agents/unity_mcp_sk/sk_integration/kernel_factory.py new file mode 100644 index 00000000..ee686a06 --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/sk_integration/kernel_factory.py @@ -0,0 +1,52 @@ +"""Convenience kernel factory for Unity MCP Semantic Kernel integration.""" + +from __future__ import annotations + +from semantic_kernel import Kernel + +from ..core.contracts import McpToolClient +from ..plugin.unity_mcp_semantic_plugin import UnityMcpSemanticPlugin +from .kernel_registration import register_unity_router_function, register_unity_tools_as_functions + + +async def create_kernel_with_unity_async( + client: McpToolClient, + *, + plugin_name: str = "unity", + expanded_mode: bool = True, +) -> tuple[Kernel, UnityMcpSemanticPlugin]: + """ + Create an SK kernel with Unity MCP integration. + + Args: + client: MCP client used for discovery and invocation. + plugin_name: SK plugin namespace for Unity functions. + expanded_mode: True to register one function per tool (recommended), + False to keep router mode with a single invoke function. + + Returns: + Tuple of configured kernel and initialized Unity MCP plugin runtime. + + Raises: + ValueError: If inputs are invalid. + + Example: + >>> # doctest: +SKIP + >>> kernel, unity_plugin = await create_kernel_with_unity_async(client, expanded_mode=True) + """ + if client is None: + raise ValueError("client must not be None.") + if not plugin_name or not isinstance(plugin_name, str): + raise ValueError("plugin_name must be a non-empty string.") + + unity_plugin = UnityMcpSemanticPlugin(client=client) + await unity_plugin.initialize() + + kernel = Kernel() + if expanded_mode: + register_unity_tools_as_functions(kernel=kernel, plugin=unity_plugin, plugin_name=plugin_name) + else: + register_unity_router_function(kernel=kernel, plugin=unity_plugin, plugin_name=plugin_name) + + return kernel, unity_plugin + diff --git a/backend/app/agents/unity_mcp_sk/sk_integration/kernel_registration.py b/backend/app/agents/unity_mcp_sk/sk_integration/kernel_registration.py new file mode 100644 index 00000000..c694cc90 --- /dev/null +++ b/backend/app/agents/unity_mcp_sk/sk_integration/kernel_registration.py @@ -0,0 +1,184 @@ +"""Helpers for registering Unity MCP tools into Semantic Kernel.""" + +from __future__ import annotations + +from typing import Any + +from semantic_kernel import Kernel +from semantic_kernel.functions import KernelPlugin, kernel_function +from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod +from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata + +from ..core.contracts import MCP_JSON_TYPE_TO_SK_TYPE, ToolDefinition, ToolParameterDefinition +from ..plugin.unity_mcp_semantic_plugin import UnityMcpSemanticPlugin + +_JSON_TYPE_TO_PYTHON_TYPE: dict[str, type[Any]] = { + "string": str, + "number": float, + "integer": int, + "boolean": bool, + "array": list, + "object": dict, +} + + +def _build_parameter_metadata(parameter: ToolParameterDefinition) -> KernelParameterMetadata: + default_value = parameter.default + is_required = parameter.required + if default_value is not None and is_required: + # Default values imply optional semantics for planners. + is_required = False + json_type = parameter.json_type if parameter.json_type in MCP_JSON_TYPE_TO_SK_TYPE else "object" + return KernelParameterMetadata( + name=parameter.name, + description=parameter.description, + default_value=default_value, + type_=MCP_JSON_TYPE_TO_SK_TYPE[json_type], + is_required=is_required, + type_object=_JSON_TYPE_TO_PYTHON_TYPE.get(json_type, dict), + schema_data=parameter.schema, + ) + + +def _build_return_metadata(tool_definition: ToolDefinition) -> KernelParameterMetadata: + return KernelParameterMetadata( + name="return", + description=f"Result from MCP tool '{tool_definition.name}'.", + default_value=None, + type_="object", + is_required=True, + type_object=dict, + schema_data=tool_definition.return_schema or {"type": "object"}, + ) + + +def _create_tool_function(tool_definition: ToolDefinition, plugin: UnityMcpSemanticPlugin, plugin_name: str) -> Any: + @kernel_function(name=tool_definition.name, description=tool_definition.description) + async def _invoke_tool(**kwargs: Any) -> Any: + return await plugin.invoke_tool(tool_definition.name, kwargs) + + return KernelFunctionFromMethod( + method=_invoke_tool, + plugin_name=plugin_name, + parameters=[_build_parameter_metadata(parameter) for parameter in tool_definition.parameters], + return_parameter=_build_return_metadata(tool_definition), + additional_metadata={ + "mcp_input_schema": tool_definition.raw_input_schema, + "mcp_return_schema": tool_definition.return_schema, + }, + ) + + +def register_unity_tools_as_functions( + kernel: Kernel, + plugin: UnityMcpSemanticPlugin, + plugin_name: str = "unity", +) -> KernelPlugin: + """ + Register discovered MCP tools as individual Semantic Kernel functions. + + Args: + kernel: Target Semantic Kernel instance. + plugin: Initialized Unity MCP semantic plugin. + plugin_name: SK namespace under which all tool functions are registered. + + Returns: + Registered KernelPlugin handle. + + Raises: + RuntimeError: If plugin has no discovered tools (not initialized or empty). + """ + if kernel is None: + raise ValueError("kernel must not be None.") + if plugin is None: + raise ValueError("plugin must not be None.") + if not plugin_name or not isinstance(plugin_name, str): + raise ValueError("plugin_name must be a non-empty string.") + + tool_definitions = plugin.get_registered_tools() + if not tool_definitions: + raise RuntimeError( + "Unity MCP plugin has no registered tools. Initialize plugin before registering with Semantic Kernel." + ) + + functions = [_create_tool_function(tool_definition, plugin, plugin_name) for tool_definition in tool_definitions] + sk_plugin = KernelPlugin(name=plugin_name, description="Unity MCP tools", functions=functions) + return kernel.add_plugin(sk_plugin, plugin_name=plugin_name) + + +def register_unity_router_function( + kernel: Kernel, + plugin: UnityMcpSemanticPlugin, + plugin_name: str = "unity", +) -> KernelPlugin: + """Register a single generic router function for backward compatibility.""" + if kernel is None: + raise ValueError("kernel must not be None.") + if plugin is None: + raise ValueError("plugin must not be None.") + if not plugin_name or not isinstance(plugin_name, str): + raise ValueError("plugin_name must be a non-empty string.") + + @kernel_function( + name="invoke", + description="Invoke any Unity MCP tool by passing tool_name and arguments.", + ) + async def _invoke(**kwargs: Any) -> Any: + raw_arguments = kwargs.get("arguments") + if hasattr(raw_arguments, "items"): + argument_payload = dict(raw_arguments) + elif isinstance(raw_arguments, dict): + argument_payload = raw_arguments + else: + argument_payload = {} + + tool_name = kwargs.get("tool_name") or argument_payload.get("tool_name") + if not tool_name or not isinstance(tool_name, str): + raise ValueError("tool_name must be a non-empty string.") + + # Some SK call paths wrap both router args into one payload object. + if "arguments" in argument_payload and isinstance(argument_payload.get("arguments"), dict): + resolved_arguments = argument_payload["arguments"] + else: + resolved_arguments = argument_payload + if not isinstance(resolved_arguments, dict): + raise ValueError("arguments must be an object when provided.") + return await plugin.invoke_tool(tool_name, resolved_arguments) + + router_function = KernelFunctionFromMethod( + method=_invoke, + plugin_name=plugin_name, + parameters=[ + KernelParameterMetadata( + name="tool_name", + description="MCP tool name to invoke.", + default_value=None, + type_="string", + is_required=True, + type_object=str, + schema_data={"type": "string"}, + ), + KernelParameterMetadata( + name="arguments", + description="Arguments object passed to the selected MCP tool.", + default_value={}, + type_="object", + is_required=False, + type_object=dict, + schema_data={"type": "object"}, + ), + ], + return_parameter=KernelParameterMetadata( + name="return", + description="Result from the selected MCP tool.", + default_value=None, + type_="object", + is_required=True, + type_object=dict, + schema_data={"type": "object"}, + ), + ) + + sk_plugin = KernelPlugin(name=plugin_name, description="Unity MCP router", functions=[router_function]) + return kernel.add_plugin(sk_plugin, plugin_name=plugin_name) + diff --git a/backend/package.json b/backend/package.json index dcef7efd..2fbc51dd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "unity-generator-backend", - "version": "1.7.0", + "version": "1.8.0", "description": "Backend for Unity Generator", "private": true, "scripts": { diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8e27159a..e794b0e4 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "unity-generator-backend" -version = "0.10.0" +version = "1.8.0" description = "Unity project generation and finalization backend with AI integration" requires-python = ">=3.11" license = {text = "MIT"} diff --git a/backend/tests/test_unity_mcp_sk_integration.py b/backend/tests/test_unity_mcp_sk_integration.py new file mode 100644 index 00000000..89859f18 --- /dev/null +++ b/backend/tests/test_unity_mcp_sk_integration.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from typing import Any + +import pytest +from semantic_kernel.functions.kernel_arguments import KernelArguments + +from app.agents.unity_mcp_sk.core.tool_mapper import DefaultToolDefinitionMapper, parse_mcp_tool_definition +from app.agents.unity_mcp_sk.plugin.unity_mcp_semantic_plugin import UnityMcpSemanticPlugin +from app.agents.unity_mcp_sk.sk_integration.kernel_factory import create_kernel_with_unity_async +from app.agents.unity_mcp_sk.sk_integration.kernel_registration import register_unity_tools_as_functions + + +class FakeMcpClient: + def __init__(self, tools: list[dict[str, Any]]) -> None: + self._tools = tools + self.list_tools_calls = 0 + self.invoke_calls: list[tuple[str, dict[str, Any]]] = [] + + async def list_tools(self) -> list[dict[str, Any]]: + self.list_tools_calls += 1 + return self._tools + + async def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]: + self.invoke_calls.append((tool_name, arguments)) + return {"success": True, "tool": tool_name, "arguments": arguments} + + +def _build_tools_fixture() -> list[dict[str, Any]]: + return [ + { + "name": "z_ping", + "description": "Ping tool.", + "inputSchema": {"type": "object", "properties": {}, "required": []}, + }, + { + "name": "build_scene", + "description": "Build a scene from parameters.", + "inputSchema": { + "type": "object", + "required": ["projectPath", "count"], + "properties": { + "projectPath": {"type": "string", "description": "Unity project path."}, + "count": {"type": "integer", "description": "Object count."}, + "usePhysics": {"type": "boolean", "description": "Enable physics.", "default": False}, + "weights": {"type": "array", "description": "Distribution weights."}, + "settings": {"type": "object", "description": "Scene settings."}, + "temperature": {"type": "number", "description": "Numeric tuning.", "default": 0.3}, + }, + }, + "outputSchema": {"type": "object"}, + }, + { + "name": "a_echo", + "description": "Echo tool.", + "inputSchema": { + "type": "object", + "required": ["message"], + "properties": {"message": {"type": "string", "description": "Message."}}, + }, + }, + ] + + +@pytest.mark.asyncio +async def test_get_registered_tools_returns_sorted_tools() -> None: + mapper = DefaultToolDefinitionMapper() + parsed = [parse_mcp_tool_definition(item) for item in _build_tools_fixture()] + mapper.initialize(parsed) + + registered_names = [tool.name for tool in mapper.get_registered_tools()] + assert registered_names == ["a_echo", "build_scene", "z_ping"] + + +@pytest.mark.asyncio +async def test_kernel_factory_lists_tools_only_once() -> None: + client = FakeMcpClient(_build_tools_fixture()) + kernel, plugin = await create_kernel_with_unity_async(client=client, plugin_name="unity", expanded_mode=True) + + assert kernel is not None + assert plugin.is_initialized is True + assert client.list_tools_calls == 1 + + register_unity_tools_as_functions(kernel=kernel, plugin=plugin, plugin_name="unity_v2") + assert client.list_tools_calls == 1 + + +@pytest.mark.asyncio +async def test_expanded_registration_uses_single_plugin_namespace() -> None: + client = FakeMcpClient(_build_tools_fixture()) + kernel, _ = await create_kernel_with_unity_async(client=client, plugin_name="unity", expanded_mode=True) + + plugin = kernel.get_plugin("unity") + assert plugin is not None + assert "unity" in list(kernel.plugins) + assert set(plugin.functions.keys()) == {"a_echo", "build_scene", "z_ping"} + + +@pytest.mark.asyncio +async def test_registered_metadata_matches_mcp_schema() -> None: + client = FakeMcpClient(_build_tools_fixture()) + kernel, _ = await create_kernel_with_unity_async(client=client, plugin_name="unity", expanded_mode=True) + + plugin = kernel.get_plugin("unity") + function = plugin["build_scene"] + parameters = {parameter.name: parameter for parameter in function.metadata.parameters} + + assert parameters["projectPath"].type_ == "string" + assert parameters["projectPath"].is_required is True + assert parameters["count"].type_ == "integer" + assert parameters["count"].is_required is True + assert parameters["temperature"].type_ == "number" + assert parameters["temperature"].default_value == 0.3 + assert parameters["temperature"].is_required is False + assert parameters["usePhysics"].type_ == "boolean" + assert parameters["usePhysics"].default_value is False + assert parameters["weights"].type_ == "array" + assert parameters["settings"].type_ == "object" + + +@pytest.mark.asyncio +async def test_router_mode_still_works() -> None: + client = FakeMcpClient(_build_tools_fixture()) + kernel, _ = await create_kernel_with_unity_async(client=client, plugin_name="unity", expanded_mode=False) + + plugin = kernel.get_plugin("unity") + assert set(plugin.functions.keys()) == {"invoke"} + + result = await kernel.invoke( + function_name="invoke", + plugin_name="unity", + arguments=KernelArguments(tool_name="a_echo", arguments={"message": "hello"}), + ) + assert result is not None + assert client.invoke_calls == [("a_echo", {"message": "hello"})] + diff --git a/docs/UNITY_MCP_SK_INTEGRATION.md b/docs/UNITY_MCP_SK_INTEGRATION.md index 7fe9826b..c7677e3e 100644 --- a/docs/UNITY_MCP_SK_INTEGRATION.md +++ b/docs/UNITY_MCP_SK_INTEGRATION.md @@ -1,53 +1,74 @@ -# Integrating Unity-MCP with Semantic Kernel (SK) in .NET 10 +# Unity MCP + Semantic Kernel Integration -## Overview -To connect your existing Unity-MCP server to Semantic Kernel (SK), you do **not** need to rewrite the server or create a separate plugin project. Instead, use the official MCP connector package for SK, which allows the kernel to discover and use your MCP tools automatically. +## Python Integration Modes -## Steps +The Python integration now supports two registration modes: -### 1. Install the MCP Connector Package +- **Expanded mode (recommended)**: each discovered MCP tool is exposed as an individual Semantic Kernel function under one plugin namespace (default: `unity`). +- **Router mode (backward compatible)**: one generic `invoke` function routes calls to a chosen MCP tool. -```sh -dotnet add package Microsoft.SemanticKernel.Connectors.Mcp -``` +Expanded mode improves planner/autonomous agent tool selection because functions and parameter schemas are visible as first-class SK functions. + +## Expanded Mode (Recommended) -### 2. Register the MCP Server as a Plugin in SK +```python +from app.agents.unity_mcp_sk.sk_integration.kernel_factory import create_kernel_with_unity_async -```csharp -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.Mcp; +# client must implement list_tools() and invoke_tool(tool_name, arguments) +kernel, unity_plugin = await create_kernel_with_unity_async( + client=my_mcp_client, + plugin_name="unity", + expanded_mode=True, +) -var builder = Kernel.CreateBuilder(); +# SK now sees one plugin namespace ("unity") with many functions: +# - unity_create_scene +# - unity_add_gameobject +# - unity_create_material +# - ... +``` -// Register the global unity-mcp tool via Stdio -var mcpPlugin = new McpStdioPlugin( - "UnityManager", - "unity-mcp" // The global tool name -); +## Router Mode (Backward Compatible) -builder.Plugins.Add(mcpPlugin); -var kernel = builder.Build(); +```python +from app.agents.unity_mcp_sk.sk_integration.kernel_factory import create_kernel_with_unity_async -// Example: Call a tool -var result = await kernel.InvokeAsync("UnityManager", "create_scene", new() { - ["name"] = "MainScene" -}); +kernel, unity_plugin = await create_kernel_with_unity_async( + client=my_mcp_client, + plugin_name="unity", + expanded_mode=False, +) + +# SK sees one function: +# unity.invoke(tool_name, arguments) ``` -### 3. How It Works -- The MCP server is a global tool (e.g., `unity-mcp`) and is discovered automatically. -- The connector queries the MCP server for available tools and exposes them as KernelFunctions. -- No .json/.yaml plugin project or manual tool registration is needed. +## Tradeoffs + +- **Expanded mode** + - Better discoverability and tool-calling reliability for planners/agents. + - Richer per-tool metadata (name, description, required/default, type mapping). + - Slightly larger function registry and token footprint. +- **Router mode** + - Smallest function footprint (single generic function). + - Weaker discoverability, often requiring stronger prompting to choose tools correctly. + +## Discovery and Registration Behavior + +- Tool discovery (`list_tools`) is performed once during plugin initialization. +- Registration uses the mapper's cached `get_registered_tools()` output as the source of truth. +- Tool registration order is deterministic (sorted by tool name). + +## Metadata Fidelity Notes + +For each MCP tool parameter, the integration preserves: -### 4. Best Practices -- Ensure `unity-mcp` is installed and available in your system PATH. -- Use the latest version of the connector for .NET 10 compatibility. -- Tool descriptions and parameters from MCP are surfaced in SK automatically. +- exact parameter name, +- description, +- required/optional state, +- default value when present, +- JSON type mapping: `string`, `number`, `integer`, `boolean`, `array`, `object`. ---- +Current Python SK limitation/workaround: -**Summary:** -- No plugin project required. -- Use the official connector package. -- Register your MCP server as shown above. -- All MCP tools become available in SK with no extra configuration. +- Complex JSON Schema constructs (`oneOf`, `anyOf`, tuple schemas, advanced constraints) are passed through in `schema_data`, but SK planner behavior primarily uses simplified `KernelParameterMetadata` fields (`type_`, `is_required`, `default_value`). diff --git a/frontend/.dockerignore b/frontend/.dockerignore index 7575545d..cbb6a0de 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -56,3 +56,17 @@ Thumbs.db .cursor/ .qwen/ .kiro/ + +.venv +.venv-local +.venv-build +.vscode +.idea +.pytest_cache +.coverage +.coverage.* +.coverage.*.* +.coverage.*.*.* +.coverage.*.*.*.* +.coverage.*.*.*.*.* +.coverage.*.*.*.*.*.* diff --git a/frontend/package.json b/frontend/package.json index e7a9a431..5c01eb58 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "unity-generator-frontend", "description": "Frontend for Unity Generator", "private": true, - "version": "1.7.0", + "version": "1.8.0", "type": "module", "scripts": { "dev": "vite", diff --git a/package.json b/package.json index ba50e064..8f2c8b31 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.7.0", + "version": "1.8.0", "description": "Desktop app to generate Unity assets using AI (Electron + FastAPI)", "name": "unity-generator", "private": true, From 08a57dee6d875612c5ce0b1e967a392bf54a42ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreu=20Mart=C3=ADnez=20Vidal?= Date: Sun, 22 Mar 2026 11:32:49 +0100 Subject: [PATCH 2/7] Update project versions to 1.8.0 and enhance .dockerignore and .gitignore files Bump frontend, backend, and package versions to 1.8.0. Add common development environment files to .dockerignore and .gitignore for better project management. This includes entries for virtual environments, IDE configurations, and test coverage files. --- README.md | 16 +- backend/app/agents/SKILLS_USAGE.md | 2 +- backend/app/agents/unity_mcp_plugin.py | 45 ++- backend/app/agents/unity_mcp_sk/__init__.py | 19 -- .../app/agents/unity_mcp_sk/core/__init__.py | 2 - .../app/agents/unity_mcp_sk/core/contracts.py | 88 ------ .../agents/unity_mcp_sk/core/tool_mapper.py | 98 ------- .../unity_mcp_sk/infrastructure/__init__.py | 2 - .../unity_mcp_sk/infrastructure/mcp_client.py | 40 --- .../agents/unity_mcp_sk/plugin/__init__.py | 2 - .../plugin/unity_mcp_semantic_plugin.py | 63 ----- .../unity_mcp_sk/sk_integration/__init__.py | 2 - .../sk_integration/kernel_factory.py | 52 ---- .../sk_integration/kernel_registration.py | 184 ------------- backend/requirements.txt | 10 +- backend/tests/test_unity_mcp_pluginwrapper.py | 44 ++- .../tests/test_unity_mcp_sk_integration.py | 136 --------- docs/DEVELOPMENT.md | 2 +- docs/UNITY_MCP_INTEGRATION.md | 33 ++- docs/UNITY_MCP_SERVER.md | 116 ++++++++ docs/UNITY_MCP_SK_INTEGRATION.md | 82 +++--- frontend/src/i18n/index.ts | 7 +- frontend/src/i18n/locales/ar.ts | 1 + frontend/src/i18n/locales/bn.ts | 1 + frontend/src/i18n/locales/ca.ts | 1 + frontend/src/i18n/locales/de.ts | 1 + frontend/src/i18n/locales/en.ts | 1 + frontend/src/i18n/locales/es.ts | 1 + frontend/src/i18n/locales/eu.ts | 1 + frontend/src/i18n/locales/fr.ts | 1 + frontend/src/i18n/locales/gl.ts | 8 + frontend/src/i18n/locales/hi.ts | 1 + frontend/src/i18n/locales/id.ts | 1 + frontend/src/i18n/locales/it.ts | 1 + frontend/src/i18n/locales/ja.ts | 1 + frontend/src/i18n/locales/ko.ts | 1 + frontend/src/i18n/locales/oc.ts | 1 + frontend/src/i18n/locales/pl.ts | 1 + frontend/src/i18n/locales/pt.ts | 1 + frontend/src/i18n/locales/tr.ts | 258 ++++++++++++++++++ frontend/src/i18n/locales/uk.ts | 1 + frontend/src/i18n/locales/ur.ts | 1 + frontend/src/i18n/locales/vi.ts | 1 + frontend/src/i18n/locales/zh.ts | 1 + pyrightconfig.json | 3 +- 45 files changed, 560 insertions(+), 774 deletions(-) delete mode 100644 backend/app/agents/unity_mcp_sk/__init__.py delete mode 100644 backend/app/agents/unity_mcp_sk/core/__init__.py delete mode 100644 backend/app/agents/unity_mcp_sk/core/contracts.py delete mode 100644 backend/app/agents/unity_mcp_sk/core/tool_mapper.py delete mode 100644 backend/app/agents/unity_mcp_sk/infrastructure/__init__.py delete mode 100644 backend/app/agents/unity_mcp_sk/infrastructure/mcp_client.py delete mode 100644 backend/app/agents/unity_mcp_sk/plugin/__init__.py delete mode 100644 backend/app/agents/unity_mcp_sk/plugin/unity_mcp_semantic_plugin.py delete mode 100644 backend/app/agents/unity_mcp_sk/sk_integration/__init__.py delete mode 100644 backend/app/agents/unity_mcp_sk/sk_integration/kernel_factory.py delete mode 100644 backend/app/agents/unity_mcp_sk/sk_integration/kernel_registration.py delete mode 100644 backend/tests/test_unity_mcp_sk_integration.py create mode 100644 docs/UNITY_MCP_SERVER.md create mode 100644 frontend/src/i18n/locales/tr.ts diff --git a/README.md b/README.md index 3bd8cc0e..7ab90b2b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ and assets without wiring multiple tools together. It’s not a full IDE or asse - **Unity UI Elements**: Generate Unity UI prefabs (health bars, buttons, dialogue boxes, HUD layouts) for both uGUI and UI Toolkit - **Incremental Asset Generation**: Save assets directly into an active Unity project - **Pixel-Art Sprites**: Generate and process 2D sprite sheets with automatic cropping -- **Unity MCP Integration**: Real-time interaction with the Unity Editor using [Unity-MCP-SK-Plugin](https://github.com/Ozymandros/Unity-MCP-SK-Plugin) and [Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server) +- **Unity MCP integration**: Python bridge [`unity-mcp-plugin`](https://github.com/Ozymandros/Unity-MCP-SK-Plugin) (Semantic Kernel; see `backend/requirements.txt` for Git vs local editable install) plus MCP server [**Unity-MCP-Server**](https://github.com/Ozymandros/Unity-MCP-Server) — both are required; see [docs/UNITY_MCP_SERVER.md](docs/UNITY_MCP_SERVER.md) for server install/Git URLs/env vars, and [docs/UNITY_MCP_INTEGRATION.md](docs/UNITY_MCP_INTEGRATION.md) for the full SK setup. - Save and reuse provider settings and preferences locally - Configure global and per-request system key prompts for tailored generation - Keep output structured so Unity can open it right away @@ -80,6 +80,7 @@ Scaffolded and functional. See docs for development and packaging details. The p - [Architecture overview](docs/ARCHITECTURE.md) - [Development guide](docs/DEVELOPMENT.md) +- [Unity MCP Server (install, Git URL, env vars)](docs/UNITY_MCP_SERVER.md) - [Packaging and distribution](docs/PACKAGING.md) - [System Prompts guide](docs/SYSTEM_PROMPTS.md) @@ -358,9 +359,18 @@ Electron app. - If a provider request errors, check `logs/` for the failed request log. - If a Docker build is slow, ensure `node_modules/` and venvs are ignored. -## Unity MCP Integration +## Unity MCP integration -See [docs/UNITY_MCP_INTEGRATION.md](docs/UNITY_MCP_INTEGRATION.md) for details on the Semantic Kernel MCP integration, configuration, and usage. +Dependencies and setup (both repos are required): + +| Role | GitHub | +|------|--------| +| MCP server (Unity tools, stdio) | [Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server) | +| Python Semantic Kernel plugin (`unity_mcp`) | [Unity-MCP-SK-Plugin](https://github.com/Ozymandros/Unity-MCP-SK-Plugin) | + +- **Unity MCP Server only (not the Python plugin):** [docs/UNITY_MCP_SERVER.md](docs/UNITY_MCP_SERVER.md) +- **Semantic Kernel + paths:** [docs/UNITY_MCP_INTEGRATION.md](docs/UNITY_MCP_INTEGRATION.md) +- **Expanded vs router SK modes:** [docs/UNITY_MCP_SK_INTEGRATION.md](docs/UNITY_MCP_SK_INTEGRATION.md) ## Download diff --git a/backend/app/agents/SKILLS_USAGE.md b/backend/app/agents/SKILLS_USAGE.md index 41a062f7..7780dda4 100644 --- a/backend/app/agents/SKILLS_USAGE.md +++ b/backend/app/agents/SKILLS_USAGE.md @@ -33,7 +33,7 @@ Provides real-time integration with a running Unity Editor via the Unity MCP Ser - Plugin Source: [Unity-MCP-SK-Plugin](https://github.com/Ozymandros/Unity-MCP-SK-Plugin) - Server Source: [Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server) -Requires the `unity-mcp-plugin` package installed in the Python environment. For the full tool list and parameter details, see the server’s tool reference (e.g. Unity-MCP-Server `Skills/SKILL.md` or README). +Requires the `unity-mcp-plugin` package installed in the Python environment (see `backend/requirements.txt` and [docs/UNITY_MCP_INTEGRATION.md](../../../docs/UNITY_MCP_INTEGRATION.md) for the SK bridge). **MCP server install, clone URL, and `UNITY_MCP_*` locations:** [docs/UNITY_MCP_SERVER.md](../../../docs/UNITY_MCP_SERVER.md). For the full tool list and parameter details, see the server’s tool reference (e.g. Unity-MCP-Server `Skills/SKILL.md` or README). **Path handling:** The server requires **projectPath** (project root) and **fileName** (or **folderName** for folder/list tools) on all file, scene, and asset tools. Pass `projectPath` as the project root directory and `fileName` as the path under the project (e.g. `Assets/Scenes/MyScene.unity`). Do not duplicate path segments (e.g. do not put the project root inside `fileName`). diff --git a/backend/app/agents/unity_mcp_plugin.py b/backend/app/agents/unity_mcp_plugin.py index 79f850c6..21780bce 100644 --- a/backend/app/agents/unity_mcp_plugin.py +++ b/backend/app/agents/unity_mcp_plugin.py @@ -1,11 +1,15 @@ import asyncio import logging +import os from contextlib import asynccontextmanager from semantic_kernel import Kernel -from semantic_kernel.connectors.mcp import MCPStdioPlugin +from unity_mcp import UnityMcpOptions, UnityMCPPlugin LOGGER = logging.getLogger("unity_mcp_plugin") +_ENV_MCP_COMMAND = "UNITY_MCP_COMMAND" +_ENV_MCP_ARGS = "UNITY_MCP_ARGS" +_DEFAULT_MCP_EXECUTABLE = "unity-mcp" async def _check_unity_mcp_live() -> bool: @@ -20,6 +24,9 @@ async def _check_unity_mcp_live() -> bool: plugin = kernel.get_plugin("UnityMCP") if plugin and "ping" in plugin: await kernel.invoke(plugin["ping"]) + elif hasattr(mcp_plugin, "is_healthy"): + if not mcp_plugin.is_healthy(): + return False return True except Exception as e: LOGGER.debug("Unity MCP live check failed: %s", e) @@ -41,22 +48,36 @@ def unity_mcp_plugin_available_for_writing() -> bool: @asynccontextmanager async def create_unity_mcp_plugin(): """ - Async context manager for the Unity MCP plugin (global unity-mcp tool). + Async context manager for the Unity MCP plugin. + + Uses the local `unity-mcp-plugin` package implementation and initializes + the plugin against the configured Unity MCP executable. + Use with: async with create_unity_mcp_plugin() as mcp_plugin: ... """ LOGGER.info("Creating UnityMCP plugin...") + command = os.environ.get(_ENV_MCP_COMMAND, _DEFAULT_MCP_EXECUTABLE).strip() or _DEFAULT_MCP_EXECUTABLE + mcp_args = os.environ.get(_ENV_MCP_ARGS, "").strip() + if mcp_args: + LOGGER.warning( + "%s is set but ignored by unity-mcp-plugin local package implementation.", + _ENV_MCP_ARGS, + ) + try: - plugin = MCPStdioPlugin( - name="UnityMCP", - description="Unity Editor automation tools", - command="unity-mcp", - args=[], - load_tools=True, - request_timeout=30, + options = UnityMcpOptions( + executable_path=command, + connection_timeout_seconds=30, + request_timeout_seconds=30, + enable_message_logging=False, ) - async with plugin as mcp_plugin: - LOGGER.info("UnityMCP plugin created successfully.") - yield mcp_plugin + plugin = UnityMCPPlugin.create(options) + await plugin.initialize() + LOGGER.info("UnityMCP plugin created successfully.") + try: + yield plugin + finally: + await plugin.cleanup() except Exception as e: LOGGER.error("Failed to create UnityMCP plugin: %s", e) raise diff --git a/backend/app/agents/unity_mcp_sk/__init__.py b/backend/app/agents/unity_mcp_sk/__init__.py deleted file mode 100644 index e143fa94..00000000 --- a/backend/app/agents/unity_mcp_sk/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Semantic Kernel integration for Unity MCP tools. - -This package supports two registration modes: -- Expanded mode: one SK function per MCP tool (recommended). -- Router mode: one generic SK function for tool routing. -""" - -from .plugin.unity_mcp_semantic_plugin import UnityMcpSemanticPlugin -from .sk_integration.kernel_factory import create_kernel_with_unity_async -from .sk_integration.kernel_registration import register_unity_router_function, register_unity_tools_as_functions - -__all__ = [ - "UnityMcpSemanticPlugin", - "create_kernel_with_unity_async", - "register_unity_router_function", - "register_unity_tools_as_functions", -] - diff --git a/backend/app/agents/unity_mcp_sk/core/__init__.py b/backend/app/agents/unity_mcp_sk/core/__init__.py deleted file mode 100644 index 95b4429a..00000000 --- a/backend/app/agents/unity_mcp_sk/core/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Core contracts and mappers for Unity MCP SK integration.""" - diff --git a/backend/app/agents/unity_mcp_sk/core/contracts.py b/backend/app/agents/unity_mcp_sk/core/contracts.py deleted file mode 100644 index 47aaac88..00000000 --- a/backend/app/agents/unity_mcp_sk/core/contracts.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Core contracts and value objects for Unity MCP SK integration.""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any, Protocol - - -MCP_JSON_TYPE_TO_SK_TYPE: dict[str, str] = { - "string": "string", - "number": "number", - "integer": "integer", - "boolean": "boolean", - "array": "array", - "object": "object", -} - - -@dataclass(frozen=True) -class ToolParameterDefinition: - """ - Represents an MCP tool parameter as a typed, SK-ready value object. - - Args: - name: Exact parameter name from MCP schema. - description: Human-readable parameter description. - json_type: MCP JSON schema type. - required: Whether the parameter is required. - default: Optional default value from MCP schema. - schema: Full original JSON schema fragment for this parameter. - """ - - name: str - description: str - json_type: str - required: bool - default: Any = None - schema: dict[str, Any] = field(default_factory=dict) - - -@dataclass(frozen=True) -class ToolDefinition: - """ - Represents one discovered MCP tool definition. - - Args: - name: MCP tool name. - description: Tool description exposed to SK planners/agents. - parameters: Parsed input parameters from MCP input schema. - return_schema: Optional output schema metadata when available. - raw_input_schema: Raw MCP input schema for metadata passthrough. - """ - - name: str - description: str - parameters: tuple[ToolParameterDefinition, ...] - return_schema: dict[str, Any] = field(default_factory=dict) - raw_input_schema: dict[str, Any] = field(default_factory=dict) - - -class ToolDefinitionMapper(Protocol): - """Maps/discovers MCP tool definitions and stores a deterministic registry.""" - - def initialize(self, tool_definitions: list[ToolDefinition]) -> None: - """Initialize mapper with tool definitions.""" - - def map_tool_definition(self, tool_definition: ToolDefinition) -> ToolDefinition: - """Normalize a tool definition for SK registration.""" - - def get_tool_by_name(self, name: str) -> ToolDefinition | None: - """Return a registered tool definition by name.""" - - def get_tool_names(self) -> list[str]: - """Return all registered tool names in deterministic order.""" - - def get_registered_tools(self) -> list[ToolDefinition]: - """Return all registered tool definitions in deterministic order.""" - - -class McpToolClient(Protocol): - """Client contract for listing and invoking MCP tools.""" - - async def list_tools(self) -> list[dict[str, Any]]: - """Return raw MCP tool definitions.""" - - async def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: - """Invoke one MCP tool with arguments.""" - diff --git a/backend/app/agents/unity_mcp_sk/core/tool_mapper.py b/backend/app/agents/unity_mcp_sk/core/tool_mapper.py deleted file mode 100644 index f23be21b..00000000 --- a/backend/app/agents/unity_mcp_sk/core/tool_mapper.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Default tool-definition mapper for Unity MCP SK integration.""" - -from __future__ import annotations - -from typing import Any - -from .contracts import MCP_JSON_TYPE_TO_SK_TYPE, ToolDefinition, ToolDefinitionMapper, ToolParameterDefinition - - -class DefaultToolDefinitionMapper(ToolDefinitionMapper): - """Stores normalized tool definitions and exposes deterministic lookup APIs.""" - - def __init__(self) -> None: - self._tools_by_name: dict[str, ToolDefinition] = {} - - def initialize(self, tool_definitions: list[ToolDefinition]) -> None: - self._tools_by_name = {tool.name: self.map_tool_definition(tool) for tool in tool_definitions} - - def map_tool_definition(self, tool_definition: ToolDefinition) -> ToolDefinition: - normalized_parameters = tuple( - ToolParameterDefinition( - name=parameter.name, - description=parameter.description, - json_type=parameter.json_type if parameter.json_type in MCP_JSON_TYPE_TO_SK_TYPE else "object", - required=parameter.required, - default=parameter.default, - schema=parameter.schema, - ) - for parameter in tool_definition.parameters - ) - return ToolDefinition( - name=tool_definition.name, - description=tool_definition.description, - parameters=normalized_parameters, - return_schema=tool_definition.return_schema, - raw_input_schema=tool_definition.raw_input_schema, - ) - - def get_tool_by_name(self, name: str) -> ToolDefinition | None: - return self._tools_by_name.get(name) - - def get_tool_names(self) -> list[str]: - return sorted(self._tools_by_name.keys()) - - def get_registered_tools(self) -> list[ToolDefinition]: - return [self._tools_by_name[name] for name in self.get_tool_names()] - - -def parse_mcp_tool_definition(raw_tool: dict[str, Any]) -> ToolDefinition: - """ - Parse one raw MCP tool definition into a typed value object. - - Args: - raw_tool: Raw MCP tool object with `name`, `description`, and `inputSchema`. - - Returns: - Parsed `ToolDefinition` with stable parameter extraction. - - Raises: - ValueError: If required fields are missing or malformed. - """ - name = raw_tool.get("name") - if not isinstance(name, str) or not name.strip(): - raise ValueError("MCP tool definition is missing a valid 'name'.") - - description = raw_tool.get("description") if isinstance(raw_tool.get("description"), str) else "" - input_schema = raw_tool.get("inputSchema") if isinstance(raw_tool.get("inputSchema"), dict) else {} - return_schema = raw_tool.get("outputSchema") if isinstance(raw_tool.get("outputSchema"), dict) else {} - required = input_schema.get("required") if isinstance(input_schema.get("required"), list) else [] - required_names = {item for item in required if isinstance(item, str)} - properties = input_schema.get("properties") if isinstance(input_schema.get("properties"), dict) else {} - - parameters: list[ToolParameterDefinition] = [] - for parameter_name in sorted(properties.keys()): - schema = properties.get(parameter_name) - if not isinstance(schema, dict): - continue - parameter_description = schema.get("description") if isinstance(schema.get("description"), str) else "" - json_type = schema.get("type") if isinstance(schema.get("type"), str) else "object" - parameters.append( - ToolParameterDefinition( - name=parameter_name, - description=parameter_description, - json_type=json_type, - required=parameter_name in required_names, - default=schema.get("default"), - schema=schema, - ) - ) - - return ToolDefinition( - name=name, - description=description, - parameters=tuple(parameters), - return_schema=return_schema, - raw_input_schema=input_schema, - ) - diff --git a/backend/app/agents/unity_mcp_sk/infrastructure/__init__.py b/backend/app/agents/unity_mcp_sk/infrastructure/__init__.py deleted file mode 100644 index 144e2c5d..00000000 --- a/backend/app/agents/unity_mcp_sk/infrastructure/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Infrastructure adapters for Unity MCP SK integration.""" - diff --git a/backend/app/agents/unity_mcp_sk/infrastructure/mcp_client.py b/backend/app/agents/unity_mcp_sk/infrastructure/mcp_client.py deleted file mode 100644 index 69dd974e..00000000 --- a/backend/app/agents/unity_mcp_sk/infrastructure/mcp_client.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Infrastructure MCP client adapter for Unity MCP SK integration.""" - -from __future__ import annotations - -from typing import Any - -from ..core.contracts import McpToolClient - - -class UnityMcpClientAdapter(McpToolClient): - """ - Minimal adapter around an MCP session-like object. - - The wrapped object must expose: - - `list_tools()` -> object with `.tools` or a dict containing `tools` - - `call_tool(tool_name, arguments)` -> tool result - """ - - def __init__(self, session: Any) -> None: - if session is None: - raise ValueError("session must not be None") - self._session = session - - async def list_tools(self) -> list[dict[str, Any]]: - raw = await self._session.list_tools() - if isinstance(raw, dict): - tools = raw.get("tools", []) - return [item for item in tools if isinstance(item, dict)] - tools = getattr(raw, "tools", []) - if not isinstance(tools, list): - return [] - return [item for item in tools if isinstance(item, dict)] - - async def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: - if not tool_name or not isinstance(tool_name, str): - raise ValueError("tool_name must be a non-empty string.") - if arguments is None or not isinstance(arguments, dict): - raise ValueError("arguments must be a dictionary.") - return await self._session.call_tool(tool_name, arguments) - diff --git a/backend/app/agents/unity_mcp_sk/plugin/__init__.py b/backend/app/agents/unity_mcp_sk/plugin/__init__.py deleted file mode 100644 index 08a84225..00000000 --- a/backend/app/agents/unity_mcp_sk/plugin/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Plugin runtime layer for Unity MCP SK integration.""" - diff --git a/backend/app/agents/unity_mcp_sk/plugin/unity_mcp_semantic_plugin.py b/backend/app/agents/unity_mcp_sk/plugin/unity_mcp_semantic_plugin.py deleted file mode 100644 index 36aee4cb..00000000 --- a/backend/app/agents/unity_mcp_sk/plugin/unity_mcp_semantic_plugin.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Unity MCP semantic plugin runtime abstraction.""" - -from __future__ import annotations - -from typing import Any - -from ..core.contracts import McpToolClient, ToolDefinition, ToolDefinitionMapper -from ..core.tool_mapper import DefaultToolDefinitionMapper, parse_mcp_tool_definition - - -class UnityMcpSemanticPlugin: - """Owns MCP discovery state and tool invocation behavior for SK registration.""" - - def __init__(self, client: McpToolClient, mapper: ToolDefinitionMapper | None = None) -> None: - if client is None: - raise ValueError("client must not be None.") - self._client = client - self._mapper = mapper or DefaultToolDefinitionMapper() - self._is_initialized = False - - @property - def is_initialized(self) -> bool: - """Return whether discovery has completed at least once.""" - return self._is_initialized - - async def initialize(self) -> None: - """ - Discover MCP tools once and cache them in the mapper. - - Repeated calls are idempotent and do not re-query the MCP server. - """ - if self._is_initialized: - return - raw_tools = await self._client.list_tools() - parsed_tools = [parse_mcp_tool_definition(raw_tool) for raw_tool in raw_tools] - self._mapper.initialize(parsed_tools) - self._is_initialized = True - - async def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: - """ - Invoke an MCP tool through the configured client. - - Args: - tool_name: Name of the tool to invoke. - arguments: Tool arguments. - - Returns: - Raw MCP tool invocation result. - """ - return await self._client.invoke_tool(tool_name=tool_name, arguments=arguments) - - def get_tool_by_name(self, name: str) -> ToolDefinition | None: - """Return a single registered tool definition by name.""" - return self._mapper.get_tool_by_name(name) - - def get_tool_names(self) -> list[str]: - """Return all registered tool names in deterministic order.""" - return self._mapper.get_tool_names() - - def get_registered_tools(self) -> list[ToolDefinition]: - """Return all registered tool definitions in deterministic order.""" - return self._mapper.get_registered_tools() - diff --git a/backend/app/agents/unity_mcp_sk/sk_integration/__init__.py b/backend/app/agents/unity_mcp_sk/sk_integration/__init__.py deleted file mode 100644 index 531ee72f..00000000 --- a/backend/app/agents/unity_mcp_sk/sk_integration/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Semantic Kernel registration/factory helpers for Unity MCP tools.""" - diff --git a/backend/app/agents/unity_mcp_sk/sk_integration/kernel_factory.py b/backend/app/agents/unity_mcp_sk/sk_integration/kernel_factory.py deleted file mode 100644 index ee686a06..00000000 --- a/backend/app/agents/unity_mcp_sk/sk_integration/kernel_factory.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Convenience kernel factory for Unity MCP Semantic Kernel integration.""" - -from __future__ import annotations - -from semantic_kernel import Kernel - -from ..core.contracts import McpToolClient -from ..plugin.unity_mcp_semantic_plugin import UnityMcpSemanticPlugin -from .kernel_registration import register_unity_router_function, register_unity_tools_as_functions - - -async def create_kernel_with_unity_async( - client: McpToolClient, - *, - plugin_name: str = "unity", - expanded_mode: bool = True, -) -> tuple[Kernel, UnityMcpSemanticPlugin]: - """ - Create an SK kernel with Unity MCP integration. - - Args: - client: MCP client used for discovery and invocation. - plugin_name: SK plugin namespace for Unity functions. - expanded_mode: True to register one function per tool (recommended), - False to keep router mode with a single invoke function. - - Returns: - Tuple of configured kernel and initialized Unity MCP plugin runtime. - - Raises: - ValueError: If inputs are invalid. - - Example: - >>> # doctest: +SKIP - >>> kernel, unity_plugin = await create_kernel_with_unity_async(client, expanded_mode=True) - """ - if client is None: - raise ValueError("client must not be None.") - if not plugin_name or not isinstance(plugin_name, str): - raise ValueError("plugin_name must be a non-empty string.") - - unity_plugin = UnityMcpSemanticPlugin(client=client) - await unity_plugin.initialize() - - kernel = Kernel() - if expanded_mode: - register_unity_tools_as_functions(kernel=kernel, plugin=unity_plugin, plugin_name=plugin_name) - else: - register_unity_router_function(kernel=kernel, plugin=unity_plugin, plugin_name=plugin_name) - - return kernel, unity_plugin - diff --git a/backend/app/agents/unity_mcp_sk/sk_integration/kernel_registration.py b/backend/app/agents/unity_mcp_sk/sk_integration/kernel_registration.py deleted file mode 100644 index c694cc90..00000000 --- a/backend/app/agents/unity_mcp_sk/sk_integration/kernel_registration.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Helpers for registering Unity MCP tools into Semantic Kernel.""" - -from __future__ import annotations - -from typing import Any - -from semantic_kernel import Kernel -from semantic_kernel.functions import KernelPlugin, kernel_function -from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod -from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata - -from ..core.contracts import MCP_JSON_TYPE_TO_SK_TYPE, ToolDefinition, ToolParameterDefinition -from ..plugin.unity_mcp_semantic_plugin import UnityMcpSemanticPlugin - -_JSON_TYPE_TO_PYTHON_TYPE: dict[str, type[Any]] = { - "string": str, - "number": float, - "integer": int, - "boolean": bool, - "array": list, - "object": dict, -} - - -def _build_parameter_metadata(parameter: ToolParameterDefinition) -> KernelParameterMetadata: - default_value = parameter.default - is_required = parameter.required - if default_value is not None and is_required: - # Default values imply optional semantics for planners. - is_required = False - json_type = parameter.json_type if parameter.json_type in MCP_JSON_TYPE_TO_SK_TYPE else "object" - return KernelParameterMetadata( - name=parameter.name, - description=parameter.description, - default_value=default_value, - type_=MCP_JSON_TYPE_TO_SK_TYPE[json_type], - is_required=is_required, - type_object=_JSON_TYPE_TO_PYTHON_TYPE.get(json_type, dict), - schema_data=parameter.schema, - ) - - -def _build_return_metadata(tool_definition: ToolDefinition) -> KernelParameterMetadata: - return KernelParameterMetadata( - name="return", - description=f"Result from MCP tool '{tool_definition.name}'.", - default_value=None, - type_="object", - is_required=True, - type_object=dict, - schema_data=tool_definition.return_schema or {"type": "object"}, - ) - - -def _create_tool_function(tool_definition: ToolDefinition, plugin: UnityMcpSemanticPlugin, plugin_name: str) -> Any: - @kernel_function(name=tool_definition.name, description=tool_definition.description) - async def _invoke_tool(**kwargs: Any) -> Any: - return await plugin.invoke_tool(tool_definition.name, kwargs) - - return KernelFunctionFromMethod( - method=_invoke_tool, - plugin_name=plugin_name, - parameters=[_build_parameter_metadata(parameter) for parameter in tool_definition.parameters], - return_parameter=_build_return_metadata(tool_definition), - additional_metadata={ - "mcp_input_schema": tool_definition.raw_input_schema, - "mcp_return_schema": tool_definition.return_schema, - }, - ) - - -def register_unity_tools_as_functions( - kernel: Kernel, - plugin: UnityMcpSemanticPlugin, - plugin_name: str = "unity", -) -> KernelPlugin: - """ - Register discovered MCP tools as individual Semantic Kernel functions. - - Args: - kernel: Target Semantic Kernel instance. - plugin: Initialized Unity MCP semantic plugin. - plugin_name: SK namespace under which all tool functions are registered. - - Returns: - Registered KernelPlugin handle. - - Raises: - RuntimeError: If plugin has no discovered tools (not initialized or empty). - """ - if kernel is None: - raise ValueError("kernel must not be None.") - if plugin is None: - raise ValueError("plugin must not be None.") - if not plugin_name or not isinstance(plugin_name, str): - raise ValueError("plugin_name must be a non-empty string.") - - tool_definitions = plugin.get_registered_tools() - if not tool_definitions: - raise RuntimeError( - "Unity MCP plugin has no registered tools. Initialize plugin before registering with Semantic Kernel." - ) - - functions = [_create_tool_function(tool_definition, plugin, plugin_name) for tool_definition in tool_definitions] - sk_plugin = KernelPlugin(name=plugin_name, description="Unity MCP tools", functions=functions) - return kernel.add_plugin(sk_plugin, plugin_name=plugin_name) - - -def register_unity_router_function( - kernel: Kernel, - plugin: UnityMcpSemanticPlugin, - plugin_name: str = "unity", -) -> KernelPlugin: - """Register a single generic router function for backward compatibility.""" - if kernel is None: - raise ValueError("kernel must not be None.") - if plugin is None: - raise ValueError("plugin must not be None.") - if not plugin_name or not isinstance(plugin_name, str): - raise ValueError("plugin_name must be a non-empty string.") - - @kernel_function( - name="invoke", - description="Invoke any Unity MCP tool by passing tool_name and arguments.", - ) - async def _invoke(**kwargs: Any) -> Any: - raw_arguments = kwargs.get("arguments") - if hasattr(raw_arguments, "items"): - argument_payload = dict(raw_arguments) - elif isinstance(raw_arguments, dict): - argument_payload = raw_arguments - else: - argument_payload = {} - - tool_name = kwargs.get("tool_name") or argument_payload.get("tool_name") - if not tool_name or not isinstance(tool_name, str): - raise ValueError("tool_name must be a non-empty string.") - - # Some SK call paths wrap both router args into one payload object. - if "arguments" in argument_payload and isinstance(argument_payload.get("arguments"), dict): - resolved_arguments = argument_payload["arguments"] - else: - resolved_arguments = argument_payload - if not isinstance(resolved_arguments, dict): - raise ValueError("arguments must be an object when provided.") - return await plugin.invoke_tool(tool_name, resolved_arguments) - - router_function = KernelFunctionFromMethod( - method=_invoke, - plugin_name=plugin_name, - parameters=[ - KernelParameterMetadata( - name="tool_name", - description="MCP tool name to invoke.", - default_value=None, - type_="string", - is_required=True, - type_object=str, - schema_data={"type": "string"}, - ), - KernelParameterMetadata( - name="arguments", - description="Arguments object passed to the selected MCP tool.", - default_value={}, - type_="object", - is_required=False, - type_object=dict, - schema_data={"type": "object"}, - ), - ], - return_parameter=KernelParameterMetadata( - name="return", - description="Result from the selected MCP tool.", - default_value=None, - type_="object", - is_required=True, - type_object=dict, - schema_data={"type": "object"}, - ), - ) - - sk_plugin = KernelPlugin(name=plugin_name, description="Unity MCP router", functions=[router_function]) - return kernel.add_plugin(sk_plugin, plugin_name=plugin_name) - diff --git a/backend/requirements.txt b/backend/requirements.txt index e50d0ace..85037c60 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,10 +1,16 @@ fastapi uvicorn mcp[cli] -semantic-kernel[mcp]>=1.15.0 pydantic requests Pillow jinja2 google-genai -anthropic \ No newline at end of file +anthropic +semantic-kernel[mcp]>=1.15.0 +# unity-mcp-plugin (Semantic Kernel bridge for Unity MCP) — use ONE of: +# (A) No sibling folder: install from Git (HTTPS or SSH). Example if you have access: +# unity-mcp-plugin @ git+ssh://git@github.com/Ozymandros/Unity-MCP-SK-Plugin.git +# (Public HTTPS fails with 404 if the repo is private — use SSH, a fork, or a wheel.) +# (B) Default dev layout: clone Unity-MCP-SK-Plugin next to this repo as "unity-mcp-server-plugin": +-e ../unity-mcp-server-plugin \ No newline at end of file diff --git a/backend/tests/test_unity_mcp_pluginwrapper.py b/backend/tests/test_unity_mcp_pluginwrapper.py index 38014676..936b017e 100644 --- a/backend/tests/test_unity_mcp_pluginwrapper.py +++ b/backend/tests/test_unity_mcp_pluginwrapper.py @@ -1,14 +1,42 @@ +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock, patch + import pytest +from app.agents.unity_mcp_plugin import create_unity_mcp_plugin, unity_mcp_plugin_available_for_writing + -# When re-enabling: update tests to match the new Unity-MCP-Server contract -# (projectPath + fileName/folderName on tools, ping with no args). -@pytest.mark.skip(reason="Requires running MCP server or better mocking of async connection") @pytest.mark.asyncio -async def test_unity_mcp_pluginwrapper_connect_and_discover(monkeypatch): - pass +async def test_unity_mcp_pluginwrapper_create_initialize_and_cleanup() -> None: + """Ensure wrapper creates plugin via local package and performs lifecycle calls.""" + mock_plugin = MagicMock() + mock_plugin.initialize = AsyncMock() + mock_plugin.cleanup = AsyncMock() + + with patch("app.agents.unity_mcp_plugin.UnityMCPPlugin.create", return_value=mock_plugin) as mock_create: + async with create_unity_mcp_plugin() as plugin: + assert plugin is mock_plugin + mock_create.assert_called_once() + mock_plugin.initialize.assert_awaited_once() + mock_plugin.cleanup.assert_awaited_once() + -@pytest.mark.skip(reason="Requires running MCP server or better mocking of async connection") @pytest.mark.asyncio -async def test_unity_mcp_pluginwrapper_error(monkeypatch): - pass +async def test_unity_mcp_pluginwrapper_error_propagates() -> None: + """Ensure initialization errors are not swallowed by wrapper context manager.""" + mock_plugin = MagicMock() + mock_plugin.initialize = AsyncMock(side_effect=RuntimeError("boom")) + mock_plugin.cleanup = AsyncMock() + + with patch("app.agents.unity_mcp_plugin.UnityMCPPlugin.create", return_value=mock_plugin): + with pytest.raises(RuntimeError, match="boom"): + async with create_unity_mcp_plugin(): + pass + mock_plugin.cleanup.assert_not_awaited() + + +def test_unity_mcp_plugin_available_for_writing_false_on_failure() -> None: + """Availability helper must return False when plugin creation fails.""" + with patch("app.agents.unity_mcp_plugin.create_unity_mcp_plugin", side_effect=RuntimeError("down")): + assert unity_mcp_plugin_available_for_writing() is False diff --git a/backend/tests/test_unity_mcp_sk_integration.py b/backend/tests/test_unity_mcp_sk_integration.py deleted file mode 100644 index 89859f18..00000000 --- a/backend/tests/test_unity_mcp_sk_integration.py +++ /dev/null @@ -1,136 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import pytest -from semantic_kernel.functions.kernel_arguments import KernelArguments - -from app.agents.unity_mcp_sk.core.tool_mapper import DefaultToolDefinitionMapper, parse_mcp_tool_definition -from app.agents.unity_mcp_sk.plugin.unity_mcp_semantic_plugin import UnityMcpSemanticPlugin -from app.agents.unity_mcp_sk.sk_integration.kernel_factory import create_kernel_with_unity_async -from app.agents.unity_mcp_sk.sk_integration.kernel_registration import register_unity_tools_as_functions - - -class FakeMcpClient: - def __init__(self, tools: list[dict[str, Any]]) -> None: - self._tools = tools - self.list_tools_calls = 0 - self.invoke_calls: list[tuple[str, dict[str, Any]]] = [] - - async def list_tools(self) -> list[dict[str, Any]]: - self.list_tools_calls += 1 - return self._tools - - async def invoke_tool(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]: - self.invoke_calls.append((tool_name, arguments)) - return {"success": True, "tool": tool_name, "arguments": arguments} - - -def _build_tools_fixture() -> list[dict[str, Any]]: - return [ - { - "name": "z_ping", - "description": "Ping tool.", - "inputSchema": {"type": "object", "properties": {}, "required": []}, - }, - { - "name": "build_scene", - "description": "Build a scene from parameters.", - "inputSchema": { - "type": "object", - "required": ["projectPath", "count"], - "properties": { - "projectPath": {"type": "string", "description": "Unity project path."}, - "count": {"type": "integer", "description": "Object count."}, - "usePhysics": {"type": "boolean", "description": "Enable physics.", "default": False}, - "weights": {"type": "array", "description": "Distribution weights."}, - "settings": {"type": "object", "description": "Scene settings."}, - "temperature": {"type": "number", "description": "Numeric tuning.", "default": 0.3}, - }, - }, - "outputSchema": {"type": "object"}, - }, - { - "name": "a_echo", - "description": "Echo tool.", - "inputSchema": { - "type": "object", - "required": ["message"], - "properties": {"message": {"type": "string", "description": "Message."}}, - }, - }, - ] - - -@pytest.mark.asyncio -async def test_get_registered_tools_returns_sorted_tools() -> None: - mapper = DefaultToolDefinitionMapper() - parsed = [parse_mcp_tool_definition(item) for item in _build_tools_fixture()] - mapper.initialize(parsed) - - registered_names = [tool.name for tool in mapper.get_registered_tools()] - assert registered_names == ["a_echo", "build_scene", "z_ping"] - - -@pytest.mark.asyncio -async def test_kernel_factory_lists_tools_only_once() -> None: - client = FakeMcpClient(_build_tools_fixture()) - kernel, plugin = await create_kernel_with_unity_async(client=client, plugin_name="unity", expanded_mode=True) - - assert kernel is not None - assert plugin.is_initialized is True - assert client.list_tools_calls == 1 - - register_unity_tools_as_functions(kernel=kernel, plugin=plugin, plugin_name="unity_v2") - assert client.list_tools_calls == 1 - - -@pytest.mark.asyncio -async def test_expanded_registration_uses_single_plugin_namespace() -> None: - client = FakeMcpClient(_build_tools_fixture()) - kernel, _ = await create_kernel_with_unity_async(client=client, plugin_name="unity", expanded_mode=True) - - plugin = kernel.get_plugin("unity") - assert plugin is not None - assert "unity" in list(kernel.plugins) - assert set(plugin.functions.keys()) == {"a_echo", "build_scene", "z_ping"} - - -@pytest.mark.asyncio -async def test_registered_metadata_matches_mcp_schema() -> None: - client = FakeMcpClient(_build_tools_fixture()) - kernel, _ = await create_kernel_with_unity_async(client=client, plugin_name="unity", expanded_mode=True) - - plugin = kernel.get_plugin("unity") - function = plugin["build_scene"] - parameters = {parameter.name: parameter for parameter in function.metadata.parameters} - - assert parameters["projectPath"].type_ == "string" - assert parameters["projectPath"].is_required is True - assert parameters["count"].type_ == "integer" - assert parameters["count"].is_required is True - assert parameters["temperature"].type_ == "number" - assert parameters["temperature"].default_value == 0.3 - assert parameters["temperature"].is_required is False - assert parameters["usePhysics"].type_ == "boolean" - assert parameters["usePhysics"].default_value is False - assert parameters["weights"].type_ == "array" - assert parameters["settings"].type_ == "object" - - -@pytest.mark.asyncio -async def test_router_mode_still_works() -> None: - client = FakeMcpClient(_build_tools_fixture()) - kernel, _ = await create_kernel_with_unity_async(client=client, plugin_name="unity", expanded_mode=False) - - plugin = kernel.get_plugin("unity") - assert set(plugin.functions.keys()) == {"invoke"} - - result = await kernel.invoke( - function_name="invoke", - plugin_name="unity", - arguments=KernelArguments(tool_name="a_echo", arguments={"message": "hello"}), - ) - assert result is not None - assert client.invoke_calls == [("a_echo", {"message": "hello"})] - diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3a085564..bd21085a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -340,7 +340,7 @@ Finalize supports two backends. In **Unity Engine Settings** (or the finalize re - **`batch`**: Always use injected Editor scripts and Unity batch mode. Use this for CI or headless environments where no Editor window is running. - **`mcp`**: Always use the MCP-Unity (Semantic Kernel) plugin. Fails with a clear error if the plugin is not configured. -To use MCP for local development, set **`UNITY_USE_MCP=1`** and either **`UNITY_MCP_SERVER_URL`** (HTTP) or ensure **`unity-mcp`** (or the command in **`UNITY_MCP_COMMAND`**) is on PATH for stdio. The backend connects via the MCP Python SDK and calls the Unity-MCP-Server contract tools. For CI pipelines, set `unity_automation_mode` to `"batch"` so finalize does not depend on MCP. See [Unity Engine Integration](UNITY_INTEGRATION.md) for the tool contract and configuration. +To use MCP for local development, set **`UNITY_USE_MCP=1`** and either **`UNITY_MCP_SERVER_URL`** (HTTP) or ensure **`unity-mcp`** (or the command in **`UNITY_MCP_COMMAND`**) is on PATH for stdio. The backend connects via the MCP Python SDK and calls the Unity-MCP-Server contract tools. For CI pipelines, set `unity_automation_mode` to `"batch"` so finalize does not depend on MCP. **Server install, Git clone URL, and where env vars are read:** [UNITY_MCP_SERVER.md](UNITY_MCP_SERVER.md). See [Unity Engine Integration](UNITY_INTEGRATION.md) for the tool contract and configuration. ### Debugging Unity batch execution diff --git a/docs/UNITY_MCP_INTEGRATION.md b/docs/UNITY_MCP_INTEGRATION.md index aeb3ef5e..48782bc5 100644 --- a/docs/UNITY_MCP_INTEGRATION.md +++ b/docs/UNITY_MCP_INTEGRATION.md @@ -1,6 +1,22 @@ # Unity MCP Integration with Semantic Kernel -This document describes the integration of Unity's Model Context Protocol (MCP) with Semantic Kernel using the official MCPSsePlugin, replacing legacy TCP/JSON-RPC code with a robust, maintainable, and auto-discoverable approach. +> **Unity MCP Server (install, Git URL, env vars — not the Python plugin):** see **[UNITY_MCP_SERVER.md](./UNITY_MCP_SERVER.md)**. + +This document describes the integration of Unity's Model Context Protocol (MCP) with Semantic Kernel using the Python **`unity-mcp-plugin`** package from **[Unity-MCP-SK-Plugin](https://github.com/Ozymandros/Unity-MCP-SK-Plugin)** (Git URL or local editable install—see `backend/requirements.txt`). The backend talks to the **Unity MCP Server** over stdio. Both pieces are required and are separate GitHub repositories. + +## Required dependencies (two repositories) + +| Component | Purpose | GitHub repository | How this project uses it | +|-----------|---------|-------------------|----------------------------| +| **Unity-MCP-Server** | Official MCP server that exposes Unity Editor automation tools (JSON-RPC over stdio when used as a global .NET tool). | **[github.com/Ozymandros/Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** | Install and run the `unity-mcp` global tool (or your built server executable). Point `UNITY_MCP_COMMAND` at that executable if it is not on `PATH`. | +| **unity-mcp-plugin** (Python distribution) | Semantic Kernel bridge: discovers MCP tools and registers them as SK functions. | **[github.com/Ozymandros/Unity-MCP-SK-Plugin](https://github.com/Ozymandros/Unity-MCP-SK-Plugin)** | `backend/requirements.txt`: default **`-e ../unity-mcp-server-plugin`**, or switch to **`unity-mcp-plugin @ git+...`** (SSH/fork if private). Imported as `from unity_mcp import UnityMCPPlugin, UnityMcpOptions`. | + +**Install order (typical dev setup):** + +1. Clone **[Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** and install the server per its README (often a global .NET tool named `unity-mcp` on your `PATH`; follow that repo’s install script or `dotnet tool` instructions). +2. Install **`unity-mcp-plugin`**: either follow the **default** in `backend/requirements.txt` (sibling clone + `-e`), **or** comment that out and use **`unity-mcp-plugin @ git+ssh://...`** (or your fork) so the app does not depend on a fixed folder next to this repo. If `git+https` fails with **404**, the GitHub repo is likely **private**—use SSH, a public fork, or a wheel. + +**Optional dev workflow:** clone **Unity-MCP-SK-Plugin** anywhere and use `pip install -e /path/to/clone` while developing the bridge. ## Path terminology (don’t confuse these) @@ -10,13 +26,13 @@ This document describes the integration of Unity's Model Context Protocol (MCP) ## Overview - **No Unity/C# code required**: This integration is entirely Python-side. - **Auto-discovery**: All Unity tools exposed by the MCP server are automatically available to Semantic Kernel. -- **No manual kernel_function decorators**: The MCPSsePlugin handles tool registration. +- **No manual kernel_function decorators**: The local Unity MCP plugin package handles tool registration. - **Robust error handling**: Connection and protocol errors are surfaced clearly. ## Configuration -- **Install requirements**: Ensure `semantic-kernel[mcp]` is in `backend/requirements.txt`. +- **Install requirements**: Ensure `backend/requirements.txt` installs **`unity-mcp-plugin`** (see comments in that file: Git URL vs `-e` sibling path). - **Set MCP server command** (this is the **Unity MCP Server executable**, not a Unity project path): - - `UNITY_MCP_COMMAND`: Path to the MCP server executable (default: `unity-mcp-server`) + - `UNITY_MCP_COMMAND`: Path to the MCP server executable (default: `unity-mcp`) - `UNITY_MCP_ARGS`: Arguments for the executable (default: empty) Example (path to the **server executable**, not to a Unity project): @@ -25,7 +41,7 @@ export UNITY_MCP_COMMAND="C:\Path\To\UnityMcp.Server.exe" ``` ## Usage -- The integration is implemented in `backend/app/agents/unity_mcp_plugin.py` as `UnityMCPPluginWrapper`. +- The integration is implemented in `backend/app/agents/unity_mcp_plugin.py` using `unity_mcp.UnityMCPPlugin` from the **`unity-mcp-plugin`** package. - The agent in `backend/app/agents/unity_agent.py` uses this wrapper to connect and interact with Unity tools. - All Unity automation tools (e.g., create_scene, create_script) are auto-discovered and callable from the agent. @@ -60,8 +76,11 @@ export UNITY_MCP_COMMAND="C:\Path\To\UnityMcp.Server.exe" - All Unity tool changes/additions are now automatically available to the agent—no Python code changes required. ## References -- [Semantic Kernel MCP Plugin Documentation](https://learn.microsoft.com/en-us/semantic-kernel/concepts/plugins/adding-mcp-plugins) -- [Model Context Protocol (MCP) Spec](https://modelcontextprotocol.io/) +- **[Unity MCP Server — install & config in this repo](./UNITY_MCP_SERVER.md)** (Git URLs, `UNITY_MCP_COMMAND`, file locations) +- [Unity-MCP-Server (GitHub)](https://github.com/Ozymandros/Unity-MCP-Server) +- [Unity-MCP-SK-Plugin / unity-mcp-plugin (GitHub)](https://github.com/Ozymandros/Unity-MCP-SK-Plugin) +- [Semantic Kernel — MCP plugins](https://learn.microsoft.com/en-us/semantic-kernel/concepts/plugins/adding-mcp-plugins) +- [Model Context Protocol (MCP) spec](https://modelcontextprotocol.io/) --- diff --git a/docs/UNITY_MCP_SERVER.md b/docs/UNITY_MCP_SERVER.md new file mode 100644 index 00000000..7334784c --- /dev/null +++ b/docs/UNITY_MCP_SERVER.md @@ -0,0 +1,116 @@ +# Unity MCP Server (Unity-MCP-Server) + +This document is about the **MCP server that talks to the Unity Editor** — **not** the Python `unity-mcp-plugin` package. +For the Semantic Kernel Python bridge, see [UNITY_MCP_INTEGRATION.md](./UNITY_MCP_INTEGRATION.md) and [UNITY_MCP_SK_INTEGRATION.md](./UNITY_MCP_SK_INTEGRATION.md). + +## Source repository (install here) + +| Item | Value | +|------|--------| +| **GitHub** | **[github.com/Ozymandros/Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** | +| **Clone (HTTPS)** | `https://github.com/Ozymandros/Unity-MCP-Server.git` | +| **Typical CLI name** | `unity-mcp` (global .NET tool or built executable — follow upstream README) | + +**Installation** is defined **upstream**: clone the repo and follow **Unity-MCP-Server** `README.md` (e.g. `dotnet tool install`, Unity package, or build steps). This Unity-Generator repo does **not** ship the server binary; it expects the executable to exist locally or on `PATH`. + +### Docker (only if upstream provides it) + +Check **[Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** for a **`Dockerfile`**, **`docker-compose.yml`**, or a **Docker** section in `README.md`. If the project does **not** document Docker, use clone + CLI install above (or ask upstream to add a container workflow). + +When Docker *is* documented upstream, use their image name, ports, and env vars as the source of truth. Below is how that maps to **Unity-Generator** only. + +| Integration path in this app | Transport | How Docker usually fits | +|------------------------------|-----------|-------------------------| +| **Finalize / orchestrator** (`unity_mcp_client.py`) | **Streamable HTTP** if you set **`UNITY_MCP_SERVER_URL`** | Run the container with a **published port** (e.g. `-p 8080:8080`). Set **`UNITY_USE_MCP=1`** and **`UNITY_MCP_SERVER_URL`** to the MCP HTTP endpoint the server exposes (must match upstream — often `http://127.0.0.1:/...`). In `unity_mcp_client.py`, if **`UNITY_MCP_SERVER_URL`** is set, the **HTTP** client is used and **stdio** (`UNITY_MCP_COMMAND`) is not used for that call. | +| **Semantic Kernel agent** (`unity_mcp_plugin.py`) | **stdio** (spawns **`UNITY_MCP_COMMAND`**) | A container is **not** a drop-in replacement unless you run a **host binary** or a small wrapper. Prefer installing the server on the host for the agent, or extend **`unity-mcp-plugin`** / server to support HTTP for the SK path (not configured in this repo today). | + +**Typical Docker flow for this app (HTTP-capable server):** + +1. Build/run the image **as documented in Unity-MCP-Server** (example shape only — replace with upstream values): + + ```bash + # Example only; use the exact commands from Ozymandros/Unity-MCP-Server README + docker compose up -d + # or: docker run -p : + ``` + +2. Confirm the Unity Editor / MCP server can still communicate (same machine or documented networking). + +3. For **finalize** MCP tools, set for the backend process: + + ```sh + set UNITY_USE_MCP=1 + set UNITY_MCP_SERVER_URL=http://127.0.0.1: + ``` + + Use the **same URL** your browser or `curl` would use for the server’s **Streamable HTTP** MCP endpoint per upstream docs. + +4. For HTTP-only finalize flows, **`UNITY_MCP_SERVER_URL`** alone is enough for `unity_mcp_client` (stdio command is skipped when the URL is set). + +**Security:** If the container exposes MCP on a network interface, restrict with firewall, bind to `127.0.0.1`, or upstream auth tokens — do not expose an unsecured MCP endpoint to the internet. + +--- + +## What this app uses it for + +- **Semantic Kernel agent** (`unity_mcp_plugin.py`): spawns/connects to the server process for **tool discovery** and Unity automation from the LLM. +- **Finalize / orchestrator** (`unity_mcp_client.py`, optional): when **`UNITY_USE_MCP=1`**, uses the MCP Python SDK to call **Unity-MCP-Server** tools (e.g. package install, scene setup) over **stdio** or **Streamable HTTP**. + +--- + +## Where configuration is defined in this repository + +### 1. Environment variables (runtime — backend) + +| Variable | Purpose | Default / notes | +|----------|-----------|-----------------| +| **`UNITY_MCP_COMMAND`** | Path or name of the **server executable** (not your Unity project folder). | `unity-mcp` | +| **`UNITY_MCP_ARGS`** | Extra arguments passed to that executable (space-separated in code). | empty | +| **`UNITY_MCP_SERVER_URL`** | Optional **HTTP** MCP endpoint (Streamable HTTP). If set, used when applicable; otherwise **stdio** via `UNITY_MCP_COMMAND`. | empty | +| **`UNITY_USE_MCP`** | **`1` / `true` / `yes`** enables MCP in **`unity_mcp_client.py`** (finalize/orchestrator path). | off (MCP client not used) | + +**Code references:** + +- `backend/app/agents/unity_mcp_plugin.py` — `UNITY_MCP_COMMAND`, `UNITY_MCP_ARGS` (SK plugin). +- `backend/app/services/unity_mcp_client.py` — `UNITY_USE_MCP`, `UNITY_MCP_SERVER_URL`, `UNITY_MCP_COMMAND`, `UNITY_MCP_ARGS`. + +### 2. Sample MCP JSON (optional) + +- **`backend/mcp_config.json`** — example `mcpServers.unity-mcp-server` with `command` / `args` (for tools that consume MCP-style config; **not** the only source of truth for FastAPI). + +### 3. Cursor / VS Code MCP (IDE only) + +- **`.vscode/mcp.json`** — **`mcpServers.unity-mcp-server`**: `command` + `args` for the **editor** MCP client. This configures **Cursor/VS Code**, not the Unity-Generator backend unless you mirror the same env vars. + +**Important:** `UNITY_MCP_*` env vars apply to **Python processes** (backend). The IDE file is separate; keep **`unity-mcp`** on `PATH` or align `command` with the same binary you use for the backend. + +### 4. Docs cross-references + +- **`docs/DEVELOPMENT.md`** — Automation mode (`UNITY_USE_MCP`, batch vs MCP). +- **`docs/UNITY_MCP_INTEGRATION.md`** — End-to-end SK + server overview. +- **`backend/app/agents/SKILLS_USAGE.md`** — Unity MCP skill notes and server link. + +--- + +## Path terminology (server vs Unity project) + +- **`UNITY_MCP_COMMAND`** must point to the **MCP server binary** (e.g. `unity-mcp` or `UnityMcp.Server.exe`), **not** to the Unity project root (`Assets/`). +- Your **Unity project path** is passed separately to tools (e.g. `projectPath` / session) — see [UNITY_MCP_INTEGRATION.md](./UNITY_MCP_INTEGRATION.md) § Path terminology. + +--- + +## Quick checklist + +1. Install **Unity-MCP-Server** from **[GitHub](https://github.com/Ozymandros/Unity-MCP-Server)** (upstream README), **or** run it via **Docker** if upstream documents `Dockerfile` / compose (see [Docker (only if upstream provides it)](#docker-only-if-upstream-provides-it)). +2. Ensure the server is running with Unity / Editor as required by that project. +3. Set **`UNITY_MCP_COMMAND`** if `unity-mcp` is not on `PATH` (stdio / SK agent path). +4. For finalize MCP automation, set **`UNITY_USE_MCP=1`** and configure **stdio** (`UNITY_MCP_COMMAND`) or **`UNITY_MCP_SERVER_URL`** (Docker/HTTP) per `unity_mcp_client.py`. + +--- + +## Related repositories + +| Role | GitHub | +|------|--------| +| **MCP server (this page)** | [Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server) | +| **Python SK bridge (`unity_mcp` package)** | [Unity-MCP-SK-Plugin](https://github.com/Ozymandros/Unity-MCP-SK-Plugin) | diff --git a/docs/UNITY_MCP_SK_INTEGRATION.md b/docs/UNITY_MCP_SK_INTEGRATION.md index c7677e3e..56c5f533 100644 --- a/docs/UNITY_MCP_SK_INTEGRATION.md +++ b/docs/UNITY_MCP_SK_INTEGRATION.md @@ -1,74 +1,60 @@ -# Unity MCP + Semantic Kernel Integration +# Unity MCP + Semantic Kernel Integration (Python) -## Python Integration Modes +> **Install/configure the Unity MCP *server* (Git URL, env vars):** [UNITY_MCP_SERVER.md](./UNITY_MCP_SERVER.md). -The Python integration now supports two registration modes: +This project relies on the **`unity-mcp-plugin`** Python package for Semantic Kernel integration. Source and releases live in **[Unity-MCP-SK-Plugin](https://github.com/Ozymandros/Unity-MCP-SK-Plugin)**. That plugin is a **client** of the MCP server **[Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** (stdio): you must have the server executable available (e.g. `unity-mcp` on `PATH`) or set `UNITY_MCP_COMMAND` (see [UNITY_MCP_INTEGRATION.md](./UNITY_MCP_INTEGRATION.md)). -- **Expanded mode (recommended)**: each discovered MCP tool is exposed as an individual Semantic Kernel function under one plugin namespace (default: `unity`). -- **Router mode (backward compatible)**: one generic `invoke` function routes calls to a chosen MCP tool. +| Piece | Repository | +|-------|------------| +| MCP server (tools, Unity contract) | [github.com/Ozymandros/Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server) | +| Python SK plugin (`unity_mcp` package) | [github.com/Ozymandros/Unity-MCP-SK-Plugin](https://github.com/Ozymandros/Unity-MCP-SK-Plugin) | -Expanded mode improves planner/autonomous agent tool selection because functions and parameter schemas are visible as first-class SK functions. +**Dependency:** `backend/requirements.txt` documents two ways to get **`unity-mcp-plugin`**: **Git URL** (no sibling folder) or **`-e ../unity-mcp-server-plugin`** (default sibling clone). Use **SSH** or a fork if the GitHub repo is private. For hacking the bridge, clone that repo anywhere and use `pip install -e `. ## Expanded Mode (Recommended) ```python -from app.agents.unity_mcp_sk.sk_integration.kernel_factory import create_kernel_with_unity_async +from unity_mcp import UnityMCPPlugin -# client must implement list_tools() and invoke_tool(tool_name, arguments) -kernel, unity_plugin = await create_kernel_with_unity_async( - client=my_mcp_client, - plugin_name="unity", - expanded_mode=True, -) +# Discovers MCP tools and registers them as individual SK functions +# under one plugin namespace ("unity" by default). +kernel = await UnityMCPPlugin.create_kernel_with_unity(plugin_name="unity") -# SK now sees one plugin namespace ("unity") with many functions: -# - unity_create_scene -# - unity_add_gameobject -# - unity_create_material -# - ... +# Example invocation: +result = await kernel.invoke("unity", "unity_create_scene", projectPath="C:/MyProj", fileName="Assets/Scenes/Main.unity") ``` ## Router Mode (Backward Compatible) ```python -from app.agents.unity_mcp_sk.sk_integration.kernel_factory import create_kernel_with_unity_async +from semantic_kernel import Kernel +from unity_mcp import UnityMCPPlugin -kernel, unity_plugin = await create_kernel_with_unity_async( - client=my_mcp_client, - plugin_name="unity", - expanded_mode=False, -) +plugin = UnityMCPPlugin.create() +await plugin.initialize() + +kernel = Kernel() +kernel.add_plugin(plugin, plugin_name="unity") -# SK sees one function: -# unity.invoke(tool_name, arguments) +result = await kernel.invoke( + "unity", + "invoke_unity_tool", + tool_name="unity_create_scene", + arguments_json='{"projectPath":"C:/MyProj","fileName":"Assets/Scenes/Main.unity"}', +) ``` ## Tradeoffs -- **Expanded mode** - - Better discoverability and tool-calling reliability for planners/agents. - - Richer per-tool metadata (name, description, required/default, type mapping). - - Slightly larger function registry and token footprint. -- **Router mode** - - Smallest function footprint (single generic function). - - Weaker discoverability, often requiring stronger prompting to choose tools correctly. - -## Discovery and Registration Behavior - -- Tool discovery (`list_tools`) is performed once during plugin initialization. -- Registration uses the mapper's cached `get_registered_tools()` output as the source of truth. -- Tool registration order is deterministic (sorted by tool name). +- **Expanded mode**: better discoverability and more reliable autonomous tool calling. +- **Router mode**: smaller tool-definition footprint but weaker discoverability. -## Metadata Fidelity Notes +## Metadata Fidelity -For each MCP tool parameter, the integration preserves: +The `unity-mcp-plugin` package maps MCP tool schemas into SK metadata (parameter name, description, required/default, and base JSON types: `string`, `number`, `integer`, `boolean`, `array`, `object`). -- exact parameter name, -- description, -- required/optional state, -- default value when present, -- JSON type mapping: `string`, `number`, `integer`, `boolean`, `array`, `object`. +Python SK limitation: advanced JSON Schema constructs are partially surfaced (typically via schema metadata) but not always fully interpreted as first-class planner constraints. -Current Python SK limitation/workaround: +## Router / parameter safety -- Complex JSON Schema constructs (`oneOf`, `anyOf`, tuple schemas, advanced constraints) are passed through in `schema_data`, but SK planner behavior primarily uses simplified `KernelParameterMetadata` fields (`type_`, `is_required`, `default_value`). +Older in-repo experiments under `backend/app/agents/unity_mcp_sk/` (removed) duplicated router logic; that path no longer exists here. The canonical implementation lives in the `unity-mcp-plugin` package: expanded-mode handlers only forward kwargs whose names match the MCP tool schema, and `invoke_unity_tool` strips `tool_name` / `toolName` from parsed JSON so they are never sent as MCP parameters. diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index 47d9d877..ef957622 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -36,16 +36,17 @@ import id from "./locales/id"; import ja from "./locales/ja"; import vi from "./locales/vi"; import ko from "./locales/ko"; +import tr from "./locales/tr"; export type SupportedLocale = | "en" | "es" | "ca" | "eu" | "oc" | "uk" | "pt" | "gl" | "fr" | "it" | "pl" | "zh" | "ar" | "de" - | "hi" | "bn" | "ur" | "id" | "ja" | "vi" | "ko"; + | "hi" | "bn" | "ur" | "id" | "ja" | "vi" | "ko" | "tr"; export const SUPPORTED_LOCALES: SupportedLocale[] = [ "en", "es", "ca", "eu", "oc", "uk", "pt", "gl", "fr", "it", "pl", "zh", "ar", "de", - "hi", "bn", "ur", "id", "ja", "vi", "ko", + "hi", "bn", "ur", "id", "ja", "vi", "ko", "tr", ]; export const DEFAULT_LOCALE: SupportedLocale = "en"; export const LOCALE_STORAGE_KEY = "appLocale"; @@ -77,7 +78,7 @@ const i18n = createI18n({ legacy: false, // Composition API mode locale: getPersistedLocale(), fallbackLocale: DEFAULT_LOCALE, - messages: { en, es, ca, eu, oc, uk, pt, gl, fr, it, pl, zh, ar, de, hi, bn, ur, id, ja, vi, ko }, + messages: { en, es, ca, eu, oc, uk, pt, gl, fr, it, pl, zh, ar, de, hi, bn, ur, id, ja, vi, ko, tr }, // Silence missing-key warnings in production missingWarn: import.meta.env.DEV, fallbackWarn: import.meta.env.DEV, diff --git a/frontend/src/i18n/locales/ar.ts b/frontend/src/i18n/locales/ar.ts index 7321fefd..726ced3f 100644 --- a/frontend/src/i18n/locales/ar.ts +++ b/frontend/src/i18n/locales/ar.ts @@ -234,5 +234,6 @@ export default { ar: "العربية", de: "الألمانية", hi: "الهندية", bn: "البنغالية", ur: "الأردية", id: "الإندونيسية", ja: "اليابانية", vi: "الفيتنامية", ko: "الكورية", + tr: "التركية", }, } as const; diff --git a/frontend/src/i18n/locales/bn.ts b/frontend/src/i18n/locales/bn.ts index 8efaac8f..e886242b 100644 --- a/frontend/src/i18n/locales/bn.ts +++ b/frontend/src/i18n/locales/bn.ts @@ -107,5 +107,6 @@ export default { pt: "পর্তুগিজ", gl: "গ্যালিশিয়ান", fr: "ফরাসি", it: "ইতালীয়", pl: "পোলিশ", zh: "চীনা", ar: "আরবি", de: "জার্মান", hi: "হিন্দি", bn: "বাংলা", ur: "উর্দু", id: "ইন্দোনেশিয়ান", ja: "জাপানি", vi: "ভিয়েতনামি", ko: "কোরিয়ান", + tr: "তুর্কি", }, } as const; diff --git a/frontend/src/i18n/locales/ca.ts b/frontend/src/i18n/locales/ca.ts index b3c4607d..c4ee0185 100644 --- a/frontend/src/i18n/locales/ca.ts +++ b/frontend/src/i18n/locales/ca.ts @@ -299,5 +299,6 @@ export default { ja: "Japonès", vi: "Vietnamita", ko: "Coreà", + tr: "Turc", }, } as const; diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 1b48b49c..07c4f244 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -241,5 +241,6 @@ export default { zh: "Chinesisch", ar: "Arabisch", de: "Deutsch", + tr: "Türkisch", }, } as const; diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 9cbf833b..901503c4 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -315,5 +315,6 @@ export default { ja: "Japanese", vi: "Vietnamese", ko: "Korean", + tr: "Turkish", }, } as const; diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index d4b1ecf3..6cb8e27b 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -299,5 +299,6 @@ export default { ja: "Japonés", vi: "Vietnamita", ko: "Coreano", + tr: "Turco", }, } as const; diff --git a/frontend/src/i18n/locales/eu.ts b/frontend/src/i18n/locales/eu.ts index 6536bab8..978041af 100644 --- a/frontend/src/i18n/locales/eu.ts +++ b/frontend/src/i18n/locales/eu.ts @@ -299,5 +299,6 @@ export default { ja: "Japoniera", vi: "Vietnamera", ko: "Korear", + tr: "Turkiera", }, } as const; diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index ce9e84dc..982bd030 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -241,5 +241,6 @@ export default { zh: "Chinois", ar: "Arabe", de: "Allemand", + tr: "turc", }, } as const; diff --git a/frontend/src/i18n/locales/gl.ts b/frontend/src/i18n/locales/gl.ts index 00c5c9bd..d2dc72b6 100644 --- a/frontend/src/i18n/locales/gl.ts +++ b/frontend/src/i18n/locales/gl.ts @@ -241,5 +241,13 @@ export default { zh: "Chinés", ar: "Árabe", de: "Alemán", + hi: "Hindi", + bn: "Bengalí", + ur: "Urdu", + id: "Indonesio", + ja: "Xaponés", + vi: "Vietnamita", + ko: "Coreano", + tr: "Turco", }, } as const; diff --git a/frontend/src/i18n/locales/hi.ts b/frontend/src/i18n/locales/hi.ts index 90b80c81..d10d10e2 100644 --- a/frontend/src/i18n/locales/hi.ts +++ b/frontend/src/i18n/locales/hi.ts @@ -108,5 +108,6 @@ export default { pt: "पुर्तगाली", gl: "गैलिशियन", fr: "फ्रेंच", it: "इतालवी", pl: "पोलिश", zh: "चीनी", ar: "अरबी", de: "जर्मन", hi: "हिंदी", bn: "बंगाली", ur: "उर्दू", id: "इंडोनेशियाई", ja: "जापानी", vi: "वियतनामी", ko: "कोरियाई", + tr: "तुर्की", }, } as const; diff --git a/frontend/src/i18n/locales/id.ts b/frontend/src/i18n/locales/id.ts index ea6e42a8..01b1cf64 100644 --- a/frontend/src/i18n/locales/id.ts +++ b/frontend/src/i18n/locales/id.ts @@ -107,5 +107,6 @@ export default { pt: "Portugis", gl: "Galisia", fr: "Prancis", it: "Italia", pl: "Polandia", zh: "Tionghoa", ar: "Arab", de: "Jerman", hi: "Hindi", bn: "Bengali", ur: "Urdu", id: "Indonesia", ja: "Jepang", vi: "Vietnam", ko: "Korea", + tr: "Turki", }, } as const; diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 2cfdaf50..32402d36 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -241,5 +241,6 @@ export default { zh: "Cinese", ar: "Arabo", de: "Tedesco", + tr: "Turco", }, } as const; diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index a4081461..8f4d865e 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -107,5 +107,6 @@ export default { pt: "ポルトガル語", gl: "ガリシア語", fr: "フランス語", it: "イタリア語", pl: "ポーランド語", zh: "中国語", ar: "アラビア語", de: "ドイツ語", hi: "ヒンディー語", bn: "ベンガル語", ur: "ウルドゥー語", id: "インドネシア語", ja: "日本語", vi: "ベトナム語", ko: "韓国語", + tr: "トルコ語", }, } as const; diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index c3140158..66b00abd 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -107,5 +107,6 @@ export default { pt: "포르투갈어", gl: "갈리시아어", fr: "프랑스어", it: "이탈리아어", pl: "폴란드어", zh: "중국어", ar: "아랍어", de: "독일어", hi: "힌디어", bn: "벵골어", ur: "우르두어", id: "인도네시아어", ja: "일본어", vi: "베트남어", ko: "한국어", + tr: "터키어", }, } as const; diff --git a/frontend/src/i18n/locales/oc.ts b/frontend/src/i18n/locales/oc.ts index 2fa48d50..b94a8bdd 100644 --- a/frontend/src/i18n/locales/oc.ts +++ b/frontend/src/i18n/locales/oc.ts @@ -248,5 +248,6 @@ export default { ja: "Japonés", vi: "Vietnamian", ko: "Corean", + tr: "Turc", }, } as const; diff --git a/frontend/src/i18n/locales/pl.ts b/frontend/src/i18n/locales/pl.ts index 4e4c9f85..d82a93d5 100644 --- a/frontend/src/i18n/locales/pl.ts +++ b/frontend/src/i18n/locales/pl.ts @@ -241,5 +241,6 @@ export default { zh: "Chiński", ar: "Arabski", de: "Niemiecki", + tr: "turecki", }, } as const; diff --git a/frontend/src/i18n/locales/pt.ts b/frontend/src/i18n/locales/pt.ts index 02216203..a84d1fe0 100644 --- a/frontend/src/i18n/locales/pt.ts +++ b/frontend/src/i18n/locales/pt.ts @@ -241,5 +241,6 @@ export default { zh: "Chinês", ar: "Árabe", de: "Alemão", + tr: "Turco", }, } as const; diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts new file mode 100644 index 00000000..afa68fee --- /dev/null +++ b/frontend/src/i18n/locales/tr.ts @@ -0,0 +1,258 @@ +/** + * Turkish (tr) locale. + * Structure mirrors `en.ts` exactly. + */ +export default { + app: { + title: "Unity Generator", + status: { + online: "Çevrimiçi", + offline: "Çevrimdışı", + }, + nav: { + settings: "Ayarlar", + scenes: "Sahneler", + code: "Kod", + text: "Metin", + image: "Görsel", + sprites: "Sprite'lar", + audio: "Ses", + unityUi: "Unity UI", + unityPhysics: "Unity Fizik", + unityProject: "Unity Projesi", + }, + actions: { repository: "Depo" }, + }, + common: { + loading: "Yükleniyor…", + saving: "Kaydediliyor…", + save: "Kaydet", + cancel: "İptal", + close: "Kapat", + ok: "Tamam", + yes: "Evet", + no: "Hayır", + error: "Hata", + warning: "Uyarı", + success: "Başarılı", + generate: "Oluştur", + result: "Sonuç", + provider: "Sağlayıcı", + model: "Model", + temperature: "Sıcaklık", + apiKey: "API Anahtarı (isteğe bağlı)", + systemPrompt: "Sistem istemi geçersiz kılma", + advancedOptions: "Gelişmiş seçenekler", + examplePrompts: "Örnek istemler", + manageModels: "Modelleri yönet", + selectProvider: "Sağlayıcı seç", + selectModel: "Model seç", + prompt: "İstem", + leaveEmptyForGlobalKey: "Genel anahtarı kullanmak için boş bırakın", + createdFiles: "Oluşturulan dosyalar", + viewSteps: "Adımları görüntüle ({n})", + waitingForLogs: "Günlükler bekleniyor…", + errorsDetected: "Hatalar algılandı", + }, + settings: { + title: "Yapılandırma", + subtitle: "Yapay zeka motorlarınızı, modellerinizi ve sistem istemlerinizi yönetin.", + tabs: { + general: "Genel", + providers: "Sağlayıcılar", + models: "Modeller", + prompts: "İstemler", + secrets: "Gizli bilgiler", + }, + }, + general: { + title: "Genel tercihler", + sections: { + networkApi: "Ağ ve API", + preferredIntelligence: "Tercih edilen yapay zeka", + appearance: "Görünüm", + language: "Dil", + }, + fields: { + backendUrl: "Backend URL", + backendUrlHint: "Unity Generator backend hizmetinizin adresi", + outputBasePath: "Temel yol (çıktı)", + outputBasePathHint: "Oluşturulan dosyaların kaydedildiği yer", + unityEditorPath: "Unity Editor yolu (isteğe bağlı)", + unityEditorPathHint: "Unity Editor yürütülebilir dosyasının tam yolu", + preferredLlm: "Varsayılan metin / mantık sağlayıcısı", + preferredLlmModel: "Varsayılan metin / mantık modeli", + preferredImage: "Varsayılan görsel oluşturma sağlayıcısı", + preferredImageModel: "Varsayılan görsel oluşturma modeli", + preferredAudio: "Varsayılan konuşma (TTS) sağlayıcısı", + preferredAudioModel: "Varsayılan konuşma (TTS) modeli", + preferredMusic: "Varsayılan müzik oluşturma sağlayıcısı", + preferredMusicModel: "Varsayılan müzik oluşturma modeli", + theme: "Tema", + language: "Arayüz dili", + }, + actions: { saveAll: "Tüm değişiklikleri kaydet" }, + status: { + saving: "Kaydediliyor…", + saved: "Tercihler başarıyla kaydedildi.", + modelNotRegistered: '"{provider}" sağlayıcısı için "{model}" modeli kayıtlı değil.', + networkError: "Ağ hatası: Backend'e ulaşılamadı.", + }, + }, + code: { + title: "Unity C# Kodu", + subtitle: "Yapay zeka ile Unity C# betikleri oluşturun.", + fields: { prompt: "İstem", maxTokens: "Maksimum token" }, + actions: { generate: "Kod oluştur" }, + activeProject: "Aktif proje: {name}", + autoSave: "Projeye otomatik kaydet", + }, + text: { + title: "Metin oluşturma", + subtitle: "Yapay zeka ile metin içeriği oluşturun.", + fields: { prompt: "İstem", length: "Uzunluk" }, + actions: { generate: "Metin oluştur" }, + }, + image: { + title: "Görsel oluşturma", + subtitle: "Yapay zeka ile görseller oluşturun.", + fields: { + prompt: "İstem", + aspectRatio: "En-boy oranı", + quality: "Kalite", + textureName: "Doku adı", + textureType: "Doku türü", + }, + actions: { generate: "Görsel oluştur", saveToUnity: "Unity'ye kaydet" }, + }, + sprites: { + title: "2D Sprite'lar", + badge: "Piksel sanatına uygun", + subtitle: "Unity için 2D sprite varlıkları oluşturun.", + fields: { + prompt: "İstem", + resolution: "Çözünürlük", + paletteSize: "Palet boyutu", + autoCrop: "Şeffaf kenarları otomatik kırp", + colors: "{n} renk", + }, + preview: "Önizleme alanı", + actions: { generate: "Sprite oluştur" }, + }, + audio: { + title: "Ses oluşturma", + subtitle: "Yapay zeka ile ses varlıkları oluşturun.", + generationType: "Oluşturma türü", + speechTts: "Konuşma (TTS)", + atmosphericMusic: "Atmosferik müzik", + fields: { + prompt: "İstem", + musicDescription: "Müzik açıklaması", + speechPrompt: "Konuşma istemi", + voiceOptional: "Ses (isteğe bağlı)", + musicModel: "Müzik modeli", + stability: "Kararlılık", + audioClipName: "Ses klibi adı", + audioFormat: "Ses formatı", + }, + actions: { generate: "Ses oluştur", saveToUnity: "Unity'ye kaydet" }, + }, + scenes: { + title: "Sahne oluşturucu", + subtitle: "Yapay zeka ile Unity sahneleri oluşturun.", + fields: { prompt: "Sahne açıklaması" }, + actions: { generate: "Sahne oluştur" }, + mediaReady: "İçe aktarmaya hazır medya", + mediaReadyText: + '{type} "{name}" Unity\'ye aktarılmaya hazır. Aşağıdaki istemi inceleyin ve devam etmek için "Sahne oluştur"a tıklayın.', + mediaTypeImage: "Görsel", + mediaTypeAudio: "Ses", + }, + unityUi: { + title: "Unity UI", + subtitle: "Yapay zeka ile Unity UI bileşenleri oluşturun.", + fields: { + uiSystem: "UI sistemi", + elementType: "Öğe türü", + prompt: "UI öğesi açıklaması", + outputFormat: "Çıktı formatı", + anchorPreset: "Sabitleme ön ayarı", + colourTheme: "Renk teması (isteğe bağlı)", + includeAnimations: "Animasyonları dahil et", + }, + actions: { generate: "UI oluştur" }, + }, + unityPhysics: { + title: "Unity Fizik", + subtitle: "Unity fizik yapılandırması oluşturun — Rigidbody'ler, çarpışma birimleri, yerçekimi ve daha fazlası.", + fields: { + physicsBackend: "Fizik motoru", + simulationMode: "Simülasyon modu", + gravityPreset: "Yerçekimi ön ayarı", + prompt: "Fizik açıklaması", + includeRigidbody: "Rigidbody kurulumunu dahil et", + includeColliders: "Çarpışma birimi kurulumunu dahil et", + includeLayers: "Fizik katmanlarını dahil et", + }, + actions: { generate: "Fizik yapılandırması oluştur" }, + quickActions: "Hızlı işlemler", + }, + unityProject: { + title: "Unity Projesi", + subtitle: "Tam bir Unity proje yapısı oluşturun.", + engineSettings: "Unity motor ayarları", + fields: { + projectName: "Proje adı", + unityTemplate: "Unity şablonu", + unityVersion: "Unity sürümü", + targetPlatform: "Hedef platform", + generateDefaultScene: "Varsayılan sahne oluştur", + autoInstallPackages: "UPM paketlerini otomatik kur", + setupUrp: "URP kurulumu", + upmPackages: "UPM paketleri (virgülle ayrılmış)", + sceneName: "Sahne adı", + unityEditorPath: "Unity Editor yolu (isteğe bağlı)", + timeout: "Zaman aşımı (saniye)", + resultJson: "Sonuç (JSON)", + }, + actions: { + generate: "Proje oluştur", + finalize: "Projeyi sonlandır", + openFolder: "Çıktı klasörünü aç", + addVersion: "Unity sürümü ekle", + downloadZip: "Sonlandırılmış projeyi indir (.zip)", + }, + dialogs: { + addVersion: { + title: "Unity sürümü ekle", + versionId: "Sürüm kimliği", + label: "Etiket (isteğe bağlı)", + }, + }, + }, + theme: { light: "Açık", dark: "Koyu", system: "Sistem" }, + languages: { + en: "İngilizce", + es: "İspanyolca", + ca: "Katalanca", + eu: "Baskça", + oc: "Oksitanca", + uk: "Ukraynaca", + pt: "Portekizce", + gl: "Galiçyaca", + fr: "Fransızca", + it: "İtalyanca", + pl: "Lehçe", + zh: "Çince", + ar: "Arapça", + de: "Almanca", + hi: "Hintçe", + bn: "Bengalce", + ur: "Urduca", + id: "Endonezce", + ja: "Japonca", + vi: "Vietnamca", + ko: "Korece", + tr: "Türkçe", + }, +} as const; diff --git a/frontend/src/i18n/locales/uk.ts b/frontend/src/i18n/locales/uk.ts index c91e845d..ff6d74e9 100644 --- a/frontend/src/i18n/locales/uk.ts +++ b/frontend/src/i18n/locales/uk.ts @@ -248,5 +248,6 @@ export default { ja: "Японська", vi: "В'єтнамська", ko: "Корейська", + tr: "Турецька", }, } as const; diff --git a/frontend/src/i18n/locales/ur.ts b/frontend/src/i18n/locales/ur.ts index 96bac5dd..1bb409da 100644 --- a/frontend/src/i18n/locales/ur.ts +++ b/frontend/src/i18n/locales/ur.ts @@ -107,5 +107,6 @@ export default { pt: "پرتگالی", gl: "گالیشین", fr: "فرانسیسی", it: "اطالوی", pl: "پولش", zh: "چینی", ar: "عربی", de: "جرمن", hi: "ہندی", bn: "بنگالی", ur: "اردو", id: "انڈونیشیائی", ja: "جاپانی", vi: "ویتنامی", ko: "کوریائی", + tr: "ترکی", }, } as const; diff --git a/frontend/src/i18n/locales/vi.ts b/frontend/src/i18n/locales/vi.ts index e8a28530..0b535379 100644 --- a/frontend/src/i18n/locales/vi.ts +++ b/frontend/src/i18n/locales/vi.ts @@ -108,5 +108,6 @@ export default { fr: "Tiếng Pháp", it: "Tiếng Ý", pl: "Tiếng Ba Lan", zh: "Tiếng Trung", ar: "Tiếng Ả Rập", de: "Tiếng Đức", hi: "Tiếng Hindi", bn: "Tiếng Bengali", ur: "Tiếng Urdu", id: "Tiếng Indonesia", ja: "Tiếng Nhật", vi: "Tiếng Việt", ko: "Tiếng Hàn", + tr: "Tiếng Thổ Nhĩ Kỳ", }, } as const; diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index fdd7423e..8f3b627b 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -241,5 +241,6 @@ export default { zh: "中文", ar: "阿拉伯语", de: "德语", + tr: "土耳其语", }, } as const; diff --git a/pyrightconfig.json b/pyrightconfig.json index 238fbf51..eab8d4e8 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -20,7 +20,8 @@ "root": "backend", "extraPaths": [ "backend", - "backend/app" + "backend/app", + "../unity-mcp-server-plugin" ] } ], From 6c9fb8bd49b78a5aa1474bbeab2479103572fbb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreu=20Mart=C3=ADnez=20Vidal?= Date: Sun, 22 Mar 2026 11:49:28 +0100 Subject: [PATCH 3/7] Enhance documentation for Docker installation and usage Updated the UNITY_MCP_SERVER.md file to include detailed instructions for installing and running the Unity MCP Server using Docker from the GitHub Container Registry (GHCR). Added sections for pulling the Docker image, running the container, and mapping Docker configurations to the application. Clarified the installation process and provided examples for better user guidance. --- docs/UNITY_MCP_SERVER.md | 42 ++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/UNITY_MCP_SERVER.md b/docs/UNITY_MCP_SERVER.md index 7334784c..17060b50 100644 --- a/docs/UNITY_MCP_SERVER.md +++ b/docs/UNITY_MCP_SERVER.md @@ -9,15 +9,40 @@ For the Semantic Kernel Python bridge, see [UNITY_MCP_INTEGRATION.md](./UNITY_MC |------|--------| | **GitHub** | **[github.com/Ozymandros/Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** | | **Clone (HTTPS)** | `https://github.com/Ozymandros/Unity-MCP-Server.git` | +| **GHCR image (Docker)** | **`ghcr.io/ozymandros/unity-mcp-server`** — package page: **[github.com/Ozymandros/Unity-MCP-Server/pkgs/container/unity-mcp-server](https://github.com/Ozymandros/Unity-MCP-Server/pkgs/container/unity-mcp-server)** | | **Typical CLI name** | `unity-mcp` (global .NET tool or built executable — follow upstream README) | **Installation** is defined **upstream**: clone the repo and follow **Unity-MCP-Server** `README.md` (e.g. `dotnet tool install`, Unity package, or build steps). This Unity-Generator repo does **not** ship the server binary; it expects the executable to exist locally or on `PATH`. -### Docker (only if upstream provides it) +### Docker install from GHCR -Check **[Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** for a **`Dockerfile`**, **`docker-compose.yml`**, or a **Docker** section in `README.md`. If the project does **not** document Docker, use clone + CLI install above (or ask upstream to add a container workflow). +Official container images are published here: -When Docker *is* documented upstream, use their image name, ports, and env vars as the source of truth. Below is how that maps to **Unity-Generator** only. +**[Unity-MCP-Server — Package `unity-mcp-server` (GHCR)](https://github.com/Ozymandros/Unity-MCP-Server/pkgs/container/unity-mcp-server)** + +| Item | Value | +|------|--------| +| **Registry / image** | `ghcr.io/ozymandros/unity-mcp-server` | +| **Tags** | Use the package page for current tags (e.g. `latest`, version tags like `v3.2.1`, or `sha-` digests). Prefer a **version tag** or digest for reproducible installs. | + +**Pull** (replace `` with a tag from the [package page](https://github.com/Ozymandros/Unity-MCP-Server/pkgs/container/unity-mcp-server), e.g. `latest` or `v3.2.1`): + +```bash +docker pull ghcr.io/ozymandros/unity-mcp-server: +``` + +**Run** (example only — **port and env vars** must match **Unity-MCP-Server** docs for MCP HTTP vs stdio; adjust `-p` and any `-e` flags per upstream): + +```bash +docker run --rm -p : ghcr.io/ozymandros/unity-mcp-server: +``` + +- If the image is **private**, authenticate to GHCR before `pull` / `run`, e.g. [GitHub docs: Working with the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) (`docker login ghcr.io` with a PAT that has `read:packages`). +- For **Unity-Generator** integration over **HTTP**, publish the port the server uses for **Streamable HTTP**, then set **`UNITY_MCP_SERVER_URL`** (see table below). + +You can also build/run from a local **`Dockerfile`** / **`docker-compose.yml`** in the **[Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** repo if you prefer; the **GHCR** image is the usual binary-free install path. + +#### How GHCR Docker maps to this app | Integration path in this app | Transport | How Docker usually fits | |------------------------------|-----------|-------------------------| @@ -26,14 +51,15 @@ When Docker *is* documented upstream, use their image name, ports, and env vars **Typical Docker flow for this app (HTTP-capable server):** -1. Build/run the image **as documented in Unity-MCP-Server** (example shape only — replace with upstream values): +1. **Pull** from GHCR and **run** the container (see [package tags](https://github.com/Ozymandros/Unity-MCP-Server/pkgs/container/unity-mcp-server)): ```bash - # Example only; use the exact commands from Ozymandros/Unity-MCP-Server README - docker compose up -d - # or: docker run -p : + docker pull ghcr.io/ozymandros/unity-mcp-server: + docker run --rm -p : ghcr.io/ozymandros/unity-mcp-server: ``` + Alternatively, use **`docker compose`** or a **Dockerfile** from the **[Unity-MCP-Server](https://github.com/Ozymandros/Unity-MCP-Server)** repo if upstream documents it. + 2. Confirm the Unity Editor / MCP server can still communicate (same machine or documented networking). 3. For **finalize** MCP tools, set for the backend process: @@ -101,7 +127,7 @@ When Docker *is* documented upstream, use their image name, ports, and env vars ## Quick checklist -1. Install **Unity-MCP-Server** from **[GitHub](https://github.com/Ozymandros/Unity-MCP-Server)** (upstream README), **or** run it via **Docker** if upstream documents `Dockerfile` / compose (see [Docker (only if upstream provides it)](#docker-only-if-upstream-provides-it)). +1. Install **Unity-MCP-Server** from **[GitHub](https://github.com/Ozymandros/Unity-MCP-Server)** (upstream README), **or** run it from **[GHCR](https://github.com/Ozymandros/Unity-MCP-Server/pkgs/container/unity-mcp-server)** (`docker pull ghcr.io/ozymandros/unity-mcp-server:` — see [Docker install from GHCR](#docker-install-from-ghcr)). 2. Ensure the server is running with Unity / Editor as required by that project. 3. Set **`UNITY_MCP_COMMAND`** if `unity-mcp` is not on `PATH` (stdio / SK agent path). 4. For finalize MCP automation, set **`UNITY_USE_MCP=1`** and configure **stdio** (`UNITY_MCP_COMMAND`) or **`UNITY_MCP_SERVER_URL`** (Docker/HTTP) per `unity_mcp_client.py`. From 52cd61c7f2d6f834b3b87cca68a9e9f45f27c84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreu=20Mart=C3=ADnez=20Vidal?= Date: Mon, 23 Mar 2026 07:46:47 +0100 Subject: [PATCH 4/7] Update unity-mcp-plugin dependency in requirements.txt to use HTTPS URL --- backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 85037c60..867e6bbb 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -13,4 +13,4 @@ semantic-kernel[mcp]>=1.15.0 # unity-mcp-plugin @ git+ssh://git@github.com/Ozymandros/Unity-MCP-SK-Plugin.git # (Public HTTPS fails with 404 if the repo is private — use SSH, a fork, or a wheel.) # (B) Default dev layout: clone Unity-MCP-SK-Plugin next to this repo as "unity-mcp-server-plugin": --e ../unity-mcp-server-plugin \ No newline at end of file +unity-mcp-plugin @ git+https://github.com/Ozymandros/unity-mcp-server-plugin.git \ No newline at end of file From 54c623304c93a69b447f6dc237ebf5ee801b5215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreu=20Mart=C3=ADnez=20Vidal?= Date: Fri, 3 Apr 2026 18:40:55 +0200 Subject: [PATCH 5/7] REFACTORING: Extract component styles; add physics view-model Move scoped styles out of multiple SFCs into dedicated .css files and wire them via + diff --git a/frontend/src/components/ManagementDashboard.css b/frontend/src/components/ManagementDashboard.css new file mode 100644 index 00000000..9211e2f4 --- /dev/null +++ b/frontend/src/components/ManagementDashboard.css @@ -0,0 +1,69 @@ +.glass-drawer { + background: rgba(15, 23, 42, 0.98) !important; + backdrop-filter: blur(20px); + border-right: 1px solid rgba(255, 255, 255, 0.2) !important; +} + +.glass-card { + background: rgba(30, 41, 59, 0.8) !important; + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.2) !important; + border-radius: 20px !important; + transition: + transform 0.2s, + box-shadow 0.2s; +} + +.transparent-table :deep(table) { + background: transparent !important; +} + +.transparent-table :deep(thead th) { + font-weight: 800 !important; + color: #cbd5e1 !important; + /* Brighter gray */ + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.05em; +} + +.transparent-table :deep(tr:hover) { + background: rgba(255, 255, 255, 0.03) !important; +} + +.code-font :deep(textarea) { + font-family: "JetBrains Mono", "Fira Code", "Roboto Mono", monospace !important; + font-size: 0.85rem !important; + line-height: 1.7 !important; + color: #e2e8f0 !important; +} + +.v-list-subheader { + font-size: 0.7rem !important; + font-weight: 900 !important; + letter-spacing: 0.1em !important; + color: #cbd5e1 !important; + /* Brighter gray */ +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--v-primary-base); +} + +.h-100 { + height: 100%; +} diff --git a/frontend/src/components/ManagementDashboard.vue b/frontend/src/components/ManagementDashboard.vue index 2c2e67ba..55fa6689 100644 --- a/frontend/src/components/ManagementDashboard.vue +++ b/frontend/src/components/ManagementDashboard.vue @@ -1,58 +1,58 @@ - + diff --git a/frontend/src/components/UnityUIPanel/UnityUIPanel.css b/frontend/src/components/UnityUIPanel/UnityUIPanel.css new file mode 100644 index 00000000..16859d31 --- /dev/null +++ b/frontend/src/components/UnityUIPanel/UnityUIPanel.css @@ -0,0 +1,42 @@ +.panel { + max-width: 820px; + margin: 1rem; +} + +.header { + margin-bottom: 20px; +} + +h2 { + margin: 0 0 6px; +} + +.subtitle { + margin: 0; + color: #666; + font-size: 0.95em; +} + +.field-group { + margin-bottom: 12px; +} + +.options-row { + display: flex; + gap: 12px; + margin-bottom: 10px; +} + +.cursor-pointer { + cursor: pointer; +} + +.cursor-pointer:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.results { + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} diff --git a/frontend/src/components/UnityUIPanel/UnityUIPanel.vue b/frontend/src/components/UnityUIPanel/UnityUIPanel.vue index 422af546..d99c4921 100644 --- a/frontend/src/components/UnityUIPanel/UnityUIPanel.vue +++ b/frontend/src/components/UnityUIPanel/UnityUIPanel.vue @@ -57,17 +57,6 @@ const anchorPresetOptions = [ { value: "stretch", label: "Stretch Horizontal" }, ]; -/** - * Handle quick action chip click — injects the chip's template prompt. - * - * @param action - The quick action that was clicked. - * - * @example - * ```typescript - * handleQuickActionClick(UI_QUICK_ACTIONS[0]); - * // prompt.value is now the health bar template - * ``` - */ function handleQuickActionClick(action: UIQuickAction): void { if (!action?.prompt) return; prompt.value = action.prompt; @@ -214,47 +203,4 @@ function handleQuickActionClick(action: UIQuickAction): void { - + diff --git a/frontend/src/components/generic/ModelManagerModal/ModelManagerModal.css b/frontend/src/components/generic/ModelManagerModal/ModelManagerModal.css new file mode 100644 index 00000000..ef64bc38 --- /dev/null +++ b/frontend/src/components/generic/ModelManagerModal/ModelManagerModal.css @@ -0,0 +1,105 @@ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.modal-content { + background: #1e1e1e; + color: white; + padding: 2rem; + border-radius: 12px; + width: 500px; + max-width: 90%; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.close-btn { + background: transparent; + border: none; + font-size: 1.5rem; + color: #888; + cursor: pointer; +} + +.model-list { + margin-bottom: 2rem; +} + +ul { + list-style: none; + padding: 0; +} + +li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid #333; +} + +.add-model h4 { + margin-bottom: 1rem; +} + +.field-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +input { + background: #2d2d2d; + border: 1px solid #444; + color: white; + padding: 0.6rem; + border-radius: 4px; +} + +.danger-btn { + background: #ff4444; + color: white; + border: none; + padding: 0.3rem 0.6rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.8rem; +} + +.primary { + background: #007bff; + color: white; + border: none; + padding: 0.7rem; + border-radius: 4px; + cursor: pointer; + margin-top: 0.5rem; +} + +.primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.error { + color: #ff4444; + margin-bottom: 1rem; +} diff --git a/frontend/src/components/generic/ModelManagerModal/ModelManagerModal.vue b/frontend/src/components/generic/ModelManagerModal/ModelManagerModal.vue index 8f78a96b..51e9ea85 100644 --- a/frontend/src/components/generic/ModelManagerModal/ModelManagerModal.vue +++ b/frontend/src/components/generic/ModelManagerModal/ModelManagerModal.vue @@ -1,10 +1,6 @@ - - - - + + + + + diff --git a/frontend/src/api/client.test.ts b/frontend/src/api/client.test.ts index 0efd41c4..0dd44330 100644 --- a/frontend/src/api/client.test.ts +++ b/frontend/src/api/client.test.ts @@ -1,198 +1,215 @@ -import { describe, expect, it, beforeEach, vi } from "vitest"; - -import { - generateAudio, - generateCode, - generateImage, - generateText, - generateUnityProject, - getLatestOutput, - getPref, - healthCheck, - saveApiKeys, - setPref, - finalizeProject, - getFinalizeJobStatus, - downloadFinalizedProject, -} from "@/api/client"; -import { getDefaultBackendUrl } from "@/api/constants"; - -const mockFetch = vi.fn(); -const defaultBackendUrl = getDefaultBackendUrl(); - -function mockResponse(data: unknown, ok = true) { - return Promise.resolve({ - ok, - status: ok ? 200 : 400, - json: () => Promise.resolve(data), - }); -} - -beforeEach(() => { - mockFetch.mockReset(); - globalThis.fetch = mockFetch as unknown as typeof fetch; - vi.stubGlobal('localStorage', { - getItem: vi.fn(() => defaultBackendUrl), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn(), - key: vi.fn(), - length: 0, - }); -}); - -describe("api client", () => { - it("calls generateCode", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { content: "code" } }) - ); - await generateCode({ prompt: "p" }); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/generate/code", - expect.objectContaining({ method: "POST" }) - ); - }); - - it("calls generateText", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { content: "text" } }) - ); - await generateText({ prompt: "p" }); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/generate/text", - expect.objectContaining({ method: "POST" }) - ); - }); - - it("calls generateImage", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { image: "img" } }) - ); - await generateImage({ prompt: "p" }); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/generate/image", - expect.objectContaining({ method: "POST" }) - ); - }); - - it("calls generateAudio", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { audio: "audio" } }) - ); - await generateAudio({ prompt: "p" }); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/generate/audio", - expect.objectContaining({ method: "POST" }) - ); - }); - - it("calls saveApiKeys", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { saved: ["openai"] } }) - ); - await saveApiKeys({ openai_api_key: "x" }); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/config/keys", - expect.objectContaining({ method: "POST" }) - ); - }); - - it("calls setPref", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { key: "k" } }) - ); - await setPref("k", "v"); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/prefs", - expect.objectContaining({ method: "POST" }) - ); - }); - - it("calls getPref", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { key: "k" } }) - ); - await getPref("k"); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/prefs/k", - expect.objectContaining({ method: "GET" }) - ); - }); - - it("calls generateUnityProject", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { project_path: "p" } }) - ); - await generateUnityProject({ project_name: "p" }); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/generate/unity-project", - expect.objectContaining({ method: "POST" }) - ); - }); - - it("calls getLatestOutput", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, data: { path: "p" } }) - ); - await getLatestOutput(); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/output/latest", - expect.objectContaining({ method: "GET" }) - ); - }); - - it("calls healthCheck", async () => { - mockFetch.mockReturnValueOnce(mockResponse({ status: "ok" })); - await healthCheck(); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/health", - expect.objectContaining({ method: "GET" }) - ); - }); - - it("calls finalizeProject", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ success: true, job_id: "abc123", message: "created" }) - ); - const result = await finalizeProject({ - project_name: "TestProject", - unity_settings: { generate_scene: true, scene_name: "MainScene" }, - }); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/api/v1/project/finalize", - expect.objectContaining({ method: "POST" }) - ); - expect(result.job_id).toBe("abc123"); - }); - - it("calls getFinalizeJobStatus", async () => { - mockFetch.mockReturnValueOnce( - mockResponse({ - job_id: "abc123", - status: "running", - step: "unity_run", - progress: 50, - logs_tail: ["log line"], - errors: [], - started_at: null, - finished_at: null, - project_path: null, - zip_path: null, - }) - ); - const result = await getFinalizeJobStatus("abc123"); - expect(mockFetch).toHaveBeenCalledWith( - defaultBackendUrl + "/api/v1/project/finalize/abc123", - expect.objectContaining({ method: "GET" }) - ); - expect(result.status).toBe("running"); - expect(result.progress).toBe(50); - }); - - it("returns download URL for downloadFinalizedProject", () => { - const url = downloadFinalizedProject("abc123"); - expect(url).toBe( - defaultBackendUrl + "/api/v1/project/finalize/abc123/download" - ); - }); -}); +import { describe, expect, it, beforeEach, vi } from "vitest"; + +import { + generateAudio, + generateCode, + generateImage, + generateText, + generateUnityProject, + getLatestOutput, + getPref, + getUnityMcpStatus, + healthCheck, + saveApiKeys, + setPref, + finalizeProject, + getFinalizeJobStatus, + downloadFinalizedProject, +} from "@/api/client"; +import { getDefaultBackendUrl } from "@/api/constants"; + +const mockFetch = vi.fn(); +const defaultBackendUrl = getDefaultBackendUrl(); + +function mockResponse(data: unknown, ok = true) { + return Promise.resolve({ + ok, + status: ok ? 200 : 400, + json: () => Promise.resolve(data), + }); +} + +beforeEach(() => { + mockFetch.mockReset(); + globalThis.fetch = mockFetch as unknown as typeof fetch; + vi.stubGlobal('localStorage', { + getItem: vi.fn(() => defaultBackendUrl), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + key: vi.fn(), + length: 0, + }); +}); + +describe("api client", () => { + it("calls generateCode", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { content: "code" } }) + ); + await generateCode({ prompt: "p" }); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/generate/code", + expect.objectContaining({ method: "POST" }) + ); + }); + + it("calls generateText", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { content: "text" } }) + ); + await generateText({ prompt: "p" }); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/generate/text", + expect.objectContaining({ method: "POST" }) + ); + }); + + it("calls generateImage", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { image: "img" } }) + ); + await generateImage({ prompt: "p" }); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/generate/image", + expect.objectContaining({ method: "POST" }) + ); + }); + + it("calls generateAudio", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { audio: "audio" } }) + ); + await generateAudio({ prompt: "p" }); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/generate/audio", + expect.objectContaining({ method: "POST" }) + ); + }); + + it("calls saveApiKeys", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { saved: ["openai"] } }) + ); + await saveApiKeys({ openai_api_key: "x" }); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/config/keys", + expect.objectContaining({ method: "POST" }) + ); + }); + + it("calls setPref", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { key: "k" } }) + ); + await setPref("k", "v"); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/prefs", + expect.objectContaining({ method: "POST" }) + ); + }); + + it("calls getPref", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { key: "k" } }) + ); + await getPref("k"); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/prefs/k", + expect.objectContaining({ method: "GET" }) + ); + }); + + it("calls generateUnityProject", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { project_path: "p" } }) + ); + await generateUnityProject({ project_name: "p" }); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/generate/unity-project", + expect.objectContaining({ method: "POST" }) + ); + }); + + it("calls getLatestOutput", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, data: { path: "p" } }) + ); + await getLatestOutput(); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/output/latest", + expect.objectContaining({ method: "GET" }) + ); + }); + + it("calls getUnityMcpStatus", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ + success: true, + data: { available: true, editor_available: false, backend: "file" }, + }) + ); + const result = await getUnityMcpStatus(); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/api/unity-mcp/status", + expect.objectContaining({ method: "GET" }) + ); + expect(result.data?.available).toBe(true); + expect(result.data?.editor_available).toBe(false); + }); + + it("calls healthCheck", async () => { + mockFetch.mockReturnValueOnce(mockResponse({ status: "ok" })); + await healthCheck(); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/health", + expect.objectContaining({ method: "GET" }) + ); + }); + + it("calls finalizeProject", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ success: true, job_id: "abc123", message: "created" }) + ); + const result = await finalizeProject({ + project_name: "TestProject", + unity_settings: { generate_scene: true, scene_name: "MainScene" }, + }); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/api/v1/project/finalize", + expect.objectContaining({ method: "POST" }) + ); + expect(result.job_id).toBe("abc123"); + }); + + it("calls getFinalizeJobStatus", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ + job_id: "abc123", + status: "running", + step: "unity_run", + progress: 50, + logs_tail: ["log line"], + errors: [], + started_at: null, + finished_at: null, + project_path: null, + zip_path: null, + }) + ); + const result = await getFinalizeJobStatus("abc123"); + expect(mockFetch).toHaveBeenCalledWith( + defaultBackendUrl + "/api/v1/project/finalize/abc123", + expect.objectContaining({ method: "GET" }) + ); + expect(result.status).toBe("running"); + expect(result.progress).toBe(50); + }); + + it("returns download URL for downloadFinalizedProject", () => { + const url = downloadFinalizedProject("abc123"); + expect(url).toBe( + defaultBackendUrl + "/api/v1/project/finalize/abc123/download" + ); + }); +}); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 2dd37fec..ec600038 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,852 +1,963 @@ -import { getDefaultBackendUrl } from "@/api/constants"; - -export type GenerationResponse = { - success: boolean; - date: string; - error: string | null; - data: Record | null; -}; - -export type DiscoveryResponse = { - success: boolean; - date: string; - error: string | null; - data: Record | null; - providers?: ProviderCapabilities[]; - models?: Record; - keys?: Record; - preferences?: Record; - prompts?: Record; - settings?: Record; -}; - -export type CreateSceneRequest = { - prompt: string; - provider?: string; - options?: Record; - api_key?: string; - system_prompt?: string; - project_name?: string; // backend always uses base_path + project_name - project_path?: string; // ignored by backend -}; - -export function createScene(body: CreateSceneRequest) { - return post("/api/scenes/create", body); -} - -export type GenerationRequest = { - prompt: string; - modality?: string; - provider?: string; - api_key?: string; - options?: Record; - system_prompt?: string; - project_name?: string; // backend always uses base_path + project_name - project_path?: string; // ignored by backend -}; - -function getBackendUrl(): string { - return localStorage.getItem("backendUrl") || getDefaultBackendUrl(); -} - -/** - * Creates a fetch error with context about the failed request. - * - * @param url - The URL that was being requested. - * @param method - The HTTP method used. - * @param cause - The original error that occurred. - * @returns An Error object with detailed context. - */ -function createFetchError(url: string, method: string, cause: unknown): Error { - let message = `Failed to connect to backend at ${url}`; - - if (cause instanceof Error) { - if (cause.name === "TypeError" && cause.message.includes("fetch")) { - message = "Backend server is not running. Please start the Python backend."; - } else { - message = `${cause.message} (Request: ${method} ${url})`; - } - } - - const error = new Error(message); - error.name = "BackendError"; - // Store cause as a custom property for compatibility - (error as Error & { cause?: unknown }).cause = cause; - return error; -} - -/** - * Makes an HTTP POST request to the backend API. - * - * @param path - The API endpoint path (e.g., "/generate/code"). - * @param body - The request body to serialize as JSON. - * @returns The parsed JSON response. - * @throws {BackendError} If the request fails or backend is unavailable. - * - * @example - * ```typescript - * try { - * const response = await post("/generate/code", { prompt: "Hello" }); - * console.log(response.data); - * } catch (error) { - * console.error("Backend error:", error.message); - * } - * ``` - */ -async function post( - path: string, - body: GenerationRequest | SpritesRequest | { keys: Record } | { key: string, value: string } -): Promise { - const url = `${getBackendUrl()}${path}`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as T; - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export function generateCode(body: GenerationRequest) { - return post("/generate/code", body); -} - -export function generateText(body: GenerationRequest) { - return post("/generate/text", body); -} - -export function generateImage(body: GenerationRequest) { - return post("/generate/image", body); -} - -export function generateAudio(body: GenerationRequest) { - return post("/generate/audio", body); -} - -export type UnityUIRequest = { - prompt: string; - provider?: string; - ui_system?: string; - element_type?: string; - output_format?: string; - anchor_preset?: string; - color_theme?: string; - include_animations?: boolean; - api_key?: string; - system_prompt?: string; - project_name?: string; - options?: Record; -}; - -/** - * Generate Unity UI prefab assets (health bars, buttons, dialogue boxes, etc.). - * - * @param body - Request body with prompt and UI generation options. - * @returns Promise resolving to a GenerationResponse with generated content. - * @throws {BackendError} If the request fails or backend is unavailable. - * - * @example - * ```typescript - * const response = await generateUnityUI({ - * prompt: "Create a health bar with smooth fill animation", - * ui_system: "ugui", - * element_type: "health_bar", - * include_animations: true, - * }); - * ``` - */ -export function generateUnityUI(body: UnityUIRequest) { - return post("/generate/unity-ui", body as unknown as GenerationRequest); -} - -export type SpritesRequest = { - prompt: string; - provider?: string; - api_key?: string; - resolution: number; - options?: Record; - system_prompt?: string; - project_name?: string; // backend always uses base_path + project_name - project_path?: string; // ignored by backend -}; - -export function generateSprites(body: SpritesRequest) { - return post("/generate/sprites", body); -} - -export async function saveApiKeys(keys: Record): Promise { - const url = `${getBackendUrl()}/config/keys`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ keys }), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as GenerationResponse; - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export async function getApiKeys(): Promise { - const url = `${getBackendUrl()}/config/keys`; - - try { - const response = await fetch(url, { - method: "GET", - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as GenerationResponse; - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -/** - * Saves a user preference to the backend. - * - * @param key - The preference key. - * @param value - The preference value. - * @returns The backend response. - * @throws {BackendError} If the request fails or backend is unavailable. - * - * @example - * ```typescript - * try { - * await setPref("theme", "dark"); - * } catch (error) { - * console.error("Failed to save preference:", error.message); - * } - * ``` - */ -export async function setPref(key: string, value: string): Promise { - const url = `${getBackendUrl()}/prefs`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key, value }), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - const body = (await response.json()) as GenerationResponse & { detail?: string }; - return body as GenerationResponse; - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -/** - * Retrieves a user preference from the backend. - * - * @param key - The preference key to retrieve. - * @returns The backend response containing the preference value. - * @throws {BackendError} If the request fails or backend is unavailable. - * - * @example - * ```typescript - * try { - * const response = await getPref("theme"); - * console.log(response.data); - * } catch (error) { - * console.error("Failed to get preference:", error.message); - * } - * ``` - */ -export async function getPref(key: string): Promise { - const url = `${getBackendUrl()}/prefs/${key}`; - - try { - const response = await fetch(url, { - method: "GET", - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as GenerationResponse; - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -// --------------------------------------------------------------------------- -// Provider model management -// --------------------------------------------------------------------------- - -// --------------------------------------------------------------------------- -// Management API -// --------------------------------------------------------------------------- - -export type ModelEntry = { - value: string; - label: string; - modality: string; -}; - -export type ProviderCapabilities = { - name: string; - api_key_name: string | null; - base_url: string | null; - openai_compatible: boolean; - requires_api_key: boolean; - supports_vision: boolean; - supports_streaming: boolean; - supports_function_calling: boolean; - supports_tool_use: boolean; - modalities: string[]; - default_models: Record; - extra: Record; -}; - -export async function listProviders(): Promise { - const url = `${getBackendUrl()}/api/management/providers`; - - try { - const response = await fetch(url); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -export async function saveProvider(provider: ProviderCapabilities & { api_key_value?: string }): Promise { - const url = `${getBackendUrl()}/api/management/providers`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(provider), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export async function deleteProvider(name: string): Promise { - const url = `${getBackendUrl()}/api/management/providers/${name}`; - - try { - const response = await fetch(url, { - method: "DELETE", - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "DELETE", error); - } -} - -export async function listModels(provider: string): Promise { - const url = `${getBackendUrl()}/api/management/models/${provider}`; - - try { - const response = await fetch(url); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -export async function addModel(provider: string, value: string, label: string, modality: string = "llm"): Promise { - const url = `${getBackendUrl()}/api/management/models`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, value, label, modality }), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export async function removeModel(provider: string, value: string): Promise { - const url = `${getBackendUrl()}/api/management/models/${provider}/${encodeURIComponent(value)}`; - - try { - const response = await fetch(url, { - method: "DELETE", - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "DELETE", error); - } -} - -export async function listApiKeys(): Promise> { - const url = `${getBackendUrl()}/api/management/keys`; - - try { - const response = await fetch(url); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -export async function saveApiKey(service_name: string, key_value: string): Promise { - const url = `${getBackendUrl()}/api/management/keys`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ service_name, key_value }), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export async function deleteApiKey(service_name: string): Promise { - const url = `${getBackendUrl()}/api/management/keys/${service_name}`; - - try { - const response = await fetch(url, { - method: "DELETE", - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "DELETE", error); - } -} - -export async function listSystemPrompts(): Promise> { - const url = `${getBackendUrl()}/api/management/prompts`; - - try { - const response = await fetch(url); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -export async function saveSystemPrompt(modality: string, content: string): Promise { - const url = `${getBackendUrl()}/api/management/prompts`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ modality, content }), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export async function resetSystemPrompts(): Promise> { - const url = `${getBackendUrl()}/api/management/system-prompts/reset`; - try { - const response = await fetch(url, { method: "POST" }); - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - throw new Error(errorBody.detail || errorBody.error || `HTTP ${response.status}`); - } - return (await response.json()).data as Record; - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export async function getAllConfig(): Promise { - const url = `${getBackendUrl()}/api/management/all`; - - try { - const response = await fetch(url); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return await response.json(); - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -export async function getApiKey(serviceName: string): Promise { - const keys = await listApiKeys(); - return keys[serviceName] || null; -} - -export type UnityProjectRequest = { - project_name: string; - code_prompt?: string; - text_prompt?: string; - image_prompt?: string; - audio_prompt?: string; - provider_overrides?: Record; - options?: Record>; - unity_template?: string; - unity_version?: string; - unity_platform?: string; -}; - -export async function generateUnityProject(body: UnityProjectRequest): Promise { - const url = `${getBackendUrl()}/generate/unity-project`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as GenerationResponse; - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export type UnityPhysicsRequest = { - prompt: string; - provider?: string; - physics_backend?: string; - simulation_mode?: string; - gravity_preset?: string; - include_rigidbody?: boolean; - include_colliders?: boolean; - include_layers?: boolean; - api_key?: string; - system_prompt?: string; - project_name?: string; - options?: Record; -}; - -/** - * Generate Unity Physics configuration code (PhysX or DOTS). - * - * @param body - Request body with prompt and physics generation options. - * @returns Promise resolving to a GenerationResponse with generated content. - * @throws {BackendError} If the request fails or backend is unavailable. - * - * @example - * ```typescript - * const response = await generateUnityPhysics({ - * prompt: "Set up a bouncy ball with Earth gravity", - * physics_backend: "physx", - * include_rigidbody: true, - * }); - * ``` - */ -export function generateUnityPhysics(body: UnityPhysicsRequest) { - return post("/generate/unity-physics", body as unknown as GenerationRequest); -} - -export async function getLatestOutput(): Promise { - const url = `${getBackendUrl()}/output/latest`; - - try { - const response = await fetch(url, { - method: "GET", - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as GenerationResponse; - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -// --------------------------------------------------------------------------- -// Unity versions (dropdown: label + id; user can add more) -// --------------------------------------------------------------------------- - -export type UnityVersionOption = { value: string; label: string }; - -export async function getUnityVersions(): Promise { - const url = `${getBackendUrl()}/unity-versions`; - - try { - const response = await fetch(url, { - method: "GET", - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as GenerationResponse; - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -export async function addUnityVersion(body: { - value: string; - label?: string; -}): Promise { - const url = `${getBackendUrl()}/unity-versions`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - value: body.value, - label: body.label ?? body.value, - }), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as GenerationResponse; - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export async function healthCheck(): Promise<{ status: string }> { - const url = `${getBackendUrl()}/health`; - - try { - const response = await fetch(url, { - method: "GET", - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as { status: string }; - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -// --------------------------------------------------------------------------- -// Finalize workflow types -// --------------------------------------------------------------------------- - -export type UnityEngineSettings = { - install_packages?: boolean; - generate_scene?: boolean; - setup_urp?: boolean; - packages?: string[]; - scene_name?: string; - unity_editor_path?: string; - timeout?: number; -}; - -export type FinalizeProjectRequest = { - project_name?: string; - project_path?: string; - code_prompt?: string; - text_prompt?: string; - image_prompt?: string; - audio_prompt?: string; - provider_overrides?: Record; - options?: Record>; - unity_settings?: UnityEngineSettings; -}; - -export type FinalizeProjectResponse = { - success: boolean; - job_id: string; - message: string; -}; - -export type FinalizeJobStatusResponse = { - job_id: string; - status: "pending" | "running" | "completed" | "failed" | "not_found"; - step: string; - progress: number; - logs_tail: string[]; - errors: string[]; - started_at: string | null; - finished_at: string | null; - project_path: string | null; - zip_path: string | null; -}; - -// --------------------------------------------------------------------------- -// Finalize workflow API calls -// --------------------------------------------------------------------------- - -export async function finalizeProject( - body: FinalizeProjectRequest -): Promise { - const url = `${getBackendUrl()}/api/v1/project/finalize`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as FinalizeProjectResponse; - } catch (error) { - throw createFetchError(url, "POST", error); - } -} - -export async function getFinalizeJobStatus( - jobId: string -): Promise { - const url = `${getBackendUrl()}/api/v1/project/finalize/${jobId}`; - - try { - const response = await fetch(url, { method: "GET" }); - - if (!response.ok) { - const errorBody = await response.json().catch(() => ({})); - const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; - throw new Error(errorMessage); - } - - return (await response.json()) as FinalizeJobStatusResponse; - } catch (error) { - throw createFetchError(url, "GET", error); - } -} - -export function downloadFinalizedProject(jobId: string): string { - return `${getBackendUrl()}/api/v1/project/finalize/${jobId}/download`; -} +import { getDefaultBackendUrl } from "@/api/constants"; + +export type GenerationResponse = { + success: boolean; + date: string; + error: string | null; + data: Record | null; +}; + +export type DiscoveryResponse = { + success: boolean; + date: string; + error: string | null; + data: Record | null; + providers?: ProviderCapabilities[]; + models?: Record; + keys?: Record; + preferences?: Record; + prompts?: Record; + settings?: Record; + unity_tool_names?: string[]; +}; + +export type CreateSceneRequest = { + prompt: string; + provider?: string; + options?: Record; + api_key?: string; + system_prompt?: string; + project_name?: string; // backend always uses base_path + project_name + project_path?: string; // ignored by backend + media_import?: Record; + apply_via_mcp?: boolean; +}; + +export function createScene(body: CreateSceneRequest) { + return post("/api/scenes/create", body); +} + +export type UnityMcpStatus = { + available: boolean; + editor_available: boolean; + backend?: string; + version?: string; + editor_gated_warning?: string | null; + error?: string; +}; + +/** + * Fetch Unity MCP availability and Editor-gated warnings. + * + * @returns Promise resolving to a GenerationResponse whose data is UnityMcpStatus. + * @throws {BackendError} If the request fails or backend is unavailable. + * + * @example + * ```typescript + * const response = await getUnityMcpStatus(); + * console.log(response.data?.available); + * ``` + */ +export async function getUnityMcpStatus(): Promise { + const url = `${getBackendUrl()}/api/unity-mcp/status`; + try { + const response = await fetch(url, { method: "GET" }); + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export type GenerationRequest = { + prompt: string; + modality?: string; + provider?: string; + api_key?: string; + options?: Record; + system_prompt?: string; + project_name?: string; // backend always uses base_path + project_name + project_path?: string; // ignored by backend +}; + +function getBackendUrl(): string { + return localStorage.getItem("backendUrl") || getDefaultBackendUrl(); +} + +/** + * Creates a fetch error with context about the failed request. + * + * @param url - The URL that was being requested. + * @param method - The HTTP method used. + * @param cause - The original error that occurred. + * @returns An Error object with detailed context. + */ +function createFetchError(url: string, method: string, cause: unknown): Error { + let message = `Failed to connect to backend at ${url}`; + + if (cause instanceof Error) { + if (cause.name === "TypeError" && cause.message.includes("fetch")) { + message = "Backend server is not running. Please start the Python backend."; + } else { + message = `${cause.message} (Request: ${method} ${url})`; + } + } + + const error = new Error(message); + error.name = "BackendError"; + // Store cause as a custom property for compatibility + (error as Error & { cause?: unknown }).cause = cause; + return error; +} + +/** + * Makes an HTTP POST request to the backend API. + * + * @param path - The API endpoint path (e.g., "/generate/code"). + * @param body - The request body to serialize as JSON. + * @returns The parsed JSON response. + * @throws {BackendError} If the request fails or backend is unavailable. + * + * @example + * ```typescript + * try { + * const response = await post("/generate/code", { prompt: "Hello" }); + * console.log(response.data); + * } catch (error) { + * console.error("Backend error:", error.message); + * } + * ``` + */ +async function post( + path: string, + body: GenerationRequest | SpritesRequest | { keys: Record } | { key: string, value: string } +): Promise { + const url = `${getBackendUrl()}${path}`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as T; + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export function generateCode(body: GenerationRequest) { + return post("/generate/code", body); +} + +export function generateText(body: GenerationRequest) { + return post("/generate/text", body); +} + +export function generateImage(body: GenerationRequest) { + return post("/generate/image", body); +} + +export function generateAudio(body: GenerationRequest) { + return post("/generate/audio", body); +} + +export type UnityUIRequest = { + prompt: string; + provider?: string; + ui_system?: string; + element_type?: string; + output_format?: string; + anchor_preset?: string; + color_theme?: string; + include_animations?: boolean; + apply_via_mcp?: boolean; + api_key?: string; + system_prompt?: string; + project_name?: string; + options?: Record; +}; + +/** + * Generate Unity UI prefab assets (health bars, buttons, dialogue boxes, etc.). + * + * @param body - Request body with prompt and UI generation options. + * @returns Promise resolving to a GenerationResponse with generated content. + * @throws {BackendError} If the request fails or backend is unavailable. + * + * @example + * ```typescript + * const response = await generateUnityUI({ + * prompt: "Create a health bar with smooth fill animation", + * ui_system: "ugui", + * element_type: "health_bar", + * include_animations: true, + * }); + * ``` + */ +export function generateUnityUI(body: UnityUIRequest) { + return post("/generate/unity-ui", body as unknown as GenerationRequest); +} + +export type SpritesRequest = { + prompt: string; + provider?: string; + api_key?: string; + resolution: number; + options?: Record; + system_prompt?: string; + project_name?: string; // backend always uses base_path + project_name + project_path?: string; // ignored by backend +}; + +export function generateSprites(body: SpritesRequest) { + return post("/generate/sprites", body); +} + +export async function saveApiKeys(keys: Record): Promise { + const url = `${getBackendUrl()}/config/keys`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ keys }), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export async function getApiKeys(): Promise { + const url = `${getBackendUrl()}/config/keys`; + + try { + const response = await fetch(url, { + method: "GET", + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +/** + * Saves a user preference to the backend. + * + * @param key - The preference key. + * @param value - The preference value. + * @returns The backend response. + * @throws {BackendError} If the request fails or backend is unavailable. + * + * @example + * ```typescript + * try { + * await setPref("theme", "dark"); + * } catch (error) { + * console.error("Failed to save preference:", error.message); + * } + * ``` + */ +export async function setPref(key: string, value: string): Promise { + const url = `${getBackendUrl()}/prefs`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key, value }), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + const body = (await response.json()) as GenerationResponse & { detail?: string }; + return body as GenerationResponse; + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +/** + * Retrieves a user preference from the backend. + * + * @param key - The preference key to retrieve. + * @returns The backend response containing the preference value. + * @throws {BackendError} If the request fails or backend is unavailable. + * + * @example + * ```typescript + * try { + * const response = await getPref("theme"); + * console.log(response.data); + * } catch (error) { + * console.error("Failed to get preference:", error.message); + * } + * ``` + */ +export async function getPref(key: string): Promise { + const url = `${getBackendUrl()}/prefs/${key}`; + + try { + const response = await fetch(url, { + method: "GET", + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +// --------------------------------------------------------------------------- +// Provider model management +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Management API +// --------------------------------------------------------------------------- + +export type ModelEntry = { + value: string; + label: string; + modality: string; +}; + +export type ProviderCapabilities = { + name: string; + api_key_name: string | null; + base_url: string | null; + openai_compatible: boolean; + requires_api_key: boolean; + supports_vision: boolean; + supports_streaming: boolean; + supports_function_calling: boolean; + supports_tool_use: boolean; + modalities: string[]; + default_models: Record; + extra: Record; +}; + +export async function listProviders(): Promise { + const url = `${getBackendUrl()}/api/management/providers`; + + try { + const response = await fetch(url); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export async function saveProvider(provider: ProviderCapabilities & { api_key_value?: string }): Promise { + const url = `${getBackendUrl()}/api/management/providers`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(provider), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export async function deleteProvider(name: string): Promise { + const url = `${getBackendUrl()}/api/management/providers/${name}`; + + try { + const response = await fetch(url, { + method: "DELETE", + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "DELETE", error); + } +} + +export async function listModels(provider: string): Promise { + const url = `${getBackendUrl()}/api/management/models/${provider}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export async function addModel(provider: string, value: string, label: string, modality: string = "llm"): Promise { + const url = `${getBackendUrl()}/api/management/models`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider, value, label, modality }), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export async function removeModel(provider: string, value: string): Promise { + const url = `${getBackendUrl()}/api/management/models/${provider}/${encodeURIComponent(value)}`; + + try { + const response = await fetch(url, { + method: "DELETE", + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "DELETE", error); + } +} + +export async function listApiKeys(): Promise> { + const url = `${getBackendUrl()}/api/management/keys`; + + try { + const response = await fetch(url); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export async function saveApiKey(service_name: string, key_value: string): Promise { + const url = `${getBackendUrl()}/api/management/keys`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ service_name, key_value }), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export async function deleteApiKey(service_name: string): Promise { + const url = `${getBackendUrl()}/api/management/keys/${service_name}`; + + try { + const response = await fetch(url, { + method: "DELETE", + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "DELETE", error); + } +} + +export async function listSystemPrompts(): Promise> { + const url = `${getBackendUrl()}/api/management/prompts`; + + try { + const response = await fetch(url); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export async function saveSystemPrompt(modality: string, content: string): Promise { + const url = `${getBackendUrl()}/api/management/prompts`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ modality, content }), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export async function resetSystemPrompts(): Promise> { + const url = `${getBackendUrl()}/api/management/system-prompts/reset`; + try { + const response = await fetch(url, { method: "POST" }); + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + throw new Error(errorBody.detail || errorBody.error || `HTTP ${response.status}`); + } + return (await response.json()).data as Record; + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export async function getAllConfig(): Promise { + const url = `${getBackendUrl()}/api/management/all`; + + try { + const response = await fetch(url); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export async function getApiKey(serviceName: string): Promise { + const keys = await listApiKeys(); + return keys[serviceName] || null; +} + +export type UnityProjectRequest = { + project_name: string; + code_prompt?: string; + text_prompt?: string; + image_prompt?: string; + audio_prompt?: string; + provider_overrides?: Record; + options?: Record>; + unity_template?: string; + unity_version?: string; + unity_platform?: string; +}; + +export async function generateUnityProject(body: UnityProjectRequest): Promise { + const url = `${getBackendUrl()}/generate/unity-project`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export type UnityPhysicsRequest = { + prompt: string; + provider?: string; + physics_backend?: string; + simulation_mode?: string; + gravity_preset?: string; + include_rigidbody?: boolean; + include_colliders?: boolean; + include_layers?: boolean; + apply_via_mcp?: boolean; + api_key?: string; + system_prompt?: string; + project_name?: string; + options?: Record; +}; + +/** + * Generate Unity Physics configuration code (PhysX or DOTS). + * + * @param body - Request body with prompt and physics generation options. + * @returns Promise resolving to a GenerationResponse with generated content. + * @throws {BackendError} If the request fails or backend is unavailable. + * + * @example + * ```typescript + * const response = await generateUnityPhysics({ + * prompt: "Set up a bouncy ball with Earth gravity", + * physics_backend: "physx", + * include_rigidbody: true, + * }); + * ``` + */ +export function generateUnityPhysics(body: UnityPhysicsRequest) { + return post("/generate/unity-physics", body as unknown as GenerationRequest); +} + +export type UnityModalityRequest = { + prompt: string; + provider?: string; + api_key?: string; + system_prompt?: string; + project_name?: string; + apply_via_mcp?: boolean; + options?: Record; +}; + +export type WizardRequest = { + prompt: string; + provider?: string; + api_key?: string; + system_prompt?: string; + project_name?: string; + options?: Record; +}; + +export type PromptAssistRequest = { + prompt: string; + mode?: "enhance" | "sample"; + provider?: string; + api_key?: string; + options?: Record; +}; + +export function generateWizard(body: WizardRequest) { + return post("/generate/wizard", body as unknown as GenerationRequest); +} + +export function generateUnityLighting(body: UnityModalityRequest) { + return post("/generate/unity-lighting", body as unknown as GenerationRequest); +} + +export function generateUnityCamera(body: UnityModalityRequest) { + return post("/generate/unity-camera", body as unknown as GenerationRequest); +} + +export function generateUnityMaterials(body: UnityModalityRequest) { + return post("/generate/unity-materials", body as unknown as GenerationRequest); +} + +export function generateUnityAnimation(body: UnityModalityRequest) { + return post("/generate/unity-animation", body as unknown as GenerationRequest); +} + +export function generateVideo(body: GenerationRequest) { + return post("/generate/video", body); +} + +export function promptAssist(body: PromptAssistRequest) { + return post("/api/prompt-assist", body as unknown as GenerationRequest); +} + +export async function getLogTail(lines = 200): Promise { + const url = `${getBackendUrl()}/api/logs/tail?lines=${lines}`; + try { + const response = await fetch(url); + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + throw new Error(errorBody.detail || errorBody.error || `HTTP ${response.status}`); + } + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export async function getLatestOutput(): Promise { + const url = `${getBackendUrl()}/output/latest`; + + try { + const response = await fetch(url, { + method: "GET", + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +// --------------------------------------------------------------------------- +// Unity versions (dropdown: label + id; user can add more) +// --------------------------------------------------------------------------- + +export type UnityVersionOption = { value: string; label: string }; + +export async function getUnityVersions(): Promise { + const url = `${getBackendUrl()}/unity-versions`; + + try { + const response = await fetch(url, { + method: "GET", + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export async function addUnityVersion(body: { + value: string; + label?: string; +}): Promise { + const url = `${getBackendUrl()}/unity-versions`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + value: body.value, + label: body.label ?? body.value, + }), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as GenerationResponse; + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export async function healthCheck(): Promise<{ status: string }> { + const url = `${getBackendUrl()}/health`; + + try { + const response = await fetch(url, { + method: "GET", + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as { status: string }; + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +// --------------------------------------------------------------------------- +// Finalize workflow types +// --------------------------------------------------------------------------- + +export type UnityEngineSettings = { + install_packages?: boolean; + generate_scene?: boolean; + setup_urp?: boolean; + packages?: string[]; + scene_name?: string; + unity_editor_path?: string; + timeout?: number; + unity_automation_mode?: "batch" | "mcp" | "auto"; +}; + +export type FinalizeProjectRequest = { + project_name?: string; + project_path?: string; + code_prompt?: string; + text_prompt?: string; + image_prompt?: string; + audio_prompt?: string; + provider_overrides?: Record; + options?: Record>; + unity_settings?: UnityEngineSettings; +}; + +export type FinalizeProjectResponse = { + success: boolean; + job_id: string; + message: string; +}; + +export type FinalizeJobStatusResponse = { + job_id: string; + status: "pending" | "running" | "completed" | "failed" | "not_found"; + step: string; + progress: number; + logs_tail: string[]; + errors: string[]; + started_at: string | null; + finished_at: string | null; + project_path: string | null; + zip_path: string | null; +}; + +// --------------------------------------------------------------------------- +// Finalize workflow API calls +// --------------------------------------------------------------------------- + +export async function finalizeProject( + body: FinalizeProjectRequest +): Promise { + const url = `${getBackendUrl()}/api/v1/project/finalize`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as FinalizeProjectResponse; + } catch (error) { + throw createFetchError(url, "POST", error); + } +} + +export async function getFinalizeJobStatus( + jobId: string +): Promise { + const url = `${getBackendUrl()}/api/v1/project/finalize/${jobId}`; + + try { + const response = await fetch(url, { method: "GET" }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + const errorMessage = errorBody.detail || errorBody.error || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return (await response.json()) as FinalizeJobStatusResponse; + } catch (error) { + throw createFetchError(url, "GET", error); + } +} + +export function downloadFinalizedProject(jobId: string): string { + return `${getBackendUrl()}/api/v1/project/finalize/${jobId}/download`; +} diff --git a/frontend/src/components/LogsPanel/LogsPanel.vue b/frontend/src/components/LogsPanel/LogsPanel.vue new file mode 100644 index 00000000..451ac1fd --- /dev/null +++ b/frontend/src/components/LogsPanel/LogsPanel.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/components/LogsPanel/index.ts b/frontend/src/components/LogsPanel/index.ts new file mode 100644 index 00000000..99773a9f --- /dev/null +++ b/frontend/src/components/LogsPanel/index.ts @@ -0,0 +1 @@ +export { default as LogsPanel } from "./LogsPanel.vue"; diff --git a/frontend/src/components/ScenesPanel.ts b/frontend/src/components/ScenesPanel.ts index 73b943d1..b807e3ea 100644 --- a/frontend/src/components/ScenesPanel.ts +++ b/frontend/src/components/ScenesPanel.ts @@ -1,182 +1,190 @@ -import { computed, ref, watch, onMounted } from "vue"; -import { createScene } from "@/api/client"; -import { DEFAULT_PROJECT_NAME } from "@/api/constants"; -import { TEMPERATURE_PRESETS } from "@/constants/providers"; -import { useIntelligenceStore } from "@/store/intelligenceStore"; -import { useSessionProject } from "@/composables/useSessionProject"; -import { useMediaImport } from "@/composables/useMediaImport"; - -export function useScenesPanel() { - const store = useIntelligenceStore(); - const { projectName } = useSessionProject(); - const { pendingMediaImport, pendingPrompt, clearPendingMediaImport, hasPendingMediaImport } = useMediaImport(); - - const prompt = ref(""); - const provider = ref(""); - const model = ref(""); - const temperature = ref(0.7); - const apiKey = ref(""); - const systemPrompt = ref(""); - const defaultSystemPrompt = ref("Default: You are a senior Unity engineer..."); - - const status = ref(null); - const tone = ref<"ok" | "error">("ok"); - - interface AgentResult { - content: string; - files: string[]; - metadata?: { - steps?: string[]; - }; - } - - interface MediaImportPayload { - data: string; - name: string; - type: string; - texture_type?: string; - audio_format?: string; - } - - interface SceneRequestPayload { - prompt: string; - system_prompt?: string; - provider?: string; - api_key?: string; - project_name: string; - options: { - model?: string; - temperature: number; - }; - media_import?: MediaImportPayload; - } - - const result = ref(null); - const loading = ref(false); - - // Discovery data from store - const providers = computed(() => store.getProvidersByModality("llm")); - const providerOptions = computed(() => - providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) - ); - const availableModels = computed(() => store.getModelsByProvider(provider.value, "llm")); - const showModelManager = ref(false); - - onMounted(async () => { - try { - await store.load(); - - const preferred = store.getPreferredEngine("llm"); - if (!provider.value) provider.value = preferred.provider; - if (!model.value) model.value = preferred.model; - - const dbSysPrompt = store.getPreference("default_code_system_prompt"); - if (dbSysPrompt) { - defaultSystemPrompt.value = `Default: ${dbSysPrompt.substring(0, 50)}...`; - } - } catch (e) { - console.error("Failed to load ScenesPanel state via store", e); - } - - // Check for pending media import from ImagePanel or AudioPanel - if (hasPendingMediaImport() && pendingPrompt.value) { - prompt.value = pendingPrompt.value; - status.value = `Ready to import ${pendingMediaImport.value?.type || 'media'} to Unity`; - tone.value = "ok"; - } - }); - - watch(provider, () => { - // Reset model if not in new provider's list - if (provider.value && !availableModels.value.some((m: { value: string }) => m.value === model.value)) { - model.value = ""; - } - }); - - async function refreshModels() { - await store.load(); - } - - - const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); - - async function run() { - if (!canGenerate.value) return; - - loading.value = true; - status.value = "Generating scene..."; - tone.value = "ok"; - result.value = null; - - try { - // Build request payload - const requestPayload: SceneRequestPayload = { - prompt: prompt.value, - system_prompt: systemPrompt.value || undefined, - provider: provider.value || undefined, - api_key: apiKey.value || undefined, - project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, - options: { - model: model.value || undefined, - temperature: temperature.value - } - }; - - // Include media import data if present - if (hasPendingMediaImport() && pendingMediaImport.value) { - requestPayload.media_import = { - data: pendingMediaImport.value.data, - name: pendingMediaImport.value.name, - type: pendingMediaImport.value.type, - texture_type: pendingMediaImport.value.textureType, - audio_format: pendingMediaImport.value.audioFormat - }; - } - - const response = await createScene(requestPayload); - - if (response.success) { - status.value = "Scene generated successfully."; - result.value = response.data as unknown as AgentResult; - - // Clear pending media import after successful generation - if (hasPendingMediaImport()) { - clearPendingMediaImport(); - } - } else { - tone.value = "error"; - status.value = response.error || "Unknown error occurred"; - } - } catch (e) { - tone.value = "error"; - status.value = e instanceof Error ? e.message : String(e); - } finally { - loading.value = false; - } - } - - return { - prompt, - provider, - model, - providers, - providerOptions, - temperature, - apiKey, - systemPrompt, - defaultSystemPrompt, - availableModels, - showModelManager, - refreshModels, - status, - tone, - result, - loading, - canGenerate, - run, - TEMPERATURE_PRESETS, - // Media import state - pendingMediaImport, - hasPendingMediaImport - }; -} +import { computed, ref, watch, onMounted } from "vue"; +import { createScene } from "@/api/client"; +import { DEFAULT_PROJECT_NAME } from "@/api/constants"; +import { TEMPERATURE_PRESETS } from "@/constants/providers"; +import { useIntelligenceStore } from "@/store/intelligenceStore"; +import { useSessionProject } from "@/composables/useSessionProject"; +import { useMediaImport } from "@/composables/useMediaImport"; +import { useUnityMcpStatus } from "@/composables/useUnityMcpStatus"; + +export function useScenesPanel() { + const store = useIntelligenceStore(); + const { projectName } = useSessionProject(); + const { pendingMediaImport, pendingPrompt, clearPendingMediaImport, hasPendingMediaImport } = useMediaImport(); + const { available: mcpAvailable, editorAvailable, warning: mcpWarning, applyViaMcp } = useUnityMcpStatus(); + + const prompt = ref(""); + const provider = ref(""); + const model = ref(""); + const temperature = ref(0.7); + const apiKey = ref(""); + const systemPrompt = ref(""); + const defaultSystemPrompt = ref("Default: You are a senior Unity engineer..."); + + const status = ref(null); + const tone = ref<"ok" | "error">("ok"); + + interface AgentResult { + content: string; + files: string[]; + metadata?: { + steps?: string[]; + }; + } + + interface MediaImportPayload { + data: string; + name: string; + type: string; + texture_type?: string; + audio_format?: string; + } + + interface SceneRequestPayload { + prompt: string; + system_prompt?: string; + provider?: string; + api_key?: string; + project_name: string; + options: { + model?: string; + temperature: number; + }; + media_import?: MediaImportPayload; + apply_via_mcp?: boolean; + } + + const result = ref(null); + const loading = ref(false); + + // Discovery data from store + const providers = computed(() => store.getProvidersByModality("llm")); + const providerOptions = computed(() => + providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) + ); + const availableModels = computed(() => store.getModelsByProvider(provider.value, "llm")); + const showModelManager = ref(false); + + onMounted(async () => { + try { + await store.load(); + + const preferred = store.getPreferredEngine("llm"); + if (!provider.value) provider.value = preferred.provider; + if (!model.value) model.value = preferred.model; + + const dbSysPrompt = store.getPreference("default_code_system_prompt"); + if (dbSysPrompt) { + defaultSystemPrompt.value = `Default: ${dbSysPrompt.substring(0, 50)}...`; + } + } catch (e) { + console.error("Failed to load ScenesPanel state via store", e); + } + + // Check for pending media import from ImagePanel or AudioPanel + if (hasPendingMediaImport() && pendingPrompt.value) { + prompt.value = pendingPrompt.value; + status.value = `Ready to import ${pendingMediaImport.value?.type || 'media'} to Unity`; + tone.value = "ok"; + } + }); + + watch(provider, () => { + // Reset model if not in new provider's list + if (provider.value && !availableModels.value.some((m: { value: string }) => m.value === model.value)) { + model.value = ""; + } + }); + + async function refreshModels() { + await store.load(); + } + + + const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); + + async function run() { + if (!canGenerate.value) return; + + loading.value = true; + status.value = "Generating scene..."; + tone.value = "ok"; + result.value = null; + + try { + // Build request payload + const requestPayload: SceneRequestPayload = { + prompt: prompt.value, + system_prompt: systemPrompt.value || undefined, + provider: provider.value || undefined, + api_key: apiKey.value || undefined, + project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, + options: { + model: model.value || undefined, + temperature: temperature.value + }, + apply_via_mcp: applyViaMcp.value, + }; + + // Include media import data if present + if (hasPendingMediaImport() && pendingMediaImport.value) { + requestPayload.media_import = { + data: pendingMediaImport.value.data, + name: pendingMediaImport.value.name, + type: pendingMediaImport.value.type, + texture_type: pendingMediaImport.value.textureType, + audio_format: pendingMediaImport.value.audioFormat + }; + } + + const response = await createScene(requestPayload); + + if (response.success) { + status.value = "Scene generated successfully."; + result.value = response.data as unknown as AgentResult; + + // Clear pending media import after successful generation + if (hasPendingMediaImport()) { + clearPendingMediaImport(); + } + } else { + tone.value = "error"; + status.value = response.error || "Unknown error occurred"; + } + } catch (e) { + tone.value = "error"; + status.value = e instanceof Error ? e.message : String(e); + } finally { + loading.value = false; + } + } + + return { + prompt, + provider, + model, + providers, + providerOptions, + temperature, + apiKey, + systemPrompt, + defaultSystemPrompt, + availableModels, + showModelManager, + refreshModels, + status, + tone, + result, + loading, + canGenerate, + run, + TEMPERATURE_PRESETS, + // Media import state + pendingMediaImport, + hasPendingMediaImport, + mcpAvailable, + editorAvailable, + mcpWarning, + applyViaMcp, + }; +} diff --git a/frontend/src/components/ScenesPanel.vue b/frontend/src/components/ScenesPanel.vue index 8ce0b174..f16bbd02 100644 --- a/frontend/src/components/ScenesPanel.vue +++ b/frontend/src/components/ScenesPanel.vue @@ -1,170 +1,190 @@ - - - - - + + + + + diff --git a/frontend/src/components/UnityAnimationsPanel/UnityAnimationsPanel.vue b/frontend/src/components/UnityAnimationsPanel/UnityAnimationsPanel.vue new file mode 100644 index 00000000..bcb931c8 --- /dev/null +++ b/frontend/src/components/UnityAnimationsPanel/UnityAnimationsPanel.vue @@ -0,0 +1,8 @@ + + + diff --git a/frontend/src/components/UnityAnimationsPanel/index.ts b/frontend/src/components/UnityAnimationsPanel/index.ts new file mode 100644 index 00000000..ede440b0 --- /dev/null +++ b/frontend/src/components/UnityAnimationsPanel/index.ts @@ -0,0 +1 @@ +export { default as UnityAnimationsPanel } from "./UnityAnimationsPanel.vue"; diff --git a/frontend/src/components/UnityCameraPanel/UnityCameraPanel.vue b/frontend/src/components/UnityCameraPanel/UnityCameraPanel.vue new file mode 100644 index 00000000..45ad075e --- /dev/null +++ b/frontend/src/components/UnityCameraPanel/UnityCameraPanel.vue @@ -0,0 +1,8 @@ + + + diff --git a/frontend/src/components/UnityCameraPanel/index.ts b/frontend/src/components/UnityCameraPanel/index.ts new file mode 100644 index 00000000..da01e093 --- /dev/null +++ b/frontend/src/components/UnityCameraPanel/index.ts @@ -0,0 +1 @@ +export { default as UnityCameraPanel } from "./UnityCameraPanel.vue"; diff --git a/frontend/src/components/UnityLightingPanel/UnityLightingPanel.vue b/frontend/src/components/UnityLightingPanel/UnityLightingPanel.vue new file mode 100644 index 00000000..4a77ad9f --- /dev/null +++ b/frontend/src/components/UnityLightingPanel/UnityLightingPanel.vue @@ -0,0 +1,8 @@ + + + diff --git a/frontend/src/components/UnityLightingPanel/index.ts b/frontend/src/components/UnityLightingPanel/index.ts new file mode 100644 index 00000000..4b7f9929 --- /dev/null +++ b/frontend/src/components/UnityLightingPanel/index.ts @@ -0,0 +1 @@ +export { default as UnityLightingPanel } from "./UnityLightingPanel.vue"; diff --git a/frontend/src/components/UnityMaterialsPanel/UnityMaterialsPanel.vue b/frontend/src/components/UnityMaterialsPanel/UnityMaterialsPanel.vue new file mode 100644 index 00000000..6ac9b796 --- /dev/null +++ b/frontend/src/components/UnityMaterialsPanel/UnityMaterialsPanel.vue @@ -0,0 +1,8 @@ + + + diff --git a/frontend/src/components/UnityMaterialsPanel/index.ts b/frontend/src/components/UnityMaterialsPanel/index.ts new file mode 100644 index 00000000..f0001947 --- /dev/null +++ b/frontend/src/components/UnityMaterialsPanel/index.ts @@ -0,0 +1 @@ +export { default as UnityMaterialsPanel } from "./UnityMaterialsPanel.vue"; diff --git a/frontend/src/components/UnityMcpHubPanel/UnityMcpHubPanel.vue b/frontend/src/components/UnityMcpHubPanel/UnityMcpHubPanel.vue new file mode 100644 index 00000000..6251479d --- /dev/null +++ b/frontend/src/components/UnityMcpHubPanel/UnityMcpHubPanel.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/frontend/src/components/UnityMcpHubPanel/index.ts b/frontend/src/components/UnityMcpHubPanel/index.ts new file mode 100644 index 00000000..1f1de7f1 --- /dev/null +++ b/frontend/src/components/UnityMcpHubPanel/index.ts @@ -0,0 +1 @@ +export { default as UnityMcpHubPanel } from "./UnityMcpHubPanel.vue"; diff --git a/frontend/src/components/UnityMcpModalityPanel/UnityMcpModalityPanel.vue b/frontend/src/components/UnityMcpModalityPanel/UnityMcpModalityPanel.vue new file mode 100644 index 00000000..9ab61e15 --- /dev/null +++ b/frontend/src/components/UnityMcpModalityPanel/UnityMcpModalityPanel.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/frontend/src/components/UnityMcpModalityPanel/index.ts b/frontend/src/components/UnityMcpModalityPanel/index.ts new file mode 100644 index 00000000..88d96d92 --- /dev/null +++ b/frontend/src/components/UnityMcpModalityPanel/index.ts @@ -0,0 +1 @@ +export { default as UnityMcpModalityPanel } from "./UnityMcpModalityPanel.vue"; diff --git a/frontend/src/components/UnityPhysicsPanel/UnityPhysicsPanel.ts b/frontend/src/components/UnityPhysicsPanel/UnityPhysicsPanel.ts index 872b977e..f7fb5ff1 100644 --- a/frontend/src/components/UnityPhysicsPanel/UnityPhysicsPanel.ts +++ b/frontend/src/components/UnityPhysicsPanel/UnityPhysicsPanel.ts @@ -1,209 +1,216 @@ -import { computed, ref, onMounted, watch } from "vue"; -import { generateUnityPhysics } from "@/api/client"; -import { DEFAULT_PROJECT_NAME } from "@/api/constants"; -import { TEMPERATURE_PRESETS } from "@/constants/providers"; -import { PHYSICS_EXAMPLE_PROMPTS } from "@/constants/unityPhysicsPrompts"; -import { useIntelligenceStore } from "@/store/intelligenceStore"; -import { useSessionProject } from "@/composables/useSessionProject"; - -export { PHYSICS_EXAMPLE_PROMPTS } from "@/constants/unityPhysicsPrompts"; - -export interface PhysicsAgentResult { - content: string; - files: string[]; - metadata?: { - steps?: string[]; - }; -} - -export const PHYSICS_QUICK_ACTIONS = [ - { label: "Bouncy Ball", icon: "mdi-circle-outline", prompt: "Create a bouncy ball with realistic PhysX physics, high bounciness PhysicMaterial, and smooth sphere collider.", gravityPreset: "earth" }, - { label: "Zero-G Object", icon: "mdi-orbit", prompt: "Set up a floating object in zero gravity with drag-based movement and no gravity influence.", gravityPreset: "zero_g" }, - { label: "Platform Character", icon: "mdi-human-handsup", prompt: "Configure a platform game character controller with Rigidbody, CapsuleCollider, and ground detection.", gravityPreset: "earth" }, - { label: "Ragdoll Setup", icon: "mdi-human", prompt: "Generate a ragdoll physics setup with connected Rigidbodies, HingeJoints, and appropriate mass distribution.", gravityPreset: "earth" }, - { label: "Vehicle Physics", icon: "mdi-car-sports", prompt: "Set up a vehicle with WheelColliders, Rigidbody centre-of-mass adjustment, and suspension configuration.", gravityPreset: "earth" }, -]; - -/** - * Composable for the Unity Physics panel. - * - * Manages all reactive state and generation logic for creating Unity physics - * configuration code via the /generate/unity-physics backend endpoint. - * - * @returns Reactive state and functions for the UnityPhysicsPanel component. - * - * @example - * ```typescript - * const { prompt, run, loading } = useUnityPhysicsPanel(); - * ``` - */ -export function useUnityPhysicsPanel() { - const store = useIntelligenceStore(); - const { projectName } = useSessionProject(); - - // Core generation fields - const prompt = ref(""); - const provider = ref(""); - const model = ref(""); - const temperature = ref(0.7); - const apiKey = ref(""); - const systemPrompt = ref(""); - const defaultSystemPrompt = ref("Default: You are a senior Unity physics engineer..."); - - // Physics-specific fields - const physicsBackend = ref("physx"); - const simulationMode = ref("fixed_update"); - const gravityPreset = ref(""); - const includeRigidbody = ref(true); - const includeColliders = ref(true); - const includeLayers = ref(false); - - // Status / result - const status = ref(null); - const tone = ref<"ok" | "error">("ok"); - const result = ref(null); - const loading = ref(false); - - // Provider/model discovery - const providers = computed(() => store.getProvidersByModality("llm")); - const providerOptions = computed(() => - providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) - ); - const availableModels = computed(() => store.getModelsByProvider(provider.value, "llm")); - const showModelManager = ref(false); - - onMounted(async () => { - try { - await store.load(); - const preferred = store.getPreferredEngine("llm"); - if (!provider.value) provider.value = preferred.provider; - if (!model.value) model.value = preferred.model; - - const dbSysPrompt = store.getPreference("default_code_system_prompt"); - if (dbSysPrompt) { - defaultSystemPrompt.value = `Default: ${dbSysPrompt.substring(0, 50)}...`; - } - } catch (e) { - console.error("Failed to load UnityPhysicsPanel state via store", e); - } - }); - - // Reset model when provider changes and current model is no longer available - watch(provider, () => { - if (provider.value && !availableModels.value.some((m: { value: string }) => m.value === model.value)) { - model.value = ""; - } - }); - - /** - * Reload available models from the intelligence store. - * - * @returns Promise that resolves when the store has finished reloading. - * - * @example - * ```typescript - * await refreshModels(); - * ``` - */ - async function refreshModels(): Promise { - await store.load(); - } - - const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); - - /** - * Submit the physics generation request to the backend. - * - * Validates the prompt, builds the request payload, calls the API, and - * updates status/result refs based on the response. - * - * @returns Promise that resolves when generation completes or fails. - * - * @example - * ```typescript - * await run(); - * // status.value and result.value are updated - * ``` - */ - async function run(): Promise { - if (!prompt.value.trim()) { - tone.value = "error"; - status.value = "Prompt cannot be empty"; - return; - } - if (loading.value) return; - - loading.value = true; - status.value = "Generating physics configuration..."; - tone.value = "ok"; - result.value = null; - - try { - const response = await generateUnityPhysics({ - prompt: prompt.value, - provider: provider.value || undefined, - api_key: apiKey.value || undefined, - system_prompt: systemPrompt.value || undefined, - project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, - physics_backend: physicsBackend.value, - simulation_mode: simulationMode.value, - gravity_preset: gravityPreset.value || undefined, - include_rigidbody: includeRigidbody.value, - include_colliders: includeColliders.value, - include_layers: includeLayers.value, - options: { - model: model.value || undefined, - temperature: temperature.value, - }, - }); - - if (response.success) { - status.value = "Physics configuration generated successfully."; - result.value = response.data as unknown as PhysicsAgentResult; - } else { - tone.value = "error"; - status.value = response.error || "Unknown error occurred"; - } - } catch (e) { - tone.value = "error"; - status.value = e instanceof Error ? e.message : String(e); - } finally { - loading.value = false; - } - } - - return { - // Core fields - prompt, - provider, - model, - temperature, - apiKey, - systemPrompt, - defaultSystemPrompt, - // Physics-specific fields - physicsBackend, - simulationMode, - gravityPreset, - includeRigidbody, - includeColliders, - includeLayers, - // Provider/model - providers, - providerOptions, - availableModels, - showModelManager, - refreshModels, - // Status / result - status, - tone, - result, - loading, - canGenerate, - run, - // Constants - TEMPERATURE_PRESETS, - PHYSICS_QUICK_ACTIONS, - PHYSICS_EXAMPLE_PROMPTS, - }; -} +import { computed, ref, onMounted, watch } from "vue"; +import { generateUnityPhysics } from "@/api/client"; +import { DEFAULT_PROJECT_NAME } from "@/api/constants"; +import { TEMPERATURE_PRESETS } from "@/constants/providers"; +import { PHYSICS_EXAMPLE_PROMPTS } from "@/constants/unityPhysicsPrompts"; +import { useIntelligenceStore } from "@/store/intelligenceStore"; +import { useSessionProject } from "@/composables/useSessionProject"; +import { useUnityMcpStatus } from "@/composables/useUnityMcpStatus"; + +export { PHYSICS_EXAMPLE_PROMPTS } from "@/constants/unityPhysicsPrompts"; + +export interface PhysicsAgentResult { + content: string; + files: string[]; + metadata?: { + steps?: string[]; + }; +} + +export const PHYSICS_QUICK_ACTIONS = [ + { label: "Bouncy Ball", icon: "mdi-circle-outline", prompt: "Create a bouncy ball with realistic PhysX physics, high bounciness PhysicMaterial, and smooth sphere collider.", gravityPreset: "earth" }, + { label: "Zero-G Object", icon: "mdi-orbit", prompt: "Set up a floating object in zero gravity with drag-based movement and no gravity influence.", gravityPreset: "zero_g" }, + { label: "Platform Character", icon: "mdi-human-handsup", prompt: "Configure a platform game character controller with Rigidbody, CapsuleCollider, and ground detection.", gravityPreset: "earth" }, + { label: "Ragdoll Setup", icon: "mdi-human", prompt: "Generate a ragdoll physics setup with connected Rigidbodies, HingeJoints, and appropriate mass distribution.", gravityPreset: "earth" }, + { label: "Vehicle Physics", icon: "mdi-car-sports", prompt: "Set up a vehicle with WheelColliders, Rigidbody centre-of-mass adjustment, and suspension configuration.", gravityPreset: "earth" }, +]; + +/** + * Composable for the Unity Physics panel. + * + * Manages all reactive state and generation logic for creating Unity physics + * configuration code via the /generate/unity-physics backend endpoint. + * + * @returns Reactive state and functions for the UnityPhysicsPanel component. + * + * @example + * ```typescript + * const { prompt, run, loading } = useUnityPhysicsPanel(); + * ``` + */ +export function useUnityPhysicsPanel() { + const store = useIntelligenceStore(); + const { projectName } = useSessionProject(); + const { available: mcpAvailable, editorAvailable, warning: mcpWarning, applyViaMcp } = useUnityMcpStatus(); + + // Core generation fields + const prompt = ref(""); + const provider = ref(""); + const model = ref(""); + const temperature = ref(0.7); + const apiKey = ref(""); + const systemPrompt = ref(""); + const defaultSystemPrompt = ref("Default: You are a senior Unity physics engineer..."); + + // Physics-specific fields + const physicsBackend = ref("physx"); + const simulationMode = ref("fixed_update"); + const gravityPreset = ref(""); + const includeRigidbody = ref(true); + const includeColliders = ref(true); + const includeLayers = ref(false); + + // Status / result + const status = ref(null); + const tone = ref<"ok" | "error">("ok"); + const result = ref(null); + const loading = ref(false); + + // Provider/model discovery + const providers = computed(() => store.getProvidersByModality("llm")); + const providerOptions = computed(() => + providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) + ); + const availableModels = computed(() => store.getModelsByProvider(provider.value, "llm")); + const showModelManager = ref(false); + + onMounted(async () => { + try { + await store.load(); + const preferred = store.getPreferredEngine("llm"); + if (!provider.value) provider.value = preferred.provider; + if (!model.value) model.value = preferred.model; + + const dbSysPrompt = store.getPreference("default_code_system_prompt"); + if (dbSysPrompt) { + defaultSystemPrompt.value = `Default: ${dbSysPrompt.substring(0, 50)}...`; + } + } catch (e) { + console.error("Failed to load UnityPhysicsPanel state via store", e); + } + }); + + // Reset model when provider changes and current model is no longer available + watch(provider, () => { + if (provider.value && !availableModels.value.some((m: { value: string }) => m.value === model.value)) { + model.value = ""; + } + }); + + /** + * Reload available models from the intelligence store. + * + * @returns Promise that resolves when the store has finished reloading. + * + * @example + * ```typescript + * await refreshModels(); + * ``` + */ + async function refreshModels(): Promise { + await store.load(); + } + + const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); + + /** + * Submit the physics generation request to the backend. + * + * Validates the prompt, builds the request payload, calls the API, and + * updates status/result refs based on the response. + * + * @returns Promise that resolves when generation completes or fails. + * + * @example + * ```typescript + * await run(); + * // status.value and result.value are updated + * ``` + */ + async function run(): Promise { + if (!prompt.value.trim()) { + tone.value = "error"; + status.value = "Prompt cannot be empty"; + return; + } + if (loading.value) return; + + loading.value = true; + status.value = "Generating physics configuration..."; + tone.value = "ok"; + result.value = null; + + try { + const response = await generateUnityPhysics({ + prompt: prompt.value, + provider: provider.value || undefined, + api_key: apiKey.value || undefined, + system_prompt: systemPrompt.value || undefined, + project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, + physics_backend: physicsBackend.value, + simulation_mode: simulationMode.value, + gravity_preset: gravityPreset.value || undefined, + include_rigidbody: includeRigidbody.value, + include_colliders: includeColliders.value, + include_layers: includeLayers.value, + apply_via_mcp: applyViaMcp.value, + options: { + model: model.value || undefined, + temperature: temperature.value, + }, + }); + + if (response.success) { + status.value = "Physics configuration generated successfully."; + result.value = response.data as unknown as PhysicsAgentResult; + } else { + tone.value = "error"; + status.value = response.error || "Unknown error occurred"; + } + } catch (e) { + tone.value = "error"; + status.value = e instanceof Error ? e.message : String(e); + } finally { + loading.value = false; + } + } + + return { + // Core fields + prompt, + provider, + model, + temperature, + apiKey, + systemPrompt, + defaultSystemPrompt, + // Physics-specific fields + physicsBackend, + simulationMode, + gravityPreset, + includeRigidbody, + includeColliders, + includeLayers, + mcpAvailable, + editorAvailable, + mcpWarning, + applyViaMcp, + // Provider/model + providers, + providerOptions, + availableModels, + showModelManager, + refreshModels, + // Status / result + status, + tone, + result, + loading, + canGenerate, + run, + // Constants + TEMPERATURE_PRESETS, + PHYSICS_QUICK_ACTIONS, + PHYSICS_EXAMPLE_PROMPTS, + }; +} diff --git a/frontend/src/components/UnityPhysicsPanel/UnityPhysicsPanel.vue b/frontend/src/components/UnityPhysicsPanel/UnityPhysicsPanel.vue index 4a5b1120..9efe1cd9 100644 --- a/frontend/src/components/UnityPhysicsPanel/UnityPhysicsPanel.vue +++ b/frontend/src/components/UnityPhysicsPanel/UnityPhysicsPanel.vue @@ -1,192 +1,204 @@ - - - - - - + + + + + + diff --git a/frontend/src/components/UnityProjectPanel/UnityProjectPanel.ts b/frontend/src/components/UnityProjectPanel/UnityProjectPanel.ts index fbdf3a4c..362fcec7 100644 --- a/frontend/src/components/UnityProjectPanel/UnityProjectPanel.ts +++ b/frontend/src/components/UnityProjectPanel/UnityProjectPanel.ts @@ -1,417 +1,460 @@ -import { computed, onMounted, onUnmounted, reactive, ref } from "vue"; -import { - generateUnityProject, - finalizeProject, - getFinalizeJobStatus, - downloadFinalizedProject, - getUnityVersions, - addUnityVersion, - getLatestOutput, - type UnityVersionOption, -} from "@/api/client"; -import type { FinalizeJobStatusResponse } from "@/api/client"; -import { - ASPECT_RATIOS, - QUALITY_OPTIONS, - TEMPERATURE_PRESETS, - LENGTH_PRESETS, - STABILITY_PRESETS -} from "@/constants/providers"; -import { DEFAULT_PROJECT_NAME } from "@/api/constants"; -import { FINALIZE_STATUS, UI_TONE } from "@/constants/finalize"; -import { UNITY_TEMPLATES, UNITY_PLATFORMS } from "@/constants/unity"; -import { electronShell } from "@/services/electronShell"; -import { setActiveProject } from "@/store/projectStore"; -import { useUnityProjectUiStore } from "@/store/unityProjectUiStore"; -import { useSessionProject } from "@/composables/useSessionProject"; -import { useOpenProject } from "@/composables/useOpenProject"; - -const DEFAULT_UNITY_VERSIONS: UnityVersionOption[] = [ - { value: "6000.3.2f1", label: "6000.3.2f1" }, -]; - -export function useUnityProjectPanel() { - const { projectName, projectPath, sessionProjectResetKey, setProjectPath } = useSessionProject(); - const { loadProject } = useOpenProject(); - const uiStore = useUnityProjectUiStore(); - - // Unity Engine Settings - // Use store object directly (do not wrap with reactive), so v-model remains stable across remounts - const settings = uiStore.settings; - - - // Finalize job state - const finalize = reactive({ - jobId: "", - status: null as FinalizeJobStatusResponse | null, - logs: [] as string[], - polling: false - }); - let pollTimer: ReturnType | null = null; - - const status = computed({ - get: () => uiStore.status, - set: (v: string | null) => uiStore.$patch({ status: v }), - }); - const tone = computed({ - get: () => uiStore.tone, - set: (v: "ok" | "error") => uiStore.$patch({ tone: v }), - }); - const result = computed({ - get: () => uiStore.result, - set: (v: string) => uiStore.$patch({ result: v }), - }); - const lastProjectPath = computed({ - get: () => uiStore.lastProjectPath, - set: (v: string) => uiStore.$patch({ lastProjectPath: v }), - }); - - // Unity versions from API (user can add more); fallback to default if empty - const unityVersions = ref(DEFAULT_UNITY_VERSIONS); - const addVersionDialog = ref(false); - const newVersionId = ref(""); - const newVersionLabel = ref(""); - const addVersionError = ref(""); - - async function loadUnityVersions() { - try { - const res = await getUnityVersions(); - const list = (res.data as { versions?: UnityVersionOption[] } | null)?.versions; - if (res.success && Array.isArray(list)) { - unityVersions.value = list.length > 0 ? list : DEFAULT_UNITY_VERSIONS; - } else { - unityVersions.value = DEFAULT_UNITY_VERSIONS; - } - } catch { - unityVersions.value = DEFAULT_UNITY_VERSIONS; - } - } - - function openAddVersionDialog() { - newVersionId.value = ""; - newVersionLabel.value = ""; - addVersionError.value = ""; - addVersionDialog.value = true; - } - - function closeAddVersionDialog() { - addVersionDialog.value = false; - } - - async function submitAddVersion() { - const id = newVersionId.value.trim(); - const label = newVersionLabel.value.trim() || id; - if (!id) { - addVersionError.value = "Version ID is required (e.g. 6000.3.2f1)."; - return; - } - addVersionError.value = ""; - try { - const res = await addUnityVersion({ value: id, label: label || id }); - if (!res.success) { - addVersionError.value = res.error || "Failed to add version."; - return; - } - const list = (res.data as { versions?: UnityVersionOption[] } | null)?.versions; - if (Array.isArray(list)) { - unityVersions.value = list; - settings.version = id; - } - closeAddVersionDialog(); - } catch (e) { - addVersionError.value = String(e); - } - } - - onMounted(() => { - loadUnityVersions(); - }); - - // Persistence is handled by the store (enableAutoPersist), so it doesn't depend on this panel being mounted. - - const availableVoices = computed(() => { - return []; - }); - - const isFinalizing = computed(() => { - if (!finalize.status) return false; - const s = finalize.status.status; - return s === FINALIZE_STATUS.PENDING || s === FINALIZE_STATUS.RUNNING; - }); - - const finalizeProgress = computed(() => { - return finalize.status?.progress ?? 0; - }); - - const finalizeStep = computed(() => { - return finalize.status?.step ?? ""; - }); - - const finalizeDownloadUrl = computed(() => { - if ( - finalize.status?.status === FINALIZE_STATUS.COMPLETED && - finalize.status?.zip_path - ) { - return downloadFinalizedProject(finalize.jobId); - } - return ""; - }); - - async function run() { - status.value = "Generating Unity project..."; - tone.value = UI_TONE.OK; - // Validation: required fields - if (!projectName.value.trim()) { - tone.value = UI_TONE.ERROR; - status.value = "Project name is required."; - return; - } - if (!settings.template) { - tone.value = UI_TONE.ERROR; - status.value = "Unity template selection is required."; - return; - } - if (!settings.version) { - tone.value = UI_TONE.ERROR; - status.value = "Unity version selection is required."; - return; - } - if (!settings.platform) { - tone.value = UI_TONE.ERROR; - status.value = "Target platform selection is required."; - return; - } - try { - const response = await generateUnityProject({ - project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, - unity_template: settings.template, - unity_version: settings.version, - unity_platform: settings.platform, - }); - if (!response.success) { - tone.value = UI_TONE.ERROR; - status.value = response.error || "Failed to generate project."; - return; - } - status.value = "Unity project generated."; - result.value = JSON.stringify(response.data || {}, null, 2); - const path = String(response.data?.project_path || ""); - lastProjectPath.value = path; - if (path) { - setProjectPath(path); - setActiveProject(projectName.value, path); - } - } catch (error) { - tone.value = UI_TONE.ERROR; - status.value = String(error); - } - } - - async function runFinalize() { - status.value = "Starting finalize workflow..."; - tone.value = UI_TONE.OK; - finalize.logs = []; - finalize.status = null; - - try { - const packages = settings.installPackages - ? settings.packages - .split(",") - .map((s) => s.trim()) - .filter(Boolean) - : []; - - const response = await finalizeProject({ - project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, - project_path: lastProjectPath.value || projectPath.value || undefined, - unity_settings: { - install_packages: settings.installPackages, - generate_scene: settings.generateScene, - setup_urp: settings.setupUrp, - packages, - scene_name: settings.sceneName, - unity_editor_path: settings.editorPath || undefined, - timeout: settings.timeout, - }, - }); - - if (!response.success) { - tone.value = UI_TONE.ERROR; - status.value = "Failed to create finalize job."; - return; - } - - finalize.jobId = response.job_id; - status.value = `Finalize job started (${response.job_id})`; - startPolling(); - } catch (error) { - tone.value = UI_TONE.ERROR; - status.value = String(error); - } - } - - function startPolling() { - stopPolling(); - finalize.polling = true; - pollTimer = setInterval(pollStatus, 2000); - } - - function stopPolling() { - finalize.polling = false; - if (pollTimer) { - clearInterval(pollTimer); - pollTimer = null; - } - } - - async function pollStatus() { - if (!finalize.jobId) { - stopPolling(); - return; - } - try { - const statusResp = await getFinalizeJobStatus(finalize.jobId); - finalize.status = statusResp; - finalize.logs = statusResp.logs_tail || []; - - if (statusResp.status === FINALIZE_STATUS.COMPLETED) { - stopPolling(); - tone.value = UI_TONE.OK; - status.value = "Finalization completed successfully!"; - const path = statusResp.project_path || ""; - lastProjectPath.value = path; - if (path) setProjectPath(path); - } else if (statusResp.status === FINALIZE_STATUS.FAILED) { - stopPolling(); - tone.value = UI_TONE.ERROR; - const errMsg = statusResp.errors?.join("; ") || "Unknown error"; - status.value = `Finalization failed: ${errMsg}`; - } - } catch (error) { - stopPolling(); - tone.value = UI_TONE.ERROR; - status.value = `Polling error: ${String(error)}`; - } - } - - /** - * Open a Unity project folder picker (Electron) and load the selected project. - * - * Delegates to the main process via `unityProject.openPicker` which shows a - * native folder dialog with the correct parent window reference (avoids the - * detached-dialog bug on Windows). The main process also validates that the - * selected folder is a Unity project before returning the path. - * - * @example - * ```typescript - * await openOutputFolder(); - * // Populates version/template fields and updates status - * ``` - */ - async function openOutputFolder(): Promise { - const api = window.electronAPI; - - if (api?.unityProject?.openPicker) { - try { - const pickerResult = await api.unityProject.openPicker(); - - if (pickerResult.canceled) return; - - if (pickerResult.error || !pickerResult.projectPath) { - tone.value = UI_TONE.ERROR; - status.value = pickerResult.error || "No project path returned."; - return; - } - - const selectedPath = pickerResult.projectPath; - - // Load project: sets name, path, scans for version/packages/template - await loadProject(selectedPath); - lastProjectPath.value = selectedPath; - - // Only set success status if loadProject didn't surface an error - if (uiStore.tone !== UI_TONE.ERROR) { - const folderName = selectedPath.replace(/\\/g, "/").split("/").pop() || selectedPath; - status.value = `Opened project: ${folderName}`; - tone.value = UI_TONE.OK; - } - } catch (error) { - tone.value = UI_TONE.ERROR; - status.value = `Failed to open project: ${String(error)}`; - } - return; - } - - // Non-Electron fallback: open the known project path, or fetch latest from backend - let knownPath = lastProjectPath.value || projectPath.value; - if (!knownPath) { - try { - const latest = await getLatestOutput(); - knownPath = latest.success ? String(latest.data?.path || "") : ""; - } catch { - knownPath = ""; - } - } - - if (!knownPath) { - tone.value = UI_TONE.ERROR; - status.value = "No project path set. Generate a project first."; - return; - } - - try { - const shellResult = await electronShell.openPath(knownPath); - if (shellResult.success) { - status.value = "Opened output folder."; - tone.value = UI_TONE.OK; - } else { - tone.value = UI_TONE.ERROR; - status.value = `Failed to open folder: ${shellResult.error || "Unknown error"}`; - } - } catch (error) { - tone.value = UI_TONE.ERROR; - status.value = `Failed to open folder: ${error}`; - } - } - - onUnmounted(() => { - stopPolling(); - }); - - return { - projectName, - sessionProjectResetKey, - settings, - finalize, - UNITY_TEMPLATES, - unityVersions, - UNITY_PLATFORMS, - loadUnityVersions, - addVersionDialog, - newVersionId, - newVersionLabel, - addVersionError, - openAddVersionDialog, - closeAddVersionDialog, - submitAddVersion, - status, - tone, - result, - lastProjectPath, - availableVoices, - isFinalizing, - finalizeProgress, - finalizeStep, - finalizeDownloadUrl, - run, - runFinalize, - openOutputFolder, - ASPECT_RATIOS, - QUALITY_OPTIONS, - TEMPERATURE_PRESETS, - LENGTH_PRESETS, - STABILITY_PRESETS, - FINALIZE_STATUS, - UI_TONE - }; -} +import { computed, onMounted, onUnmounted, reactive, ref } from "vue"; +import { + generateUnityProject, + finalizeProject, + getFinalizeJobStatus, + downloadFinalizedProject, + getUnityVersions, + addUnityVersion, + getLatestOutput, + getUnityMcpStatus, + type UnityVersionOption, +} from "@/api/client"; +import type { FinalizeJobStatusResponse } from "@/api/client"; +import { + ASPECT_RATIOS, + QUALITY_OPTIONS, + TEMPERATURE_PRESETS, + LENGTH_PRESETS, + STABILITY_PRESETS +} from "@/constants/providers"; +import { DEFAULT_PROJECT_NAME } from "@/api/constants"; +import { FINALIZE_STATUS, UI_TONE } from "@/constants/finalize"; +import { UNITY_TEMPLATES, UNITY_PLATFORMS } from "@/constants/unity"; +import { electronShell } from "@/services/electronShell"; +import { setActiveProject } from "@/store/projectStore"; +import { useUnityProjectUiStore } from "@/store/unityProjectUiStore"; +import { useSessionProject } from "@/composables/useSessionProject"; +import { useOpenProject } from "@/composables/useOpenProject"; + +const DEFAULT_UNITY_VERSIONS: UnityVersionOption[] = [ + { value: "6000.3.2f1", label: "6000.3.2f1" }, +]; + +const AUTOMATION_MODE_OPTIONS = [ + { value: "auto", label: "Auto (MCP when available, else batch)" }, + { value: "mcp", label: "Unity MCP (live Editor — recommended)" }, + { value: "batch", label: "Batch mode (headless Unity.exe)" }, +] as const; + +export type UnityMcpStatusPayload = { + available?: boolean; + editor_available?: boolean; + backend?: string; + editor_gated_warning?: string | null; + error?: string; +}; + +export function useUnityProjectPanel() { + const { projectName, projectPath, sessionProjectResetKey, setProjectPath } = useSessionProject(); + const { loadProject } = useOpenProject(); + const uiStore = useUnityProjectUiStore(); + + // Unity Engine Settings + // Use store object directly (do not wrap with reactive), so v-model remains stable across remounts + const settings = uiStore.settings; + + + // Finalize job state + const finalize = reactive({ + jobId: "", + status: null as FinalizeJobStatusResponse | null, + logs: [] as string[], + polling: false + }); + let pollTimer: ReturnType | null = null; + + const status = computed({ + get: () => uiStore.status, + set: (v: string | null) => uiStore.$patch({ status: v }), + }); + const tone = computed({ + get: () => uiStore.tone, + set: (v: "ok" | "error") => uiStore.$patch({ tone: v }), + }); + const result = computed({ + get: () => uiStore.result, + set: (v: string) => uiStore.$patch({ result: v }), + }); + const lastProjectPath = computed({ + get: () => uiStore.lastProjectPath, + set: (v: string) => uiStore.$patch({ lastProjectPath: v }), + }); + + // Unity versions from API (user can add more); fallback to default if empty + const unityVersions = ref(DEFAULT_UNITY_VERSIONS); + const addVersionDialog = ref(false); + const newVersionId = ref(""); + const newVersionLabel = ref(""); + const addVersionError = ref(""); + + const mcpStatus = ref(null); + const mcpStatusLoading = ref(false); + + /** + * Refresh Unity MCP availability (server + Editor bridge) from the backend. + */ + async function refreshMcpStatus(): Promise { + mcpStatusLoading.value = true; + try { + const res = await getUnityMcpStatus(); + mcpStatus.value = (res.data as UnityMcpStatusPayload | null) ?? null; + } catch { + mcpStatus.value = { + available: false, + editor_available: false, + error: "Could not reach Unity MCP status endpoint", + }; + } finally { + mcpStatusLoading.value = false; + } + } + + async function loadUnityVersions() { + try { + const res = await getUnityVersions(); + const list = (res.data as { versions?: UnityVersionOption[] } | null)?.versions; + if (res.success && Array.isArray(list)) { + unityVersions.value = list.length > 0 ? list : DEFAULT_UNITY_VERSIONS; + } else { + unityVersions.value = DEFAULT_UNITY_VERSIONS; + } + } catch { + unityVersions.value = DEFAULT_UNITY_VERSIONS; + } + } + + function openAddVersionDialog() { + newVersionId.value = ""; + newVersionLabel.value = ""; + addVersionError.value = ""; + addVersionDialog.value = true; + } + + function closeAddVersionDialog() { + addVersionDialog.value = false; + } + + async function submitAddVersion() { + const id = newVersionId.value.trim(); + const label = newVersionLabel.value.trim() || id; + if (!id) { + addVersionError.value = "Version ID is required (e.g. 6000.3.2f1)."; + return; + } + addVersionError.value = ""; + try { + const res = await addUnityVersion({ value: id, label: label || id }); + if (!res.success) { + addVersionError.value = res.error || "Failed to add version."; + return; + } + const list = (res.data as { versions?: UnityVersionOption[] } | null)?.versions; + if (Array.isArray(list)) { + unityVersions.value = list; + settings.version = id; + } + closeAddVersionDialog(); + } catch (e) { + addVersionError.value = String(e); + } + } + + onMounted(() => { + void loadUnityVersions(); + void refreshMcpStatus(); + }); + + // Persistence is handled by the store (enableAutoPersist), so it doesn't depend on this panel being mounted. + + const availableVoices = computed(() => { + return []; + }); + + const isFinalizing = computed(() => { + if (!finalize.status) return false; + const s = finalize.status.status; + return s === FINALIZE_STATUS.PENDING || s === FINALIZE_STATUS.RUNNING; + }); + + const finalizeProgress = computed(() => { + return finalize.status?.progress ?? 0; + }); + + const finalizeStep = computed(() => { + return finalize.status?.step ?? ""; + }); + + const finalizeDownloadUrl = computed(() => { + if ( + finalize.status?.status === FINALIZE_STATUS.COMPLETED && + finalize.status?.zip_path + ) { + return downloadFinalizedProject(finalize.jobId); + } + return ""; + }); + + async function run() { + status.value = "Generating Unity project..."; + tone.value = UI_TONE.OK; + // Validation: required fields + if (!projectName.value.trim()) { + tone.value = UI_TONE.ERROR; + status.value = "Project name is required."; + return; + } + if (!settings.template) { + tone.value = UI_TONE.ERROR; + status.value = "Unity template selection is required."; + return; + } + if (!settings.version) { + tone.value = UI_TONE.ERROR; + status.value = "Unity version selection is required."; + return; + } + if (!settings.platform) { + tone.value = UI_TONE.ERROR; + status.value = "Target platform selection is required."; + return; + } + try { + const response = await generateUnityProject({ + project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, + unity_template: settings.template, + unity_version: settings.version, + unity_platform: settings.platform, + }); + if (!response.success) { + tone.value = UI_TONE.ERROR; + status.value = response.error || "Failed to generate project."; + return; + } + status.value = "Unity project generated."; + result.value = JSON.stringify(response.data || {}, null, 2); + const path = String(response.data?.project_path || ""); + lastProjectPath.value = path; + if (path) { + setProjectPath(path); + setActiveProject(projectName.value, path); + } + } catch (error) { + tone.value = UI_TONE.ERROR; + status.value = String(error); + } + } + + async function runFinalize() { + status.value = "Starting finalize workflow..."; + tone.value = UI_TONE.OK; + finalize.logs = []; + finalize.status = null; + + try { + const packages = settings.installPackages + ? settings.packages + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + : []; + + const response = await finalizeProject({ + project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, + project_path: lastProjectPath.value || projectPath.value || undefined, + unity_settings: { + install_packages: settings.installPackages, + generate_scene: settings.generateScene, + setup_urp: settings.setupUrp, + packages, + scene_name: settings.sceneName, + unity_editor_path: settings.editorPath || undefined, + timeout: settings.timeout, + unity_automation_mode: settings.automationMode, + }, + }); + + if (!response.success) { + tone.value = UI_TONE.ERROR; + status.value = "Failed to create finalize job."; + return; + } + + finalize.jobId = response.job_id; + status.value = `Finalize job started (${response.job_id})`; + startPolling(); + } catch (error) { + tone.value = UI_TONE.ERROR; + status.value = String(error); + } + } + + function startPolling() { + stopPolling(); + finalize.polling = true; + pollTimer = setInterval(pollStatus, 2000); + } + + function stopPolling() { + finalize.polling = false; + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + } + + async function pollStatus() { + if (!finalize.jobId) { + stopPolling(); + return; + } + try { + const statusResp = await getFinalizeJobStatus(finalize.jobId); + finalize.status = statusResp; + finalize.logs = statusResp.logs_tail || []; + + if (statusResp.status === FINALIZE_STATUS.COMPLETED) { + stopPolling(); + tone.value = UI_TONE.OK; + status.value = "Finalization completed successfully!"; + const path = statusResp.project_path || ""; + lastProjectPath.value = path; + if (path) setProjectPath(path); + } else if (statusResp.status === FINALIZE_STATUS.FAILED) { + stopPolling(); + tone.value = UI_TONE.ERROR; + const errMsg = statusResp.errors?.join("; ") || "Unknown error"; + status.value = `Finalization failed: ${errMsg}`; + } + } catch (error) { + stopPolling(); + tone.value = UI_TONE.ERROR; + status.value = `Polling error: ${String(error)}`; + } + } + + /** + * Open a Unity project folder picker (Electron) and load the selected project. + * + * Delegates to the main process via `unityProject.openPicker` which shows a + * native folder dialog with the correct parent window reference (avoids the + * detached-dialog bug on Windows). The main process also validates that the + * selected folder is a Unity project before returning the path. + * + * @example + * ```typescript + * await openOutputFolder(); + * // Populates version/template fields and updates status + * ``` + */ + async function openOutputFolder(): Promise { + const api = window.electronAPI; + + if (api?.unityProject?.openPicker) { + try { + const pickerResult = await api.unityProject.openPicker(); + + if (pickerResult.canceled) return; + + if (pickerResult.error || !pickerResult.projectPath) { + tone.value = UI_TONE.ERROR; + status.value = pickerResult.error || "No project path returned."; + return; + } + + const selectedPath = pickerResult.projectPath; + + // Load project: sets name, path, scans for version/packages/template + await loadProject(selectedPath); + lastProjectPath.value = selectedPath; + + // Only set success status if loadProject didn't surface an error + if (uiStore.tone !== UI_TONE.ERROR) { + const folderName = selectedPath.replace(/\\/g, "/").split("/").pop() || selectedPath; + status.value = `Opened project: ${folderName}`; + tone.value = UI_TONE.OK; + } + } catch (error) { + tone.value = UI_TONE.ERROR; + status.value = `Failed to open project: ${String(error)}`; + } + return; + } + + // Non-Electron fallback: open the known project path, or fetch latest from backend + let knownPath = lastProjectPath.value || projectPath.value; + if (!knownPath) { + try { + const latest = await getLatestOutput(); + knownPath = latest.success ? String(latest.data?.path || "") : ""; + } catch { + knownPath = ""; + } + } + + if (!knownPath) { + tone.value = UI_TONE.ERROR; + status.value = "No project path set. Generate a project first."; + return; + } + + try { + const shellResult = await electronShell.openPath(knownPath); + if (shellResult.success) { + status.value = "Opened output folder."; + tone.value = UI_TONE.OK; + } else { + tone.value = UI_TONE.ERROR; + status.value = `Failed to open folder: ${shellResult.error || "Unknown error"}`; + } + } catch (error) { + tone.value = UI_TONE.ERROR; + status.value = `Failed to open folder: ${error}`; + } + } + + onUnmounted(() => { + stopPolling(); + }); + + return { + projectName, + sessionProjectResetKey, + settings, + finalize, + UNITY_TEMPLATES, + unityVersions, + UNITY_PLATFORMS, + loadUnityVersions, + addVersionDialog, + newVersionId, + newVersionLabel, + addVersionError, + openAddVersionDialog, + closeAddVersionDialog, + submitAddVersion, + status, + tone, + result, + lastProjectPath, + availableVoices, + isFinalizing, + finalizeProgress, + finalizeStep, + finalizeDownloadUrl, + mcpStatus, + mcpStatusLoading, + refreshMcpStatus, + AUTOMATION_MODE_OPTIONS, + run, + runFinalize, + openOutputFolder, + ASPECT_RATIOS, + QUALITY_OPTIONS, + TEMPERATURE_PRESETS, + LENGTH_PRESETS, + STABILITY_PRESETS, + FINALIZE_STATUS, + UI_TONE + }; +} diff --git a/frontend/src/components/UnityProjectPanel/UnityProjectPanel.vue b/frontend/src/components/UnityProjectPanel/UnityProjectPanel.vue index 5cfd7534..7a37efb0 100644 --- a/frontend/src/components/UnityProjectPanel/UnityProjectPanel.vue +++ b/frontend/src/components/UnityProjectPanel/UnityProjectPanel.vue @@ -28,6 +28,10 @@ const { finalizeProgress, finalizeStep, finalizeDownloadUrl, + mcpStatus, + mcpStatusLoading, + refreshMcpStatus, + AUTOMATION_MODE_OPTIONS, run, runFinalize, openOutputFolder, @@ -89,6 +93,38 @@ const {

{{ t('unityProject.engineSettings') }}

+ + +
{{ t('unityProject.mcp.title') }}
+
+ {{ t('unityProject.mcp.editorReady') }} +
+
+ {{ mcpStatus.editor_gated_warning || t('unityProject.mcp.editorRequired') }} +
+
+ {{ mcpStatus.error || t('unityProject.mcp.notConfigured') }} +
+
+ {{ t('unityProject.mcp.standaloneNote') }} +
+ + {{ t('unityProject.mcp.refresh') }} + +
+
@@ -109,6 +145,17 @@ const {
+
+
+ +
+
+
("ugui"); - const elementType = ref("custom"); - const outputFormat = ref("script"); - const anchorPreset = ref(""); - const colorTheme = ref(""); - const includeAnimations = ref(false); - - // Status / result - const status = ref(null); - const tone = ref<"ok" | "error">("ok"); - const result = ref(null); - const loading = ref(false); - - // Provider/model discovery - const providers = computed(() => store.getProvidersByModality("llm")); - const providerOptions = computed(() => - providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) - ); - const availableModels = computed(() => store.getModelsByProvider(provider.value, "llm")); - const showModelManager = ref(false); - - onMounted(async () => { - try { - await store.load(); - const preferred = store.getPreferredEngine("llm"); - if (!provider.value) provider.value = preferred.provider; - if (!model.value) model.value = preferred.model; - - const dbSysPrompt = store.getPreference("default_code_system_prompt"); - if (dbSysPrompt) { - defaultSystemPrompt.value = `Default: ${dbSysPrompt.substring(0, 50)}...`; - } - } catch (e) { - console.error("Failed to load UnityUIPanel state via store", e); - } - }); - - // Reset model when provider changes and current model is no longer available - watch(provider, () => { - if (provider.value && !availableModels.value.some((m: { value: string }) => m.value === model.value)) { - model.value = ""; - } - }); - - // When element type or UI system changes, pre-fill the prompt with a template - watch([elementType, uiSystem], ([newType, newSystem]) => { - if (newType && newType !== "custom") { - const template = getTemplatePrompt(newType, newSystem as UISystem); - if (template) prompt.value = template; - } - }); - - /** - * Reload available models from the intelligence store. - * Called after the user adds or removes models via the ModelManagerModal. - * - * @returns Promise that resolves when the store has finished reloading. - * - * @example - * ```typescript - * await refreshModels(); - * ``` - */ - async function refreshModels(): Promise { - await store.load(); - } - - const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); - - /** - * Submit the UI generation request to the backend. - * - * Validates the prompt, builds the request payload, calls the API, and - * updates status/result refs based on the response. - * - * @returns Promise that resolves when generation completes or fails. - * - * @example - * ```typescript - * await run(); - * // status.value and result.value are updated - * ``` - */ - async function run(): Promise { - if (!prompt.value.trim()) { - tone.value = "error"; - status.value = "Prompt cannot be empty"; - return; - } - if (loading.value) return; - - loading.value = true; - status.value = "Generating UI element..."; - tone.value = "ok"; - result.value = null; - - try { - const response = await generateUnityUI({ - prompt: prompt.value, - provider: provider.value || undefined, - api_key: apiKey.value || undefined, - system_prompt: systemPrompt.value || undefined, - project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, - ui_system: uiSystem.value, - element_type: elementType.value !== "custom" ? elementType.value : undefined, - output_format: outputFormat.value, - anchor_preset: anchorPreset.value || undefined, - color_theme: colorTheme.value || undefined, - include_animations: includeAnimations.value, - options: { - model: model.value || undefined, - temperature: temperature.value, - }, - }); - - if (response.success) { - status.value = "UI element generated successfully."; - result.value = response.data as unknown as AgentResult; - } else { - tone.value = "error"; - status.value = response.error || "Unknown error occurred"; - } - } catch (e) { - tone.value = "error"; - status.value = e instanceof Error ? e.message : String(e); - } finally { - loading.value = false; - } - } - - return { - // Core fields - prompt, - provider, - model, - temperature, - apiKey, - systemPrompt, - defaultSystemPrompt, - // UI-specific fields - uiSystem, - elementType, - outputFormat, - anchorPreset, - colorTheme, - includeAnimations, - // Provider/model - providers, - providerOptions, - availableModels, - showModelManager, - refreshModels, - // Status / result - status, - tone, - result, - loading, - canGenerate, - run, - // Constants - TEMPERATURE_PRESETS, - UI_QUICK_ACTIONS, - UI_EXAMPLE_PROMPTS, - UI_ELEMENT_TYPES, - }; -} - -export type { UIQuickAction }; +import { computed, ref, watch, onMounted } from "vue"; +import { generateUnityUI } from "@/api/client"; +import { DEFAULT_PROJECT_NAME } from "@/api/constants"; +import { TEMPERATURE_PRESETS } from "@/constants/providers"; +import { useIntelligenceStore } from "@/store/intelligenceStore"; +import { useSessionProject } from "@/composables/useSessionProject"; +import { useUnityMcpStatus } from "@/composables/useUnityMcpStatus"; +import { + UI_QUICK_ACTIONS, + UI_EXAMPLE_PROMPTS, + UI_ELEMENT_TYPES, + getTemplatePrompt, + type UIQuickAction, + type UISystem, + type UIOutputFormat, + type UIAnchorPreset, +} from "@/constants/unityUIPrompts"; + +export interface AgentResult { + content: string; + files: string[]; + metadata?: { + steps?: string[]; + }; +} + +/** + * Composable for the Unity UI Elements panel. + * + * Manages all reactive state and generation logic for creating Unity UI prefab + * assets (health bars, buttons, dialogue boxes, etc.) via the /generate/unity-ui + * backend endpoint. + * + * @returns Reactive state and functions for the UnityUIPanel component. + * + * @example + * ```typescript + * const { prompt, run, loading } = useUnityUIPanel(); + * ``` + */ +export function useUnityUIPanel() { + const store = useIntelligenceStore(); + const { projectName } = useSessionProject(); + const { available: mcpAvailable, editorAvailable, warning: mcpWarning, applyViaMcp } = useUnityMcpStatus(); + + // Core generation fields + const prompt = ref(""); + const provider = ref(""); + const model = ref(""); + const temperature = ref(0.7); + const apiKey = ref(""); + const systemPrompt = ref(""); + const defaultSystemPrompt = ref("Default: You are a senior Unity UI engineer..."); + + // UI-specific fields + const uiSystem = ref("ugui"); + const elementType = ref("custom"); + const outputFormat = ref("script"); + const anchorPreset = ref(""); + const colorTheme = ref(""); + const includeAnimations = ref(false); + + // Status / result + const status = ref(null); + const tone = ref<"ok" | "error">("ok"); + const result = ref(null); + const loading = ref(false); + + // Provider/model discovery + const providers = computed(() => store.getProvidersByModality("llm")); + const providerOptions = computed(() => + providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) + ); + const availableModels = computed(() => store.getModelsByProvider(provider.value, "llm")); + const showModelManager = ref(false); + + onMounted(async () => { + try { + await store.load(); + const preferred = store.getPreferredEngine("llm"); + if (!provider.value) provider.value = preferred.provider; + if (!model.value) model.value = preferred.model; + + const dbSysPrompt = store.getPreference("default_code_system_prompt"); + if (dbSysPrompt) { + defaultSystemPrompt.value = `Default: ${dbSysPrompt.substring(0, 50)}...`; + } + } catch (e) { + console.error("Failed to load UnityUIPanel state via store", e); + } + }); + + // Reset model when provider changes and current model is no longer available + watch(provider, () => { + if (provider.value && !availableModels.value.some((m: { value: string }) => m.value === model.value)) { + model.value = ""; + } + }); + + // When element type or UI system changes, pre-fill the prompt with a template + watch([elementType, uiSystem], ([newType, newSystem]) => { + if (newType && newType !== "custom") { + const template = getTemplatePrompt(newType, newSystem as UISystem); + if (template) prompt.value = template; + } + }); + + /** + * Reload available models from the intelligence store. + * Called after the user adds or removes models via the ModelManagerModal. + * + * @returns Promise that resolves when the store has finished reloading. + * + * @example + * ```typescript + * await refreshModels(); + * ``` + */ + async function refreshModels(): Promise { + await store.load(); + } + + const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); + + /** + * Submit the UI generation request to the backend. + * + * Validates the prompt, builds the request payload, calls the API, and + * updates status/result refs based on the response. + * + * @returns Promise that resolves when generation completes or fails. + * + * @example + * ```typescript + * await run(); + * // status.value and result.value are updated + * ``` + */ + async function run(): Promise { + if (!prompt.value.trim()) { + tone.value = "error"; + status.value = "Prompt cannot be empty"; + return; + } + if (loading.value) return; + + loading.value = true; + status.value = "Generating UI element..."; + tone.value = "ok"; + result.value = null; + + try { + const response = await generateUnityUI({ + prompt: prompt.value, + provider: provider.value || undefined, + api_key: apiKey.value || undefined, + system_prompt: systemPrompt.value || undefined, + project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, + ui_system: uiSystem.value, + element_type: elementType.value !== "custom" ? elementType.value : undefined, + output_format: outputFormat.value, + anchor_preset: anchorPreset.value || undefined, + color_theme: colorTheme.value || undefined, + include_animations: includeAnimations.value, + apply_via_mcp: applyViaMcp.value, + options: { + model: model.value || undefined, + temperature: temperature.value, + }, + }); + + if (response.success) { + status.value = "UI element generated successfully."; + result.value = response.data as unknown as AgentResult; + } else { + tone.value = "error"; + status.value = response.error || "Unknown error occurred"; + } + } catch (e) { + tone.value = "error"; + status.value = e instanceof Error ? e.message : String(e); + } finally { + loading.value = false; + } + } + + return { + // Core fields + prompt, + provider, + model, + temperature, + apiKey, + systemPrompt, + defaultSystemPrompt, + // UI-specific fields + uiSystem, + elementType, + outputFormat, + anchorPreset, + colorTheme, + includeAnimations, + mcpAvailable, + editorAvailable, + mcpWarning, + applyViaMcp, + // Provider/model + providers, + providerOptions, + availableModels, + showModelManager, + refreshModels, + // Status / result + status, + tone, + result, + loading, + canGenerate, + run, + // Constants + TEMPERATURE_PRESETS, + UI_QUICK_ACTIONS, + UI_EXAMPLE_PROMPTS, + UI_ELEMENT_TYPES, + }; +} + +export type { UIQuickAction }; diff --git a/frontend/src/components/UnityUIPanel/UnityUIPanel.vue b/frontend/src/components/UnityUIPanel/UnityUIPanel.vue index d99c4921..327ea8da 100644 --- a/frontend/src/components/UnityUIPanel/UnityUIPanel.vue +++ b/frontend/src/components/UnityUIPanel/UnityUIPanel.vue @@ -1,206 +1,219 @@ - - - - - + + + + + diff --git a/frontend/src/components/VideoPanel/VideoPanel.ts b/frontend/src/components/VideoPanel/VideoPanel.ts new file mode 100644 index 00000000..a0471efa --- /dev/null +++ b/frontend/src/components/VideoPanel/VideoPanel.ts @@ -0,0 +1,83 @@ +import { computed, ref, onMounted, watch } from "vue"; +import { generateVideo } from "@/api/client"; +import { DEFAULT_PROJECT_NAME } from "@/api/constants"; +import { useIntelligenceStore } from "@/store/intelligenceStore"; +import { useSessionProject } from "@/composables/useSessionProject"; + +/** + * Composable for the Video generation panel. + */ +export function useVideoPanel() { + const store = useIntelligenceStore(); + const { projectName } = useSessionProject(); + + const prompt = ref(""); + const provider = ref(""); + const duration = ref(5); + const resolution = ref("720p"); + const aspectRatio = ref("16:9"); + const status = ref(null); + const tone = ref<"ok" | "error">("ok"); + const result = ref | null>(null); + const loading = ref(false); + + const providers = computed(() => store.getProvidersByModality("video")); + const providerOptions = computed(() => + providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) + ); + const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); + + onMounted(async () => { + await store.load(); + const preferred = store.getPreferredEngine("video"); + if (!provider.value) provider.value = preferred.provider; + }); + + watch(provider, () => { + if (!provider.value && providerOptions.value.length) { + provider.value = providerOptions.value[0].value; + } + }); + + async function run(): Promise { + if (!prompt.value.trim()) { + tone.value = "error"; + status.value = "Prompt cannot be empty"; + return; + } + loading.value = true; + status.value = "Generating video..."; + tone.value = "ok"; + result.value = null; + + try { + const response = await generateVideo({ + prompt: prompt.value, + provider: provider.value || undefined, + project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, + options: { + duration: duration.value, + resolution: resolution.value, + aspect_ratio: aspectRatio.value, + }, + }); + if (response.success && response.data) { + status.value = "Video request completed."; + result.value = response.data; + } else { + tone.value = "error"; + status.value = response.error || "Unknown error"; + } + } catch (e) { + tone.value = "error"; + status.value = e instanceof Error ? e.message : String(e); + } finally { + loading.value = false; + } + } + + return { + prompt, provider, duration, resolution, aspectRatio, + status, tone, result, loading, canGenerate, run, providerOptions, + }; +} diff --git a/frontend/src/components/VideoPanel/VideoPanel.vue b/frontend/src/components/VideoPanel/VideoPanel.vue new file mode 100644 index 00000000..ede013f7 --- /dev/null +++ b/frontend/src/components/VideoPanel/VideoPanel.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/components/VideoPanel/index.ts b/frontend/src/components/VideoPanel/index.ts new file mode 100644 index 00000000..b0469fbd --- /dev/null +++ b/frontend/src/components/VideoPanel/index.ts @@ -0,0 +1 @@ +export { default as VideoPanel } from "./VideoPanel.vue"; diff --git a/frontend/src/components/WizardPanel/WizardPanel.ts b/frontend/src/components/WizardPanel/WizardPanel.ts new file mode 100644 index 00000000..adc4f825 --- /dev/null +++ b/frontend/src/components/WizardPanel/WizardPanel.ts @@ -0,0 +1,100 @@ +import { computed, ref, onMounted, watch } from "vue"; +import { generateWizard } from "@/api/client"; +import { DEFAULT_PROJECT_NAME } from "@/api/constants"; +import { TEMPERATURE_PRESETS } from "@/constants/providers"; +import { useIntelligenceStore } from "@/store/intelligenceStore"; +import { useSessionProject } from "@/composables/useSessionProject"; +import { useUnityMcpStatus } from "@/composables/useUnityMcpStatus"; + +export const WIZARD_QUICK_ACTIONS = [ + { label: "FPS Prototype", icon: "mdi-target", prompt: "Create a playable FPS prototype: scene, player, camera, input, and basic UI." }, + { label: "Platformer Level", icon: "mdi-layers", prompt: "Build a platformer level with ground, platforms, player spawn, lighting, and collectibles." }, + { label: "UI + Menu", icon: "mdi-menu", prompt: "Create a main menu scene with canvas UI, buttons, and a playable start flow." }, +]; + +/** + * Composable for the Unity Wizard orchestration panel. + */ +export function useWizardPanel() { + const store = useIntelligenceStore(); + const { projectName } = useSessionProject(); + const { available: mcpAvailable, warning: mcpWarning } = useUnityMcpStatus(); + + const prompt = ref(""); + const provider = ref(""); + const model = ref(""); + const temperature = ref(0.7); + const apiKey = ref(""); + const systemPrompt = ref(""); + const status = ref(null); + const tone = ref<"ok" | "error">("ok"); + const result = ref | null>(null); + const loading = ref(false); + const showModelManager = ref(false); + + const providers = computed(() => store.getProvidersByModality("llm")); + const providerOptions = computed(() => + providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) + ); + const availableModels = computed(() => store.getModelsByProvider(provider.value, "llm")); + const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); + + onMounted(async () => { + await store.load(); + const preferred = store.getPreferredEngine("llm"); + if (!provider.value) provider.value = preferred.provider; + if (!model.value) model.value = preferred.model; + }); + + watch(provider, () => { + if (provider.value && !availableModels.value.some((m: { value: string }) => m.value === model.value)) { + model.value = ""; + } + }); + + async function refreshModels(): Promise { + await store.load(true); + } + + async function run(): Promise { + if (!prompt.value.trim()) { + tone.value = "error"; + status.value = "Prompt cannot be empty"; + return; + } + loading.value = true; + status.value = "Running wizard orchestration..."; + tone.value = "ok"; + result.value = null; + + try { + const response = await generateWizard({ + prompt: prompt.value, + provider: provider.value || undefined, + api_key: apiKey.value || undefined, + system_prompt: systemPrompt.value || undefined, + project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, + options: { model: model.value || undefined, temperature: temperature.value, max_tokens: 8000 }, + }); + if (response.success && response.data) { + status.value = "Wizard run completed."; + result.value = response.data; + } else { + tone.value = "error"; + status.value = response.error || "Unknown error"; + } + } catch (e) { + tone.value = "error"; + status.value = e instanceof Error ? e.message : String(e); + } finally { + loading.value = false; + } + } + + return { + prompt, provider, model, temperature, apiKey, systemPrompt, + status, tone, result, loading, canGenerate, run, + mcpAvailable, mcpWarning, providerOptions, availableModels, + showModelManager, refreshModels, TEMPERATURE_PRESETS, WIZARD_QUICK_ACTIONS, + }; +} diff --git a/frontend/src/components/WizardPanel/WizardPanel.vue b/frontend/src/components/WizardPanel/WizardPanel.vue new file mode 100644 index 00000000..7fbbf2eb --- /dev/null +++ b/frontend/src/components/WizardPanel/WizardPanel.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/frontend/src/components/WizardPanel/index.ts b/frontend/src/components/WizardPanel/index.ts new file mode 100644 index 00000000..4b37ebb2 --- /dev/null +++ b/frontend/src/components/WizardPanel/index.ts @@ -0,0 +1,2 @@ +export { default as WizardPanel } from "./WizardPanel.vue"; +export { useWizardPanel } from "./WizardPanel"; diff --git a/frontend/src/components/generic/PromptInputSection/PromptInputSection.vue b/frontend/src/components/generic/PromptInputSection/PromptInputSection.vue index ccc9f66a..bbb02c41 100644 --- a/frontend/src/components/generic/PromptInputSection/PromptInputSection.vue +++ b/frontend/src/components/generic/PromptInputSection/PromptInputSection.vue @@ -1,44 +1,75 @@ - - - - - + + + + + diff --git a/frontend/src/composables/usePromptAssistWand.ts b/frontend/src/composables/usePromptAssistWand.ts new file mode 100644 index 00000000..ed9c35df --- /dev/null +++ b/frontend/src/composables/usePromptAssistWand.ts @@ -0,0 +1,42 @@ +import { ref } from "vue"; +import { promptAssist } from "@/api/client"; +import { useIntelligenceStore } from "@/store/intelligenceStore"; + +/** + * Prompt assist wand actions (enhance / sample) without MCP tools. + */ +export function usePromptAssistWand(onUpdate: (text: string) => void) { + const loading = ref(false); + const error = ref(null); + const store = useIntelligenceStore(); + + async function run(mode: "enhance" | "sample", prompt: string): Promise { + if (!prompt.trim()) { + error.value = "Prompt cannot be empty"; + return; + } + loading.value = true; + error.value = null; + try { + await store.load(); + const preferred = store.getPreferredEngine("llm"); + const response = await promptAssist({ + prompt, + mode, + provider: preferred.provider || undefined, + options: { model: preferred.model || undefined }, + }); + if (response.success && response.data?.content) { + onUpdate(String(response.data.content)); + } else { + error.value = response.error || "Prompt assist failed"; + } + } catch (e) { + error.value = e instanceof Error ? e.message : String(e); + } finally { + loading.value = false; + } + } + + return { loading, error, enhance: (prompt: string) => run("enhance", prompt), sample: (prompt: string) => run("sample", prompt) }; +} diff --git a/frontend/src/composables/useUnityMcpModalityPanel.ts b/frontend/src/composables/useUnityMcpModalityPanel.ts new file mode 100644 index 00000000..d9b608e8 --- /dev/null +++ b/frontend/src/composables/useUnityMcpModalityPanel.ts @@ -0,0 +1,144 @@ +import { computed, ref, onMounted, watch } from "vue"; +import { DEFAULT_PROJECT_NAME } from "@/api/constants"; +import { TEMPERATURE_PRESETS } from "@/constants/providers"; +import { useIntelligenceStore } from "@/store/intelligenceStore"; +import { useSessionProject } from "@/composables/useSessionProject"; +import { useUnityMcpStatus } from "@/composables/useUnityMcpStatus"; +import type { UnityMcpModalityConfig, UnityModalityQuickAction } from "@/constants/unityMcpModalityConfigs"; + +export interface ModalityAgentResult { + content: string; + files?: string[]; + metadata?: Record; +} + +/** + * Shared composable for Unity MCP modality panels (lighting, camera, materials, animation). + * + * @param config - Modality configuration with endpoint and quick actions. + * @returns Reactive panel state and run handler. + */ +export function useUnityMcpModalityPanel(config: UnityMcpModalityConfig) { + const store = useIntelligenceStore(); + const { projectName } = useSessionProject(); + const { available: mcpAvailable, editorAvailable, warning: mcpWarning, applyViaMcp } = useUnityMcpStatus(); + + const prompt = ref(""); + const provider = ref(""); + const model = ref(""); + const temperature = ref(0.7); + const apiKey = ref(""); + const systemPrompt = ref(""); + const defaultSystemPrompt = ref("Default Unity MCP assistant prompt."); + const status = ref(null); + const tone = ref<"ok" | "error">("ok"); + const result = ref(null); + const loading = ref(false); + const showModelManager = ref(false); + + const providers = computed(() => store.getProvidersByModality("llm")); + const providerOptions = computed(() => + providers.value.map((p: { name: string }) => ({ value: p.name, label: p.name })) + ); + const availableModels = computed(() => store.getModelsByProvider(provider.value, "llm")); + const canGenerate = computed(() => prompt.value.trim().length > 0 && !loading.value); + + onMounted(async () => { + try { + await store.load(); + const preferred = store.getPreferredEngine("llm"); + if (!provider.value) provider.value = preferred.provider; + if (!model.value) model.value = preferred.model; + if (config.systemPromptPrefKey) { + const dbPrompt = store.getPreference(config.systemPromptPrefKey); + if (dbPrompt) defaultSystemPrompt.value = `Default: ${dbPrompt.substring(0, 60)}...`; + } + } catch (e) { + console.error(`Failed to load ${config.modalityKey} panel state`, e); + } + }); + + watch(provider, () => { + if (provider.value && !availableModels.value.some((m: { value: string }) => m.value === model.value)) { + model.value = ""; + } + }); + + async function refreshModels(): Promise { + await store.load(true); + } + + function handleQuickActionClick(action: UnityModalityQuickAction): void { + prompt.value = action.prompt; + } + + async function run(): Promise { + if (!prompt.value.trim()) { + tone.value = "error"; + status.value = "Prompt cannot be empty"; + return; + } + if (loading.value) return; + + loading.value = true; + status.value = "Generating..."; + tone.value = "ok"; + result.value = null; + + try { + const response = await config.generate({ + prompt: prompt.value, + provider: provider.value || undefined, + api_key: apiKey.value || undefined, + system_prompt: systemPrompt.value || undefined, + project_name: projectName.value?.trim() || DEFAULT_PROJECT_NAME, + apply_via_mcp: applyViaMcp.value, + options: { + model: model.value || undefined, + temperature: temperature.value, + }, + }); + + if (response.success && response.data) { + status.value = "Generation completed."; + result.value = response.data as unknown as ModalityAgentResult; + } else { + tone.value = "error"; + status.value = response.error || "Unknown error occurred"; + } + } catch (e) { + tone.value = "error"; + status.value = e instanceof Error ? e.message : String(e); + } finally { + loading.value = false; + } + } + + return { + config, + prompt, + provider, + model, + temperature, + apiKey, + systemPrompt, + defaultSystemPrompt, + mcpAvailable, + editorAvailable, + mcpWarning, + applyViaMcp, + providers, + providerOptions, + availableModels, + showModelManager, + refreshModels, + status, + tone, + result, + loading, + canGenerate, + run, + handleQuickActionClick, + TEMPERATURE_PRESETS, + }; +} diff --git a/frontend/src/composables/useUnityMcpStatus.ts b/frontend/src/composables/useUnityMcpStatus.ts new file mode 100644 index 00000000..e7263ac1 --- /dev/null +++ b/frontend/src/composables/useUnityMcpStatus.ts @@ -0,0 +1,51 @@ +import { onMounted, ref } from "vue"; +import { getUnityMcpStatus } from "@/api/client"; + +/** + * Load Unity MCP availability for optional Apply-via-MCP panel actions. + * + * @returns Reactive MCP status plus an Apply-via-MCP checkbox ref. + * + * @example + * ```typescript + * const { available, editorAvailable, warning, applyViaMcp } = useUnityMcpStatus(); + * ``` + */ +export function useUnityMcpStatus() { + const available = ref(false); + const editorAvailable = ref(false); + const warning = ref(null); + const applyViaMcp = ref(false); + + /** + * Refresh MCP status from GET /api/unity-mcp/status. + * + * @returns Promise that resolves when status has been updated. + */ + async function refresh(): Promise { + try { + const response = await getUnityMcpStatus(); + const data = (response.data || {}) as Record; + available.value = Boolean(data.available); + editorAvailable.value = Boolean(data.editor_available); + warning.value = + typeof data.editor_gated_warning === "string" ? data.editor_gated_warning : null; + } catch { + available.value = false; + editorAvailable.value = false; + warning.value = "Unity MCP server is not reachable."; + } + } + + onMounted(() => { + void refresh(); + }); + + return { + available, + editorAvailable, + warning, + applyViaMcp, + refresh, + }; +} diff --git a/frontend/src/constants/unityMcpModalityConfigs.ts b/frontend/src/constants/unityMcpModalityConfigs.ts new file mode 100644 index 00000000..63300265 --- /dev/null +++ b/frontend/src/constants/unityMcpModalityConfigs.ts @@ -0,0 +1,82 @@ +import type { GenerationResponse } from "@/api/client"; +import { + generateUnityAnimation, + generateUnityCamera, + generateUnityLighting, + generateUnityMaterials, +} from "@/api/client"; + +export type UnityModalityQuickAction = { + label: string; + icon: string; + prompt: string; +}; + +export type UnityMcpModalityConfig = { + modalityKey: string; + i18nPrefix: string; + generate: (body: Record) => Promise; + quickActions: UnityModalityQuickAction[]; + examplePrompts: { text: string }[]; + systemPromptPrefKey?: string; +}; + +export const LIGHTING_CONFIG: UnityMcpModalityConfig = { + modalityKey: "unity-lighting", + i18nPrefix: "unityLighting", + generate: (body) => generateUnityLighting(body), + quickActions: [ + { label: "Sun + Ambient", icon: "mdi-white-balance-sunny", prompt: "Add a directional sun light with warm color and soft shadows for an outdoor scene." }, + { label: "Indoor Point", icon: "mdi-lightbulb-on", prompt: "Create point lights for a cozy indoor room with warm temperature." }, + { label: "Spotlight Stage", icon: "mdi-spotlight", prompt: "Add a spotlight rig for a small stage with rim and key lights." }, + ], + examplePrompts: [ + { text: "Set up a three-point lighting rig for a character showcase scene." }, + { text: "Create moonlit night lighting with cool directional light and low ambient." }, + ], +}; + +export const CAMERA_CONFIG: UnityMcpModalityConfig = { + modalityKey: "unity-camera", + i18nPrefix: "unityCamera", + generate: (body) => generateUnityCamera(body), + quickActions: [ + { label: "Third Person", icon: "mdi-camera", prompt: "Create a third-person follow camera with smooth damping behind the player." }, + { label: "Top Down", icon: "mdi-arrow-down-bold", prompt: "Add an orthographic top-down camera for a strategy game." }, + { label: "Cinematic", icon: "mdi-movie-open", prompt: "Configure a cinematic perspective camera with letterbox-friendly FOV." }, + ], + examplePrompts: [ + { text: "Set up a main camera with post-processing friendly clip planes for a 3D platformer." }, + { text: "Create a split-screen compatible camera prefab setup." }, + ], +}; + +export const MATERIALS_CONFIG: UnityMcpModalityConfig = { + modalityKey: "unity-materials", + i18nPrefix: "unityMaterials", + generate: (body) => generateUnityMaterials(body), + quickActions: [ + { label: "PBR Metal", icon: "mdi-gold", prompt: "Create a metallic PBR material with moderate smoothness for armor." }, + { label: "Stylized Toon", icon: "mdi-palette", prompt: "Create a stylized toon material with bold albedo and rim lighting hints." }, + { label: "Glass", icon: "mdi-glass-wine", prompt: "Create a transparent glass material with refraction-friendly settings." }, + ], + examplePrompts: [ + { text: "Generate materials for a low-poly environment: grass, stone, and wood." }, + { text: "Create an emissive sci-fi panel material with pulsing color." }, + ], +}; + +export const ANIMATION_CONFIG: UnityMcpModalityConfig = { + modalityKey: "unity-animation", + i18nPrefix: "unityAnimation", + generate: (body) => generateUnityAnimation(body), + quickActions: [ + { label: "Locomotion", icon: "mdi-run", prompt: "Create a basic animator with Idle, Walk, and Run states and smooth transitions." }, + { label: "Cutscene", icon: "mdi-movie-roll", prompt: "Create a Timeline asset for a short intro cutscene with camera and activation tracks." }, + { label: "Combat", icon: "mdi-sword", prompt: "Create an advanced animator with attack combo blend tree and parameters." }, + ], + examplePrompts: [ + { text: "Build an animator controller for a platformer character with jump and fall states." }, + { text: "Create a timeline sequence that fades in UI and moves the camera." }, + ], +}; diff --git a/frontend/src/i18n/locales/ar.ts b/frontend/src/i18n/locales/ar.ts index 726ced3f..dabe19e7 100644 --- a/frontend/src/i18n/locales/ar.ts +++ b/frontend/src/i18n/locales/ar.ts @@ -1,239 +1,247 @@ -/** - * Arabic (ar) locale. - * Note: Arabic is RTL — the app layout may need dir="rtl" for full support. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "متصل", offline: "غير متصل" }, - nav: { - settings: "الإعدادات", - scenes: "المشاهد", - code: "الكود", - text: "النص", - image: "الصورة", - sprites: "الرسوم المتحركة", - audio: "الصوت", - unityUi: "Unity UI", - unityPhysics: "فيزياء Unity", - unityProject: "مشروع Unity", - }, - actions: { repository: "المستودع" }, - }, - common: { - loading: "جارٍ التحميل…", - saving: "جارٍ الحفظ…", - save: "حفظ", - cancel: "إلغاء", - close: "إغلاق", - ok: "موافق", - yes: "نعم", - no: "لا", - error: "خطأ", - warning: "تحذير", - success: "نجاح", - generate: "توليد", - result: "النتيجة", - provider: "المزود", - model: "النموذج", - temperature: "درجة الحرارة", - apiKey: "مفتاح API (تجاوز اختياري)", - systemPrompt: "تجاوز موجه النظام", - advancedOptions: "خيارات متقدمة", - examplePrompts: "أمثلة على الموجهات", - manageModels: "إدارة النماذج", - selectProvider: "اختر مزوداً", - selectModel: "اختر نموذجاً", - prompt: "الموجه", - leaveEmptyForGlobalKey: "اتركه فارغاً لاستخدام المفتاح العام", - createdFiles: "الملفات المنشأة", - viewSteps: "عرض الخطوات ({n})", - waitingForLogs: "في انتظار السجلات…", - errorsDetected: "تم اكتشاف أخطاء", - }, - settings: { - title: "الإعدادات", - subtitle: "إدارة محركات الذكاء والنماذج وموجهات النظام.", - tabs: { - general: "عام", - providers: "المزودون", - models: "النماذج", - prompts: "الموجهات", - secrets: "الأسرار", - }, - }, - general: { - title: "الإعدادات العامة", - sections: { - networkApi: "الشبكة وAPI", - preferredIntelligence: "الذكاء المفضل", - appearance: "المظهر", - language: "اللغة", - }, - fields: { - backendUrl: "رابط الخادم الخلفي", - backendUrlHint: "عنوان خدمة Unity Generator الخلفية", - outputBasePath: "المسار الأساسي (الإخراج)", - outputBasePathHint: "مكان حفظ الملفات المولّدة", - unityEditorPath: "مسار محرر Unity (اختياري)", - unityEditorPathHint: "المسار الكامل لملف تنفيذ محرر Unity", - preferredLlm: "مزود النص/المنطق الافتراضي", - preferredLlmModel: "نموذج النص/المنطق الافتراضي", - preferredImage: "مزود توليد الصور الافتراضي", - preferredImageModel: "نموذج توليد الصور الافتراضي", - preferredAudio: "مزود الصوت (TTS) الافتراضي", - preferredAudioModel: "نموذج الصوت (TTS) الافتراضي", - preferredMusic: "مزود الموسيقى الافتراضي", - preferredMusicModel: "نموذج الموسيقى الافتراضي", - theme: "السمة", - language: "لغة الواجهة", - }, - actions: { saveAll: "حفظ جميع التغييرات" }, - status: { - saving: "جارٍ الحفظ…", - saved: "تم حفظ الإعدادات بنجاح.", - modelNotRegistered: 'النموذج "{model}" غير مسجّل للمزود "{provider}".', - networkError: "خطأ في الشبكة: تعذّر الاتصال بالخادم الخلفي.", - }, - }, - code: { - title: "كود C# لـ Unity", - subtitle: "توليد سكريبتات C# لـ Unity باستخدام الذكاء الاصطناعي.", - fields: { prompt: "الموجه", maxTokens: "الحد الأقصى للرموز" }, - actions: { generate: "توليد الكود" }, - activeProject: "المشروع النشط: {name}", - autoSave: "الحفظ التلقائي في المشروع", - }, - text: { - title: "توليد النص", - subtitle: "توليد محتوى نصي باستخدام الذكاء الاصطناعي.", - fields: { prompt: "الموجه", length: "الطول" }, - actions: { generate: "توليد النص" }, - }, - image: { - title: "توليد الصور", - subtitle: "توليد صور باستخدام الذكاء الاصطناعي.", - fields: { - prompt: "الموجه", - aspectRatio: "نسبة العرض إلى الارتفاع", - quality: "الجودة", - textureName: "اسم النسيج", - textureType: "نوع النسيج", - }, - actions: { generate: "توليد الصورة", saveToUnity: "حفظ في Unity" }, - }, - sprites: { - title: "الرسوم المتحركة 2D", - badge: "محسّن لفن البكسل", - subtitle: "توليد رسوم متحركة 2D لـ Unity.", - fields: { - prompt: "الموجه", - resolution: "الدقة", - paletteSize: "حجم لوحة الألوان", - autoCrop: "قص الحواف الشفافة تلقائياً", - colors: "{n} ألوان", - }, - preview: "منطقة المعاينة", - actions: { generate: "توليد الرسوم المتحركة" }, - }, - audio: { - title: "توليد الصوت", - subtitle: "توليد أصول صوتية باستخدام الذكاء الاصطناعي.", - generationType: "نوع التوليد", - speechTts: "الكلام (TTS)", - atmosphericMusic: "موسيقى جوية", - fields: { - prompt: "الموجه", - musicDescription: "وصف موسيقي", - speechPrompt: "موجه الكلام", - voiceOptional: "الصوت (اختياري)", - musicModel: "نموذج الموسيقى", - stability: "الاستقرار", - audioClipName: "اسم مقطع الصوت", - audioFormat: "تنسيق الصوت", - }, - actions: { generate: "توليد الصوت", saveToUnity: "حفظ في Unity" }, - }, - scenes: { - title: "منشئ المشاهد", - subtitle: "توليد مشاهد Unity باستخدام الذكاء الاصطناعي.", - fields: { prompt: "وصف المشهد" }, - actions: { generate: "إنشاء مشهد" }, - mediaReady: "الوسائط جاهزة للاستيراد", - mediaReadyText: "{type} \"{name}\" جاهز للاستيراد إلى Unity. راجع الموجه وانقر على \"إنشاء مشهد\" للمتابعة.", - mediaTypeImage: "صورة", - mediaTypeAudio: "صوت", - }, - unityUi: { - title: "Unity UI", - subtitle: "توليد مكونات واجهة مستخدم Unity باستخدام الذكاء الاصطناعي.", - fields: { - uiSystem: "نظام واجهة المستخدم", - elementType: "نوع العنصر", - prompt: "وصف عنصر واجهة المستخدم", - outputFormat: "تنسيق الإخراج", - anchorPreset: "إعداد الارتساء المسبق", - colourTheme: "سمة اللون (اختياري)", - includeAnimations: "تضمين الرسوم المتحركة", - }, - actions: { generate: "توليد واجهة المستخدم" }, - }, - unityPhysics: { - title: "فيزياء Unity", - subtitle: "توليد إعدادات فيزياء Unity — الأجسام الصلبة، المصادمات، الجاذبية والمزيد.", - fields: { - physicsBackend: "محرك الفيزياء", - simulationMode: "وضع المحاكاة", - gravityPreset: "إعداد الجاذبية المسبق", - prompt: "وصف الفيزياء", - includeRigidbody: "تضمين إعداد الجسم الصلب", - includeColliders: "تضمين إعداد المصادمات", - includeLayers: "تضمين طبقات الفيزياء", - }, - actions: { generate: "توليد إعدادات الفيزياء" }, - quickActions: "إجراءات سريعة", - }, - unityProject: { - title: "مشروع Unity", - subtitle: "توليد هيكل مشروع Unity كامل.", - engineSettings: "إعدادات محرك Unity", - fields: { - projectName: "اسم المشروع", - unityTemplate: "قالب Unity", - unityVersion: "إصدار Unity", - targetPlatform: "المنصة المستهدفة", - generateDefaultScene: "توليد المشهد الافتراضي", - autoInstallPackages: "تثبيت حزم UPM تلقائياً", - setupUrp: "إعداد URP", - upmPackages: "حزم UPM (مفصولة بفواصل)", - sceneName: "اسم المشهد", - unityEditorPath: "مسار محرر Unity (اختياري)", - timeout: "المهلة (ثوانٍ)", - resultJson: "النتيجة (JSON)", - }, - actions: { - generate: "توليد المشروع", - finalize: "إنهاء المشروع", - openFolder: "فتح مجلد الإخراج", - addVersion: "إضافة إصدار Unity", - downloadZip: "تنزيل المشروع المكتمل (.zip)", - }, - dialogs: { - addVersion: { - title: "إضافة إصدار Unity", - versionId: "معرّف الإصدار", - label: "التسمية (اختياري)", - }, - }, - }, - theme: { light: "فاتح", dark: "داكن", system: "النظام" }, - languages: { - en: "الإنجليزية", es: "الإسبانية", ca: "الكاتالانية", eu: "الباسكية", - oc: "الأوكسيتانية", uk: "الأوكرانية", pt: "البرتغالية", gl: "الجاليكية", - fr: "الفرنسية", it: "الإيطالية", pl: "البولندية", zh: "الصينية", - ar: "العربية", de: "الألمانية", hi: "الهندية", bn: "البنغالية", - ur: "الأردية", id: "الإندونيسية", ja: "اليابانية", vi: "الفيتنامية", - ko: "الكورية", - tr: "التركية", - }, -} as const; +/** + * Arabic (ar) locale. + * Note: Arabic is RTL — the app layout may need dir="rtl" for full support. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "متصل", offline: "غير متصل" }, + nav: { + settings: "الإعدادات", + scenes: "المشاهد", + code: "الكود", + text: "النص", + image: "الصورة", + sprites: "الرسوم المتحركة", + audio: "الصوت", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "فيزياء Unity", + unityProject: "مشروع Unity", + logs: "Logs", + }, + actions: { repository: "المستودع" }, + }, + common: { + loading: "جارٍ التحميل…", + saving: "جارٍ الحفظ…", + save: "حفظ", + cancel: "إلغاء", + close: "إغلاق", + ok: "موافق", + yes: "نعم", + no: "لا", + error: "خطأ", + warning: "تحذير", + success: "نجاح", + generate: "توليد", + result: "النتيجة", + provider: "المزود", + model: "النموذج", + temperature: "درجة الحرارة", + apiKey: "مفتاح API (تجاوز اختياري)", + systemPrompt: "تجاوز موجه النظام", + advancedOptions: "خيارات متقدمة", + examplePrompts: "أمثلة على الموجهات", + manageModels: "إدارة النماذج", + selectProvider: "اختر مزوداً", + selectModel: "اختر نموذجاً", + prompt: "الموجه", + leaveEmptyForGlobalKey: "اتركه فارغاً لاستخدام المفتاح العام", + createdFiles: "الملفات المنشأة", + viewSteps: "عرض الخطوات ({n})", + waitingForLogs: "في انتظار السجلات…", + errorsDetected: "تم اكتشاف أخطاء", + }, + settings: { + title: "الإعدادات", + subtitle: "إدارة محركات الذكاء والنماذج وموجهات النظام.", + tabs: { + general: "عام", + providers: "المزودون", + models: "النماذج", + prompts: "الموجهات", + secrets: "الأسرار", + }, + }, + general: { + title: "الإعدادات العامة", + sections: { + networkApi: "الشبكة وAPI", + preferredIntelligence: "الذكاء المفضل", + appearance: "المظهر", + language: "اللغة", + }, + fields: { + backendUrl: "رابط الخادم الخلفي", + backendUrlHint: "عنوان خدمة Unity Generator الخلفية", + outputBasePath: "المسار الأساسي (الإخراج)", + outputBasePathHint: "مكان حفظ الملفات المولّدة", + unityEditorPath: "مسار محرر Unity (اختياري)", + unityEditorPathHint: "المسار الكامل لملف تنفيذ محرر Unity", + preferredLlm: "مزود النص/المنطق الافتراضي", + preferredLlmModel: "نموذج النص/المنطق الافتراضي", + preferredImage: "مزود توليد الصور الافتراضي", + preferredImageModel: "نموذج توليد الصور الافتراضي", + preferredAudio: "مزود الصوت (TTS) الافتراضي", + preferredAudioModel: "نموذج الصوت (TTS) الافتراضي", + preferredMusic: "مزود الموسيقى الافتراضي", + preferredMusicModel: "نموذج الموسيقى الافتراضي", + theme: "السمة", + language: "لغة الواجهة", + }, + actions: { saveAll: "حفظ جميع التغييرات" }, + status: { + saving: "جارٍ الحفظ…", + saved: "تم حفظ الإعدادات بنجاح.", + modelNotRegistered: 'النموذج "{model}" غير مسجّل للمزود "{provider}".', + networkError: "خطأ في الشبكة: تعذّر الاتصال بالخادم الخلفي.", + }, + }, + code: { + title: "كود C# لـ Unity", + subtitle: "توليد سكريبتات C# لـ Unity باستخدام الذكاء الاصطناعي.", + fields: { prompt: "الموجه", maxTokens: "الحد الأقصى للرموز" }, + actions: { generate: "توليد الكود" }, + activeProject: "المشروع النشط: {name}", + autoSave: "الحفظ التلقائي في المشروع", + }, + text: { + title: "توليد النص", + subtitle: "توليد محتوى نصي باستخدام الذكاء الاصطناعي.", + fields: { prompt: "الموجه", length: "الطول" }, + actions: { generate: "توليد النص" }, + }, + image: { + title: "توليد الصور", + subtitle: "توليد صور باستخدام الذكاء الاصطناعي.", + fields: { + prompt: "الموجه", + aspectRatio: "نسبة العرض إلى الارتفاع", + quality: "الجودة", + textureName: "اسم النسيج", + textureType: "نوع النسيج", + }, + actions: { generate: "توليد الصورة", saveToUnity: "حفظ في Unity" }, + }, + sprites: { + title: "الرسوم المتحركة 2D", + badge: "محسّن لفن البكسل", + subtitle: "توليد رسوم متحركة 2D لـ Unity.", + fields: { + prompt: "الموجه", + resolution: "الدقة", + paletteSize: "حجم لوحة الألوان", + autoCrop: "قص الحواف الشفافة تلقائياً", + colors: "{n} ألوان", + }, + preview: "منطقة المعاينة", + actions: { generate: "توليد الرسوم المتحركة" }, + }, + audio: { + title: "توليد الصوت", + subtitle: "توليد أصول صوتية باستخدام الذكاء الاصطناعي.", + generationType: "نوع التوليد", + speechTts: "الكلام (TTS)", + atmosphericMusic: "موسيقى جوية", + fields: { + prompt: "الموجه", + musicDescription: "وصف موسيقي", + speechPrompt: "موجه الكلام", + voiceOptional: "الصوت (اختياري)", + musicModel: "نموذج الموسيقى", + stability: "الاستقرار", + audioClipName: "اسم مقطع الصوت", + audioFormat: "تنسيق الصوت", + }, + actions: { generate: "توليد الصوت", saveToUnity: "حفظ في Unity" }, + }, + scenes: { + title: "منشئ المشاهد", + subtitle: "توليد مشاهد Unity باستخدام الذكاء الاصطناعي.", + fields: { prompt: "وصف المشهد" }, + actions: { generate: "إنشاء مشهد" }, + mediaReady: "الوسائط جاهزة للاستيراد", + mediaReadyText: "{type} \"{name}\" جاهز للاستيراد إلى Unity. راجع الموجه وانقر على \"إنشاء مشهد\" للمتابعة.", + mediaTypeImage: "صورة", + mediaTypeAudio: "صوت", + }, + unityUi: { + title: "Unity UI", + subtitle: "توليد مكونات واجهة مستخدم Unity باستخدام الذكاء الاصطناعي.", + fields: { + uiSystem: "نظام واجهة المستخدم", + elementType: "نوع العنصر", + prompt: "وصف عنصر واجهة المستخدم", + outputFormat: "تنسيق الإخراج", + anchorPreset: "إعداد الارتساء المسبق", + colourTheme: "سمة اللون (اختياري)", + includeAnimations: "تضمين الرسوم المتحركة", + }, + actions: { generate: "توليد واجهة المستخدم" }, + }, + unityPhysics: { + title: "فيزياء Unity", + subtitle: "توليد إعدادات فيزياء Unity — الأجسام الصلبة، المصادمات، الجاذبية والمزيد.", + fields: { + physicsBackend: "محرك الفيزياء", + simulationMode: "وضع المحاكاة", + gravityPreset: "إعداد الجاذبية المسبق", + prompt: "وصف الفيزياء", + includeRigidbody: "تضمين إعداد الجسم الصلب", + includeColliders: "تضمين إعداد المصادمات", + includeLayers: "تضمين طبقات الفيزياء", + }, + actions: { generate: "توليد إعدادات الفيزياء" }, + quickActions: "إجراءات سريعة", + }, + unityProject: { + title: "مشروع Unity", + subtitle: "توليد هيكل مشروع Unity كامل.", + engineSettings: "إعدادات محرك Unity", + fields: { + projectName: "اسم المشروع", + unityTemplate: "قالب Unity", + unityVersion: "إصدار Unity", + targetPlatform: "المنصة المستهدفة", + generateDefaultScene: "توليد المشهد الافتراضي", + autoInstallPackages: "تثبيت حزم UPM تلقائياً", + setupUrp: "إعداد URP", + upmPackages: "حزم UPM (مفصولة بفواصل)", + sceneName: "اسم المشهد", + unityEditorPath: "مسار محرر Unity (اختياري)", + timeout: "المهلة (ثوانٍ)", + resultJson: "النتيجة (JSON)", + }, + actions: { + generate: "توليد المشروع", + finalize: "إنهاء المشروع", + openFolder: "فتح مجلد الإخراج", + addVersion: "إضافة إصدار Unity", + downloadZip: "تنزيل المشروع المكتمل (.zip)", + }, + dialogs: { + addVersion: { + title: "إضافة إصدار Unity", + versionId: "معرّف الإصدار", + label: "التسمية (اختياري)", + }, + }, + }, + theme: { light: "فاتح", dark: "داكن", system: "النظام" }, + languages: { + en: "الإنجليزية", es: "الإسبانية", ca: "الكاتالانية", eu: "الباسكية", + oc: "الأوكسيتانية", uk: "الأوكرانية", pt: "البرتغالية", gl: "الجاليكية", + fr: "الفرنسية", it: "الإيطالية", pl: "البولندية", zh: "الصينية", + ar: "العربية", de: "الألمانية", hi: "الهندية", bn: "البنغالية", + ur: "الأردية", id: "الإندونيسية", ja: "اليابانية", vi: "الفيتنامية", + ko: "الكورية", + tr: "التركية", + }, +} as const; diff --git a/frontend/src/i18n/locales/bn.ts b/frontend/src/i18n/locales/bn.ts index e886242b..7017549e 100644 --- a/frontend/src/i18n/locales/bn.ts +++ b/frontend/src/i18n/locales/bn.ts @@ -1,112 +1,120 @@ -/** - * Bengali (bn) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "অনলাইন", offline: "অফলাইন" }, - nav: { - settings: "সেটিংস", scenes: "দৃশ্য", code: "কোড", text: "পাঠ্য", - image: "ছবি", sprites: "স্প্রাইট", audio: "অডিও", - unityUi: "Unity UI", unityPhysics: "Unity পদার্থবিজ্ঞান", unityProject: "Unity প্রকল্প", - }, - actions: { repository: "রিপোজিটরি" }, - }, - common: { - loading: "লোড হচ্ছে…", saving: "সংরক্ষণ হচ্ছে…", save: "সংরক্ষণ করুন", - cancel: "বাতিল করুন", close: "বন্ধ করুন", ok: "ঠিক আছে", yes: "হ্যাঁ", no: "না", - error: "ত্রুটি", warning: "সতর্কতা", success: "সাফল্য", generate: "তৈরি করুন", - result: "ফলাফল", provider: "প্রদানকারী", model: "মডেল", temperature: "তাপমাত্রা", - apiKey: "API কী (ঐচ্ছিক ওভাররাইড)", systemPrompt: "সিস্টেম প্রম্পট ওভাররাইড", - advancedOptions: "উন্নত বিকল্প", examplePrompts: "উদাহরণ প্রম্পট", - manageModels: "মডেল পরিচালনা করুন", selectProvider: "প্রদানকারী নির্বাচন করুন", - selectModel: "মডেল নির্বাচন করুন", prompt: "প্রম্পট", - leaveEmptyForGlobalKey: "গ্লোবাল কী ব্যবহারের জন্য খালি রাখুন", - createdFiles: "তৈরি ফাইল", viewSteps: "ধাপ দেখুন ({n})", - waitingForLogs: "লগের জন্য অপেক্ষা করছে…", errorsDetected: "ত্রুটি পাওয়া গেছে", - }, - settings: { - title: "কনফিগারেশন", - subtitle: "আপনার ইন্টেলিজেন্স ইঞ্জিন, মডেল এবং সিস্টেম প্রম্পট পরিচালনা করুন।", - tabs: { general: "সাধারণ", providers: "প্রদানকারী", models: "মডেল", prompts: "প্রম্পট", secrets: "গোপন" }, - }, - general: { - title: "সাধারণ পছন্দ", - sections: { networkApi: "নেটওয়ার্ক ও API", preferredIntelligence: "পছন্দের ইন্টেলিজেন্স", appearance: "চেহারা", language: "ভাষা" }, - fields: { - backendUrl: "ব্যাকএন্ড URL", backendUrlHint: "আপনার Unity Generator ব্যাকএন্ড সেবার ঠিকানা", - outputBasePath: "বেস পাথ (আউটপুট)", outputBasePathHint: "তৈরি ফাইল কোথায় সংরক্ষিত হয়", - unityEditorPath: "Unity এডিটর পাথ (ঐচ্ছিক)", unityEditorPathHint: "Unity এডিটর এক্সিকিউটেবলের সম্পূর্ণ পাথ", - preferredLlm: "ডিফল্ট টেক্সট/লজিক প্রদানকারী", preferredLlmModel: "ডিফল্ট টেক্সট/লজিক মডেল", - preferredImage: "ডিফল্ট ইমেজ জেনারেশন প্রদানকারী", preferredImageModel: "ডিফল্ট ইমেজ জেনারেশন মডেল", - preferredAudio: "ডিফল্ট ভয়েস (TTS) প্রদানকারী", preferredAudioModel: "ডিফল্ট ভয়েস (TTS) মডেল", - preferredMusic: "ডিফল্ট মিউজিক প্রদানকারী", preferredMusicModel: "ডিফল্ট মিউজিক মডেল", - theme: "থিম", language: "ইন্টারফেস ভাষা", - }, - actions: { saveAll: "সব পরিবর্তন সংরক্ষণ করুন" }, - status: { - saving: "সংরক্ষণ হচ্ছে…", saved: "পছন্দ সফলভাবে সংরক্ষিত হয়েছে।", - modelNotRegistered: 'মডেল "{model}" প্রদানকারী "{provider}"-এর জন্য নিবন্ধিত নয়।', - networkError: "নেটওয়ার্ক ত্রুটি: ব্যাকএন্ডে সংযোগ করা যায়নি।", - }, - }, - code: { - title: "Unity C# কোড", subtitle: "AI ব্যবহার করে Unity C# স্ক্রিপ্ট তৈরি করুন।", - fields: { prompt: "প্রম্পট", maxTokens: "সর্বোচ্চ টোকেন" }, - actions: { generate: "কোড তৈরি করুন" }, - activeProject: "সক্রিয় প্রকল্প: {name}", autoSave: "প্রকল্পে স্বয়ংক্রিয়ভাবে সংরক্ষণ করুন", - }, - text: { - title: "টেক্সট জেনারেশন", subtitle: "AI ব্যবহার করে টেক্সট কন্টেন্ট তৈরি করুন।", - fields: { prompt: "প্রম্পট", length: "দৈর্ঘ্য" }, actions: { generate: "টেক্সট তৈরি করুন" }, - }, - image: { - title: "ইমেজ জেনারেশন", subtitle: "AI ব্যবহার করে ইমেজ তৈরি করুন।", - fields: { prompt: "প্রম্পট", aspectRatio: "আসপেক্ট রেশিও", quality: "গুণমান", textureName: "টেক্সচার নাম", textureType: "টেক্সচার ধরন" }, - actions: { generate: "ইমেজ তৈরি করুন", saveToUnity: "Unity-তে সংরক্ষণ করুন" }, - }, - sprites: { - title: "2D স্প্রাইট", badge: "পিক্সেল আর্টের জন্য অপ্টিমাইজড", subtitle: "Unity-র জন্য 2D স্প্রাইট তৈরি করুন।", - fields: { prompt: "প্রম্পট", resolution: "রেজোলিউশন", paletteSize: "প্যালেট আকার", autoCrop: "স্বচ্ছ প্রান্ত স্বয়ংক্রিয়ভাবে কাটুন", colors: "{n} রঙ" }, - preview: "প্রিভিউ এলাকা", actions: { generate: "স্প্রাইট তৈরি করুন" }, - }, - audio: { - title: "অডিও জেনারেশন", subtitle: "AI ব্যবহার করে অডিও রিসোর্স তৈরি করুন।", - generationType: "জেনারেশন ধরন", speechTts: "বক্তৃতা (TTS)", atmosphericMusic: "পরিবেশগত সংগীত", - fields: { prompt: "প্রম্পট", musicDescription: "সংগীত বিবরণ", speechPrompt: "বক্তৃতা প্রম্পট", voiceOptional: "কণ্ঠস্বর (ঐচ্ছিক)", musicModel: "মিউজিক মডেল", stability: "স্থিতিশীলতা", audioClipName: "অডিও ক্লিপ নাম", audioFormat: "অডিও ফরম্যাট" }, - actions: { generate: "অডিও তৈরি করুন", saveToUnity: "Unity-তে সংরক্ষণ করুন" }, - }, - scenes: { - title: "দৃশ্য নির্মাতা", subtitle: "AI ব্যবহার করে Unity দৃশ্য তৈরি করুন।", - fields: { prompt: "দৃশ্য বিবরণ" }, actions: { generate: "দৃশ্য তৈরি করুন" }, - mediaReady: "মিডিয়া আমদানির জন্য প্রস্তুত", - mediaReadyText: "{type} \"{name}\" Unity-তে আমদানির জন্য প্রস্তুত। প্রম্পট পর্যালোচনা করুন এবং চালিয়ে যেতে \"দৃশ্য তৈরি করুন\" ক্লিক করুন।", - mediaTypeImage: "ছবি", mediaTypeAudio: "অডিও", - }, - unityUi: { - title: "Unity UI", subtitle: "AI ব্যবহার করে Unity UI উপাদান তৈরি করুন।", - fields: { uiSystem: "UI সিস্টেম", elementType: "উপাদান ধরন", prompt: "UI উপাদান বিবরণ", outputFormat: "আউটপুট ফরম্যাট", anchorPreset: "অ্যাঙ্কর প্রিসেট", colourTheme: "রঙ থিম (ঐচ্ছিক)", includeAnimations: "অ্যানিমেশন অন্তর্ভুক্ত করুন" }, - actions: { generate: "UI তৈরি করুন" }, - }, - unityPhysics: { - title: "Unity পদার্থবিজ্ঞান", subtitle: "Unity পদার্থবিজ্ঞান কনফিগারেশন তৈরি করুন।", - fields: { physicsBackend: "পদার্থবিজ্ঞান ইঞ্জিন", simulationMode: "সিমুলেশন মোড", gravityPreset: "মাধ্যাকর্ষণ প্রিসেট", prompt: "পদার্থবিজ্ঞান বিবরণ", includeRigidbody: "Rigidbody কনফিগারেশন অন্তর্ভুক্ত করুন", includeColliders: "Collider কনফিগারেশন অন্তর্ভুক্ত করুন", includeLayers: "পদার্থবিজ্ঞান স্তর অন্তর্ভুক্ত করুন" }, - actions: { generate: "পদার্থবিজ্ঞান কনফিগারেশন তৈরি করুন" }, - quickActions: "দ্রুত ক্রিয়া", - }, - unityProject: { - title: "Unity প্রকল্প", subtitle: "সম্পূর্ণ Unity প্রকল্প কাঠামো তৈরি করুন।", - engineSettings: "Unity ইঞ্জিন সেটিংস", - fields: { projectName: "প্রকল্পের নাম", unityTemplate: "Unity টেমপ্লেট", unityVersion: "Unity সংস্করণ", targetPlatform: "লক্ষ্য প্ল্যাটফর্ম", generateDefaultScene: "ডিফল্ট দৃশ্য তৈরি করুন", autoInstallPackages: "UPM প্যাকেজ স্বয়ংক্রিয়ভাবে ইনস্টল করুন", setupUrp: "URP কনফিগার করুন", upmPackages: "UPM প্যাকেজ (কমা দিয়ে আলাদা)", sceneName: "দৃশ্যের নাম", unityEditorPath: "Unity এডিটর পাথ (ঐচ্ছিক)", timeout: "টাইমআউট (সেকেন্ড)", resultJson: "ফলাফল (JSON)" }, - actions: { generate: "প্রকল্প তৈরি করুন", finalize: "প্রকল্প চূড়ান্ত করুন", openFolder: "আউটপুট ফোল্ডার খুলুন", addVersion: "Unity সংস্করণ যোগ করুন", downloadZip: "সম্পূর্ণ প্রকল্প ডাউনলোড করুন (.zip)" }, - dialogs: { addVersion: { title: "Unity সংস্করণ যোগ করুন", versionId: "সংস্করণ ID", label: "লেবেল (ঐচ্ছিক)" } }, - }, - theme: { light: "হালকা", dark: "গাঢ়", system: "সিস্টেম" }, - languages: { - en: "ইংরেজি", es: "স্পেনীয়", ca: "কাতালান", eu: "বাস্ক", oc: "অক্সিটান", uk: "ইউক্রেনীয়", - pt: "পর্তুগিজ", gl: "গ্যালিশিয়ান", fr: "ফরাসি", it: "ইতালীয়", pl: "পোলিশ", zh: "চীনা", - ar: "আরবি", de: "জার্মান", hi: "হিন্দি", bn: "বাংলা", ur: "উর্দু", - id: "ইন্দোনেশিয়ান", ja: "জাপানি", vi: "ভিয়েতনামি", ko: "কোরিয়ান", - tr: "তুর্কি", - }, -} as const; +/** + * Bengali (bn) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "অনলাইন", offline: "অফলাইন" }, + nav: { + settings: "সেটিংস", scenes: "দৃশ্য", code: "কোড", text: "পাঠ্য", + image: "ছবি", sprites: "স্প্রাইট", audio: "অডিও", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", unityPhysics: "Unity পদার্থবিজ্ঞান", unityProject: "Unity প্রকল্প", + logs: "Logs", + }, + actions: { repository: "রিপোজিটরি" }, + }, + common: { + loading: "লোড হচ্ছে…", saving: "সংরক্ষণ হচ্ছে…", save: "সংরক্ষণ করুন", + cancel: "বাতিল করুন", close: "বন্ধ করুন", ok: "ঠিক আছে", yes: "হ্যাঁ", no: "না", + error: "ত্রুটি", warning: "সতর্কতা", success: "সাফল্য", generate: "তৈরি করুন", + result: "ফলাফল", provider: "প্রদানকারী", model: "মডেল", temperature: "তাপমাত্রা", + apiKey: "API কী (ঐচ্ছিক ওভাররাইড)", systemPrompt: "সিস্টেম প্রম্পট ওভাররাইড", + advancedOptions: "উন্নত বিকল্প", examplePrompts: "উদাহরণ প্রম্পট", + manageModels: "মডেল পরিচালনা করুন", selectProvider: "প্রদানকারী নির্বাচন করুন", + selectModel: "মডেল নির্বাচন করুন", prompt: "প্রম্পট", + leaveEmptyForGlobalKey: "গ্লোবাল কী ব্যবহারের জন্য খালি রাখুন", + createdFiles: "তৈরি ফাইল", viewSteps: "ধাপ দেখুন ({n})", + waitingForLogs: "লগের জন্য অপেক্ষা করছে…", errorsDetected: "ত্রুটি পাওয়া গেছে", + }, + settings: { + title: "কনফিগারেশন", + subtitle: "আপনার ইন্টেলিজেন্স ইঞ্জিন, মডেল এবং সিস্টেম প্রম্পট পরিচালনা করুন।", + tabs: { general: "সাধারণ", providers: "প্রদানকারী", models: "মডেল", prompts: "প্রম্পট", secrets: "গোপন" }, + }, + general: { + title: "সাধারণ পছন্দ", + sections: { networkApi: "নেটওয়ার্ক ও API", preferredIntelligence: "পছন্দের ইন্টেলিজেন্স", appearance: "চেহারা", language: "ভাষা" }, + fields: { + backendUrl: "ব্যাকএন্ড URL", backendUrlHint: "আপনার Unity Generator ব্যাকএন্ড সেবার ঠিকানা", + outputBasePath: "বেস পাথ (আউটপুট)", outputBasePathHint: "তৈরি ফাইল কোথায় সংরক্ষিত হয়", + unityEditorPath: "Unity এডিটর পাথ (ঐচ্ছিক)", unityEditorPathHint: "Unity এডিটর এক্সিকিউটেবলের সম্পূর্ণ পাথ", + preferredLlm: "ডিফল্ট টেক্সট/লজিক প্রদানকারী", preferredLlmModel: "ডিফল্ট টেক্সট/লজিক মডেল", + preferredImage: "ডিফল্ট ইমেজ জেনারেশন প্রদানকারী", preferredImageModel: "ডিফল্ট ইমেজ জেনারেশন মডেল", + preferredAudio: "ডিফল্ট ভয়েস (TTS) প্রদানকারী", preferredAudioModel: "ডিফল্ট ভয়েস (TTS) মডেল", + preferredMusic: "ডিফল্ট মিউজিক প্রদানকারী", preferredMusicModel: "ডিফল্ট মিউজিক মডেল", + theme: "থিম", language: "ইন্টারফেস ভাষা", + }, + actions: { saveAll: "সব পরিবর্তন সংরক্ষণ করুন" }, + status: { + saving: "সংরক্ষণ হচ্ছে…", saved: "পছন্দ সফলভাবে সংরক্ষিত হয়েছে।", + modelNotRegistered: 'মডেল "{model}" প্রদানকারী "{provider}"-এর জন্য নিবন্ধিত নয়।', + networkError: "নেটওয়ার্ক ত্রুটি: ব্যাকএন্ডে সংযোগ করা যায়নি।", + }, + }, + code: { + title: "Unity C# কোড", subtitle: "AI ব্যবহার করে Unity C# স্ক্রিপ্ট তৈরি করুন।", + fields: { prompt: "প্রম্পট", maxTokens: "সর্বোচ্চ টোকেন" }, + actions: { generate: "কোড তৈরি করুন" }, + activeProject: "সক্রিয় প্রকল্প: {name}", autoSave: "প্রকল্পে স্বয়ংক্রিয়ভাবে সংরক্ষণ করুন", + }, + text: { + title: "টেক্সট জেনারেশন", subtitle: "AI ব্যবহার করে টেক্সট কন্টেন্ট তৈরি করুন।", + fields: { prompt: "প্রম্পট", length: "দৈর্ঘ্য" }, actions: { generate: "টেক্সট তৈরি করুন" }, + }, + image: { + title: "ইমেজ জেনারেশন", subtitle: "AI ব্যবহার করে ইমেজ তৈরি করুন।", + fields: { prompt: "প্রম্পট", aspectRatio: "আসপেক্ট রেশিও", quality: "গুণমান", textureName: "টেক্সচার নাম", textureType: "টেক্সচার ধরন" }, + actions: { generate: "ইমেজ তৈরি করুন", saveToUnity: "Unity-তে সংরক্ষণ করুন" }, + }, + sprites: { + title: "2D স্প্রাইট", badge: "পিক্সেল আর্টের জন্য অপ্টিমাইজড", subtitle: "Unity-র জন্য 2D স্প্রাইট তৈরি করুন।", + fields: { prompt: "প্রম্পট", resolution: "রেজোলিউশন", paletteSize: "প্যালেট আকার", autoCrop: "স্বচ্ছ প্রান্ত স্বয়ংক্রিয়ভাবে কাটুন", colors: "{n} রঙ" }, + preview: "প্রিভিউ এলাকা", actions: { generate: "স্প্রাইট তৈরি করুন" }, + }, + audio: { + title: "অডিও জেনারেশন", subtitle: "AI ব্যবহার করে অডিও রিসোর্স তৈরি করুন।", + generationType: "জেনারেশন ধরন", speechTts: "বক্তৃতা (TTS)", atmosphericMusic: "পরিবেশগত সংগীত", + fields: { prompt: "প্রম্পট", musicDescription: "সংগীত বিবরণ", speechPrompt: "বক্তৃতা প্রম্পট", voiceOptional: "কণ্ঠস্বর (ঐচ্ছিক)", musicModel: "মিউজিক মডেল", stability: "স্থিতিশীলতা", audioClipName: "অডিও ক্লিপ নাম", audioFormat: "অডিও ফরম্যাট" }, + actions: { generate: "অডিও তৈরি করুন", saveToUnity: "Unity-তে সংরক্ষণ করুন" }, + }, + scenes: { + title: "দৃশ্য নির্মাতা", subtitle: "AI ব্যবহার করে Unity দৃশ্য তৈরি করুন।", + fields: { prompt: "দৃশ্য বিবরণ" }, actions: { generate: "দৃশ্য তৈরি করুন" }, + mediaReady: "মিডিয়া আমদানির জন্য প্রস্তুত", + mediaReadyText: "{type} \"{name}\" Unity-তে আমদানির জন্য প্রস্তুত। প্রম্পট পর্যালোচনা করুন এবং চালিয়ে যেতে \"দৃশ্য তৈরি করুন\" ক্লিক করুন।", + mediaTypeImage: "ছবি", mediaTypeAudio: "অডিও", + }, + unityUi: { + title: "Unity UI", subtitle: "AI ব্যবহার করে Unity UI উপাদান তৈরি করুন।", + fields: { uiSystem: "UI সিস্টেম", elementType: "উপাদান ধরন", prompt: "UI উপাদান বিবরণ", outputFormat: "আউটপুট ফরম্যাট", anchorPreset: "অ্যাঙ্কর প্রিসেট", colourTheme: "রঙ থিম (ঐচ্ছিক)", includeAnimations: "অ্যানিমেশন অন্তর্ভুক্ত করুন" }, + actions: { generate: "UI তৈরি করুন" }, + }, + unityPhysics: { + title: "Unity পদার্থবিজ্ঞান", subtitle: "Unity পদার্থবিজ্ঞান কনফিগারেশন তৈরি করুন।", + fields: { physicsBackend: "পদার্থবিজ্ঞান ইঞ্জিন", simulationMode: "সিমুলেশন মোড", gravityPreset: "মাধ্যাকর্ষণ প্রিসেট", prompt: "পদার্থবিজ্ঞান বিবরণ", includeRigidbody: "Rigidbody কনফিগারেশন অন্তর্ভুক্ত করুন", includeColliders: "Collider কনফিগারেশন অন্তর্ভুক্ত করুন", includeLayers: "পদার্থবিজ্ঞান স্তর অন্তর্ভুক্ত করুন" }, + actions: { generate: "পদার্থবিজ্ঞান কনফিগারেশন তৈরি করুন" }, + quickActions: "দ্রুত ক্রিয়া", + }, + unityProject: { + title: "Unity প্রকল্প", subtitle: "সম্পূর্ণ Unity প্রকল্প কাঠামো তৈরি করুন।", + engineSettings: "Unity ইঞ্জিন সেটিংস", + fields: { projectName: "প্রকল্পের নাম", unityTemplate: "Unity টেমপ্লেট", unityVersion: "Unity সংস্করণ", targetPlatform: "লক্ষ্য প্ল্যাটফর্ম", generateDefaultScene: "ডিফল্ট দৃশ্য তৈরি করুন", autoInstallPackages: "UPM প্যাকেজ স্বয়ংক্রিয়ভাবে ইনস্টল করুন", setupUrp: "URP কনফিগার করুন", upmPackages: "UPM প্যাকেজ (কমা দিয়ে আলাদা)", sceneName: "দৃশ্যের নাম", unityEditorPath: "Unity এডিটর পাথ (ঐচ্ছিক)", timeout: "টাইমআউট (সেকেন্ড)", resultJson: "ফলাফল (JSON)" }, + actions: { generate: "প্রকল্প তৈরি করুন", finalize: "প্রকল্প চূড়ান্ত করুন", openFolder: "আউটপুট ফোল্ডার খুলুন", addVersion: "Unity সংস্করণ যোগ করুন", downloadZip: "সম্পূর্ণ প্রকল্প ডাউনলোড করুন (.zip)" }, + dialogs: { addVersion: { title: "Unity সংস্করণ যোগ করুন", versionId: "সংস্করণ ID", label: "লেবেল (ঐচ্ছিক)" } }, + }, + theme: { light: "হালকা", dark: "গাঢ়", system: "সিস্টেম" }, + languages: { + en: "ইংরেজি", es: "স্পেনীয়", ca: "কাতালান", eu: "বাস্ক", oc: "অক্সিটান", uk: "ইউক্রেনীয়", + pt: "পর্তুগিজ", gl: "গ্যালিশিয়ান", fr: "ফরাসি", it: "ইতালীয়", pl: "পোলিশ", zh: "চীনা", + ar: "আরবি", de: "জার্মান", hi: "হিন্দি", bn: "বাংলা", ur: "উর্দু", + id: "ইন্দোনেশিয়ান", ja: "জাপানি", vi: "ভিয়েতনামি", ko: "কোরিয়ান", + tr: "তুর্কি", + }, +} as const; diff --git a/frontend/src/i18n/locales/ca.ts b/frontend/src/i18n/locales/ca.ts index c4ee0185..f6d64eee 100644 --- a/frontend/src/i18n/locales/ca.ts +++ b/frontend/src/i18n/locales/ca.ts @@ -1,304 +1,312 @@ -/** - * Catalan (ca) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { - online: "En línia", - offline: "Sense connexió", - }, - nav: { - settings: "Configuració", - scenes: "Escenes", - code: "Codi", - text: "Text", - image: "Imatge", - sprites: "Sprites", - audio: "Àudio", - unityUi: "Unity UI", - unityPhysics: "Unity Física", - unityProject: "Projecte Unity", - }, - actions: { - repository: "Repositori", - }, - }, - - common: { - loading: "Carregant…", - saving: "Desant…", - save: "Desa", - cancel: "Cancel·la", - close: "Tanca", - ok: "D'acord", - yes: "Sí", - no: "No", - error: "Error", - warning: "Avís", - success: "Èxit", - generate: "Genera", - result: "Resultat", - provider: "Proveïdor", - model: "Model", - temperature: "Temperatura", - apiKey: "Clau API (substitució opcional)", - systemPrompt: "Substitució del prompt de sistema", - advancedOptions: "Opcions avançades", - examplePrompts: "Exemples de prompts", - manageModels: "Gestiona models", - selectProvider: "Selecciona proveïdor", - selectModel: "Selecciona model", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Deixa buit per usar la clau global", - createdFiles: "Fitxers creats", - viewSteps: "Veure passos ({n})", - waitingForLogs: "Esperant registres…", - errorsDetected: "Errors detectats", - }, - - settings: { - title: "Configuració", - subtitle: "Gestiona els teus motors d'intel·ligència, models i prompts de sistema.", - tabs: { - general: "General", - providers: "Proveïdors", - models: "Models", - prompts: "Prompts", - secrets: "Secrets", - }, - }, - - general: { - title: "Preferències generals", - sections: { - networkApi: "Xarxa i API", - preferredIntelligence: "Intel·ligència preferida", - appearance: "Aparença", - language: "Idioma", - }, - fields: { - backendUrl: "URL del backend", - backendUrlHint: "L'adreça del teu servei backend de Unity Generator", - outputBasePath: "Ruta base (sortida)", - outputBasePathHint: "On es desen els fitxers generats", - unityEditorPath: "Ruta de l'editor Unity (opcional)", - unityEditorPathHint: "Ruta completa a l'executable de l'editor Unity", - preferredLlm: "Proveïdor de text / lògica predeterminat", - preferredLlmModel: "Model de text / lògica predeterminat", - preferredImage: "Proveïdor de generació d'imatges predeterminat", - preferredImageModel: "Model de generació d'imatges predeterminat", - preferredAudio: "Proveïdor de veu (TTS) predeterminat", - preferredAudioModel: "Model de veu (TTS) predeterminat", - preferredMusic: "Proveïdor de música predeterminat", - preferredMusicModel: "Model de música predeterminat", - theme: "Tema", - language: "Idioma de la interfície", - }, - actions: { - saveAll: "Desa tots els canvis", - }, - status: { - saving: "Desant…", - saved: "Preferències desades correctament.", - modelNotRegistered: 'El model "{model}" no està registrat per al proveïdor "{provider}".', - networkError: "Error de xarxa: no es pot connectar amb el backend.", - }, - }, - - code: { - title: "Codi C# per a Unity", - subtitle: "Genera scripts C# per a Unity amb IA.", - fields: { - prompt: "Prompt", - maxTokens: "Tokens màxims", - }, - actions: { - generate: "Genera codi", - }, - activeProject: "Projecte actiu: {name}", - autoSave: "Desa automàticament al projecte", - }, - - text: { - title: "Generació de text", - subtitle: "Genera contingut de text amb IA.", - fields: { - prompt: "Prompt", - length: "Longitud", - }, - actions: { - generate: "Genera text", - }, - }, - - image: { - title: "Generació d'imatges", - subtitle: "Genera imatges amb IA.", - fields: { - prompt: "Prompt", - aspectRatio: "Relació d'aspecte", - quality: "Qualitat", - textureName: "Nom de la textura", - textureType: "Tipus de textura", - }, - actions: { - generate: "Genera imatge", - saveToUnity: "Desa a Unity", - }, - }, - - sprites: { - title: "Sprites 2D", - badge: "Optimitzat per a Pixel Art", - subtitle: "Genera sprites 2D per a Unity.", - fields: { - prompt: "Prompt", - resolution: "Resolució", - paletteSize: "Mida de la paleta", - autoCrop: "Retall automàtic de vores transparents", - colors: "{n} colors", - }, - preview: "Àrea de previsualització", - actions: { - generate: "Genera sprites", - }, - }, - - audio: { - title: "Generació d'àudio", - subtitle: "Genera recursos d'àudio amb IA.", - generationType: "Tipus de generació", - speechTts: "Veu (TTS)", - atmosphericMusic: "Música ambiental", - fields: { - prompt: "Prompt", - musicDescription: "Descripció musical", - speechPrompt: "Prompt de veu", - voiceOptional: "Veu (opcional)", - musicModel: "Model de música", - stability: "Estabilitat", - audioClipName: "Nom del clip d'àudio", - audioFormat: "Format d'àudio", - }, - actions: { - generate: "Genera àudio", - saveToUnity: "Desa a Unity", - }, - }, - - scenes: { - title: "Creador d'escenes", - subtitle: "Genera escenes de Unity amb IA.", - fields: { - prompt: "Descripció de l'escena", - }, - actions: { - generate: "Crea escena", - }, - mediaReady: "Multimèdia llesta per importar", - mediaReadyText: "{type} \"{name}\" està llesta per importar-se a Unity. Revisa el prompt i fes clic a \"Crea escena\" per continuar.", - mediaTypeImage: "Imatge", - mediaTypeAudio: "Àudio", - }, - - unityUi: { - title: "Unity UI", - subtitle: "Genera components d'UI per a Unity amb IA.", - fields: { - uiSystem: "Sistema d'UI", - elementType: "Tipus d'element", - prompt: "Descripció de l'element UI", - outputFormat: "Format de sortida", - anchorPreset: "Preset d'ancoratge", - colourTheme: "Tema de color (opcional)", - includeAnimations: "Inclou animacions", - }, - actions: { - generate: "Genera UI", - }, - }, - - unityPhysics: { - title: "Unity Física", - subtitle: "Genera configuració de física per a Unity — Rigidbodies, col·lisionadors, gravetat i més.", - fields: { - physicsBackend: "Motor de física", - simulationMode: "Mode de simulació", - gravityPreset: "Preset de gravetat", - prompt: "Descripció de la física", - includeRigidbody: "Inclou configuració de Rigidbody", - includeColliders: "Inclou configuració de col·lisionadors", - includeLayers: "Inclou capes de física", - }, - actions: { - generate: "Genera configuració de física", - }, - quickActions: "Accions ràpides", - }, - - unityProject: { - title: "Projecte Unity", - subtitle: "Genera una estructura completa de projecte Unity.", - engineSettings: "Configuració del motor Unity", - fields: { - projectName: "Nom del projecte", - unityTemplate: "Plantilla de Unity", - unityVersion: "Versió de Unity", - targetPlatform: "Plataforma de destinació", - generateDefaultScene: "Genera escena predeterminada", - autoInstallPackages: "Instal·la paquets UPM automàticament", - setupUrp: "Configura URP", - upmPackages: "Paquets UPM (separats per comes)", - sceneName: "Nom de l'escena", - unityEditorPath: "Ruta de l'editor Unity (opcional)", - timeout: "Temps d'espera (segons)", - resultJson: "Resultat (JSON)", - }, - actions: { - generate: "Genera projecte", - finalize: "Finalitza projecte", - openFolder: "Obre la carpeta de sortida", - addVersion: "Afegeix versió de Unity", - downloadZip: "Descarrega el projecte finalitzat (.zip)", - }, - dialogs: { - addVersion: { - title: "Afegeix versió de Unity", - versionId: "ID de versió", - label: "Etiqueta (opcional)", - }, - }, - }, - - theme: { - light: "Clar", - dark: "Fosc", - system: "Sistema", - }, - - languages: { - en: "Anglès", - es: "Castellà", - ca: "Català", - eu: "Euskara", - oc: "Occità", - uk: "Ucrainès", - pt: "Portuguès", - gl: "Gallec", - fr: "Francès", - it: "Italià", - pl: "Polonès", - zh: "Xinès", - ar: "Àrab", - de: "Alemany", - hi: "Hindi", - bn: "Bengalí", - ur: "Urdú", - id: "Indonesi", - ja: "Japonès", - vi: "Vietnamita", - ko: "Coreà", - tr: "Turc", - }, -} as const; +/** + * Catalan (ca) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { + online: "En línia", + offline: "Sense connexió", + }, + nav: { + settings: "Configuració", + scenes: "Escenes", + code: "Codi", + text: "Text", + image: "Imatge", + sprites: "Sprites", + audio: "Àudio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Física", + unityProject: "Projecte Unity", + logs: "Logs", + }, + actions: { + repository: "Repositori", + }, + }, + + common: { + loading: "Carregant…", + saving: "Desant…", + save: "Desa", + cancel: "Cancel·la", + close: "Tanca", + ok: "D'acord", + yes: "Sí", + no: "No", + error: "Error", + warning: "Avís", + success: "Èxit", + generate: "Genera", + result: "Resultat", + provider: "Proveïdor", + model: "Model", + temperature: "Temperatura", + apiKey: "Clau API (substitució opcional)", + systemPrompt: "Substitució del prompt de sistema", + advancedOptions: "Opcions avançades", + examplePrompts: "Exemples de prompts", + manageModels: "Gestiona models", + selectProvider: "Selecciona proveïdor", + selectModel: "Selecciona model", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Deixa buit per usar la clau global", + createdFiles: "Fitxers creats", + viewSteps: "Veure passos ({n})", + waitingForLogs: "Esperant registres…", + errorsDetected: "Errors detectats", + }, + + settings: { + title: "Configuració", + subtitle: "Gestiona els teus motors d'intel·ligència, models i prompts de sistema.", + tabs: { + general: "General", + providers: "Proveïdors", + models: "Models", + prompts: "Prompts", + secrets: "Secrets", + }, + }, + + general: { + title: "Preferències generals", + sections: { + networkApi: "Xarxa i API", + preferredIntelligence: "Intel·ligència preferida", + appearance: "Aparença", + language: "Idioma", + }, + fields: { + backendUrl: "URL del backend", + backendUrlHint: "L'adreça del teu servei backend de Unity Generator", + outputBasePath: "Ruta base (sortida)", + outputBasePathHint: "On es desen els fitxers generats", + unityEditorPath: "Ruta de l'editor Unity (opcional)", + unityEditorPathHint: "Ruta completa a l'executable de l'editor Unity", + preferredLlm: "Proveïdor de text / lògica predeterminat", + preferredLlmModel: "Model de text / lògica predeterminat", + preferredImage: "Proveïdor de generació d'imatges predeterminat", + preferredImageModel: "Model de generació d'imatges predeterminat", + preferredAudio: "Proveïdor de veu (TTS) predeterminat", + preferredAudioModel: "Model de veu (TTS) predeterminat", + preferredMusic: "Proveïdor de música predeterminat", + preferredMusicModel: "Model de música predeterminat", + theme: "Tema", + language: "Idioma de la interfície", + }, + actions: { + saveAll: "Desa tots els canvis", + }, + status: { + saving: "Desant…", + saved: "Preferències desades correctament.", + modelNotRegistered: 'El model "{model}" no està registrat per al proveïdor "{provider}".', + networkError: "Error de xarxa: no es pot connectar amb el backend.", + }, + }, + + code: { + title: "Codi C# per a Unity", + subtitle: "Genera scripts C# per a Unity amb IA.", + fields: { + prompt: "Prompt", + maxTokens: "Tokens màxims", + }, + actions: { + generate: "Genera codi", + }, + activeProject: "Projecte actiu: {name}", + autoSave: "Desa automàticament al projecte", + }, + + text: { + title: "Generació de text", + subtitle: "Genera contingut de text amb IA.", + fields: { + prompt: "Prompt", + length: "Longitud", + }, + actions: { + generate: "Genera text", + }, + }, + + image: { + title: "Generació d'imatges", + subtitle: "Genera imatges amb IA.", + fields: { + prompt: "Prompt", + aspectRatio: "Relació d'aspecte", + quality: "Qualitat", + textureName: "Nom de la textura", + textureType: "Tipus de textura", + }, + actions: { + generate: "Genera imatge", + saveToUnity: "Desa a Unity", + }, + }, + + sprites: { + title: "Sprites 2D", + badge: "Optimitzat per a Pixel Art", + subtitle: "Genera sprites 2D per a Unity.", + fields: { + prompt: "Prompt", + resolution: "Resolució", + paletteSize: "Mida de la paleta", + autoCrop: "Retall automàtic de vores transparents", + colors: "{n} colors", + }, + preview: "Àrea de previsualització", + actions: { + generate: "Genera sprites", + }, + }, + + audio: { + title: "Generació d'àudio", + subtitle: "Genera recursos d'àudio amb IA.", + generationType: "Tipus de generació", + speechTts: "Veu (TTS)", + atmosphericMusic: "Música ambiental", + fields: { + prompt: "Prompt", + musicDescription: "Descripció musical", + speechPrompt: "Prompt de veu", + voiceOptional: "Veu (opcional)", + musicModel: "Model de música", + stability: "Estabilitat", + audioClipName: "Nom del clip d'àudio", + audioFormat: "Format d'àudio", + }, + actions: { + generate: "Genera àudio", + saveToUnity: "Desa a Unity", + }, + }, + + scenes: { + title: "Creador d'escenes", + subtitle: "Genera escenes de Unity amb IA.", + fields: { + prompt: "Descripció de l'escena", + }, + actions: { + generate: "Crea escena", + }, + mediaReady: "Multimèdia llesta per importar", + mediaReadyText: "{type} \"{name}\" està llesta per importar-se a Unity. Revisa el prompt i fes clic a \"Crea escena\" per continuar.", + mediaTypeImage: "Imatge", + mediaTypeAudio: "Àudio", + }, + + unityUi: { + title: "Unity UI", + subtitle: "Genera components d'UI per a Unity amb IA.", + fields: { + uiSystem: "Sistema d'UI", + elementType: "Tipus d'element", + prompt: "Descripció de l'element UI", + outputFormat: "Format de sortida", + anchorPreset: "Preset d'ancoratge", + colourTheme: "Tema de color (opcional)", + includeAnimations: "Inclou animacions", + }, + actions: { + generate: "Genera UI", + }, + }, + + unityPhysics: { + title: "Unity Física", + subtitle: "Genera configuració de física per a Unity — Rigidbodies, col·lisionadors, gravetat i més.", + fields: { + physicsBackend: "Motor de física", + simulationMode: "Mode de simulació", + gravityPreset: "Preset de gravetat", + prompt: "Descripció de la física", + includeRigidbody: "Inclou configuració de Rigidbody", + includeColliders: "Inclou configuració de col·lisionadors", + includeLayers: "Inclou capes de física", + }, + actions: { + generate: "Genera configuració de física", + }, + quickActions: "Accions ràpides", + }, + + unityProject: { + title: "Projecte Unity", + subtitle: "Genera una estructura completa de projecte Unity.", + engineSettings: "Configuració del motor Unity", + fields: { + projectName: "Nom del projecte", + unityTemplate: "Plantilla de Unity", + unityVersion: "Versió de Unity", + targetPlatform: "Plataforma de destinació", + generateDefaultScene: "Genera escena predeterminada", + autoInstallPackages: "Instal·la paquets UPM automàticament", + setupUrp: "Configura URP", + upmPackages: "Paquets UPM (separats per comes)", + sceneName: "Nom de l'escena", + unityEditorPath: "Ruta de l'editor Unity (opcional)", + timeout: "Temps d'espera (segons)", + resultJson: "Resultat (JSON)", + }, + actions: { + generate: "Genera projecte", + finalize: "Finalitza projecte", + openFolder: "Obre la carpeta de sortida", + addVersion: "Afegeix versió de Unity", + downloadZip: "Descarrega el projecte finalitzat (.zip)", + }, + dialogs: { + addVersion: { + title: "Afegeix versió de Unity", + versionId: "ID de versió", + label: "Etiqueta (opcional)", + }, + }, + }, + + theme: { + light: "Clar", + dark: "Fosc", + system: "Sistema", + }, + + languages: { + en: "Anglès", + es: "Castellà", + ca: "Català", + eu: "Euskara", + oc: "Occità", + uk: "Ucrainès", + pt: "Portuguès", + gl: "Gallec", + fr: "Francès", + it: "Italià", + pl: "Polonès", + zh: "Xinès", + ar: "Àrab", + de: "Alemany", + hi: "Hindi", + bn: "Bengalí", + ur: "Urdú", + id: "Indonesi", + ja: "Japonès", + vi: "Vietnamita", + ko: "Coreà", + tr: "Turc", + }, +} as const; diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 07c4f244..76a426c9 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -1,246 +1,254 @@ -/** - * German (de) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "Online", offline: "Offline" }, - nav: { - settings: "Einstellungen", - scenes: "Szenen", - code: "Code", - text: "Text", - image: "Bild", - sprites: "Sprites", - audio: "Audio", - unityUi: "Unity UI", - unityPhysics: "Unity Physik", - unityProject: "Unity-Projekt", - }, - actions: { repository: "Repository" }, - }, - common: { - loading: "Laden…", - saving: "Speichern…", - save: "Speichern", - cancel: "Abbrechen", - close: "Schließen", - ok: "OK", - yes: "Ja", - no: "Nein", - error: "Fehler", - warning: "Warnung", - success: "Erfolg", - generate: "Generieren", - result: "Ergebnis", - provider: "Anbieter", - model: "Modell", - temperature: "Temperatur", - apiKey: "API-Schlüssel (optionale Überschreibung)", - systemPrompt: "System-Prompt-Überschreibung", - advancedOptions: "Erweiterte Optionen", - examplePrompts: "Beispiel-Prompts", - manageModels: "Modelle verwalten", - selectProvider: "Anbieter auswählen", - selectModel: "Modell auswählen", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Leer lassen, um den globalen Schlüssel zu verwenden", - createdFiles: "Erstellte Dateien", - viewSteps: "Schritte anzeigen ({n})", - waitingForLogs: "Warte auf Protokolle…", - errorsDetected: "Fehler erkannt", - }, - settings: { - title: "Konfiguration", - subtitle: "Verwalten Sie Ihre Intelligenz-Engines, Modelle und System-Prompts.", - tabs: { - general: "Allgemein", - providers: "Anbieter", - models: "Modelle", - prompts: "Prompts", - secrets: "Geheimnisse", - }, - }, - general: { - title: "Allgemeine Einstellungen", - sections: { - networkApi: "Netzwerk und API", - preferredIntelligence: "Bevorzugte Intelligenz", - appearance: "Erscheinungsbild", - language: "Sprache", - }, - fields: { - backendUrl: "Backend-URL", - backendUrlHint: "Die Adresse Ihres Unity Generator Backend-Dienstes", - outputBasePath: "Basispfad (Ausgabe)", - outputBasePathHint: "Wo generierte Dateien gespeichert werden", - unityEditorPath: "Unity-Editor-Pfad (optional)", - unityEditorPathHint: "Vollständiger Pfad zur ausführbaren Datei des Unity-Editors", - preferredLlm: "Standard-Text/Logik-Anbieter", - preferredLlmModel: "Standard-Text/Logik-Modell", - preferredImage: "Standard-Bildgenerierungs-Anbieter", - preferredImageModel: "Standard-Bildgenerierungs-Modell", - preferredAudio: "Standard-Sprach-(TTS)-Anbieter", - preferredAudioModel: "Standard-Sprach-(TTS)-Modell", - preferredMusic: "Standard-Musik-Anbieter", - preferredMusicModel: "Standard-Musik-Modell", - theme: "Design", - language: "Oberflächensprache", - }, - actions: { saveAll: "Alle Änderungen speichern" }, - status: { - saving: "Speichern…", - saved: "Einstellungen erfolgreich gespeichert.", - modelNotRegistered: 'Das Modell "{model}" ist für den Anbieter "{provider}" nicht registriert.', - networkError: "Netzwerkfehler: Backend nicht erreichbar.", - }, - }, - code: { - title: "Unity C#-Code", - subtitle: "Generieren Sie Unity C#-Skripte mit KI.", - fields: { prompt: "Prompt", maxTokens: "Maximale Token" }, - actions: { generate: "Code generieren" }, - activeProject: "Aktives Projekt: {name}", - autoSave: "Automatisch im Projekt speichern", - }, - text: { - title: "Textgenerierung", - subtitle: "Generieren Sie Textinhalte mit KI.", - fields: { prompt: "Prompt", length: "Länge" }, - actions: { generate: "Text generieren" }, - }, - image: { - title: "Bildgenerierung", - subtitle: "Generieren Sie Bilder mit KI.", - fields: { - prompt: "Prompt", - aspectRatio: "Seitenverhältnis", - quality: "Qualität", - textureName: "Texturname", - textureType: "Texturtyp", - }, - actions: { generate: "Bild generieren", saveToUnity: "In Unity speichern" }, - }, - sprites: { - title: "2D-Sprites", - badge: "Optimiert für Pixel Art", - subtitle: "Generieren Sie 2D-Sprites für Unity.", - fields: { - prompt: "Prompt", - resolution: "Auflösung", - paletteSize: "Palettengröße", - autoCrop: "Transparente Ränder automatisch zuschneiden", - colors: "{n} Farben", - }, - preview: "Vorschaubereich", - actions: { generate: "Sprites generieren" }, - }, - audio: { - title: "Audiogenerierung", - subtitle: "Generieren Sie Audio-Assets mit KI.", - generationType: "Generierungstyp", - speechTts: "Sprache (TTS)", - atmosphericMusic: "Atmosphärische Musik", - fields: { - prompt: "Prompt", - musicDescription: "Musikbeschreibung", - speechPrompt: "Sprach-Prompt", - voiceOptional: "Stimme (optional)", - musicModel: "Musikmodell", - stability: "Stabilität", - audioClipName: "Audio-Clip-Name", - audioFormat: "Audioformat", - }, - actions: { generate: "Audio generieren", saveToUnity: "In Unity speichern" }, - }, - scenes: { - title: "Szenen-Ersteller", - subtitle: "Generieren Sie Unity-Szenen mit KI.", - fields: { prompt: "Szenenbeschreibung" }, - actions: { generate: "Szene erstellen" }, - mediaReady: "Medien bereit zum Importieren", - mediaReadyText: "{type} \"{name}\" ist bereit, in Unity importiert zu werden. Überprüfen Sie den Prompt und klicken Sie auf \"Szene erstellen\", um fortzufahren.", - mediaTypeImage: "Bild", - mediaTypeAudio: "Audio", - }, - unityUi: { - title: "Unity UI", - subtitle: "Generieren Sie Unity-UI-Komponenten mit KI.", - fields: { - uiSystem: "UI-System", - elementType: "Elementtyp", - prompt: "UI-Element-Beschreibung", - outputFormat: "Ausgabeformat", - anchorPreset: "Anker-Preset", - colourTheme: "Farbthema (optional)", - includeAnimations: "Animationen einschließen", - }, - actions: { generate: "UI generieren" }, - }, - unityPhysics: { - title: "Unity Physik", - subtitle: "Generieren Sie Unity-Physikkonfiguration — Rigidbodies, Collider, Schwerkraft und mehr.", - fields: { - physicsBackend: "Physik-Engine", - simulationMode: "Simulationsmodus", - gravityPreset: "Schwerkraft-Preset", - prompt: "Physikbeschreibung", - includeRigidbody: "Rigidbody-Konfiguration einschließen", - includeColliders: "Collider-Konfiguration einschließen", - includeLayers: "Physik-Layer einschließen", - }, - actions: { generate: "Physikkonfiguration generieren" }, - quickActions: "Schnellaktionen", - }, - unityProject: { - title: "Unity-Projekt", - subtitle: "Generieren Sie eine vollständige Unity-Projektstruktur.", - engineSettings: "Unity-Engine-Einstellungen", - fields: { - projectName: "Projektname", - unityTemplate: "Unity-Vorlage", - unityVersion: "Unity-Version", - targetPlatform: "Zielplattform", - generateDefaultScene: "Standardszene generieren", - autoInstallPackages: "UPM-Pakete automatisch installieren", - setupUrp: "URP konfigurieren", - upmPackages: "UPM-Pakete (kommagetrennt)", - sceneName: "Szenenname", - unityEditorPath: "Unity-Editor-Pfad (optional)", - timeout: "Zeitlimit (Sekunden)", - resultJson: "Ergebnis (JSON)", - }, - actions: { - generate: "Projekt generieren", - finalize: "Projekt abschließen", - openFolder: "Ausgabeordner öffnen", - addVersion: "Unity-Version hinzufügen", - downloadZip: "Abgeschlossenes Projekt herunterladen (.zip)", - }, - dialogs: { - addVersion: { - title: "Unity-Version hinzufügen", - versionId: "Versions-ID", - label: "Bezeichnung (optional)", - }, - }, - }, - theme: { light: "Hell", dark: "Dunkel", system: "System" }, - languages: { - en: "Englisch", - es: "Spanisch", - ca: "Katalanisch", - eu: "Baskisch", - oc: "Okzitanisch", - uk: "Ukrainisch", - pt: "Portugiesisch", - gl: "Galizisch", - fr: "Französisch", - it: "Italienisch", - pl: "Polnisch", - zh: "Chinesisch", - ar: "Arabisch", - de: "Deutsch", - tr: "Türkisch", - }, -} as const; +/** + * German (de) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "Online", offline: "Offline" }, + nav: { + settings: "Einstellungen", + scenes: "Szenen", + code: "Code", + text: "Text", + image: "Bild", + sprites: "Sprites", + audio: "Audio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Physik", + unityProject: "Unity-Projekt", + logs: "Logs", + }, + actions: { repository: "Repository" }, + }, + common: { + loading: "Laden…", + saving: "Speichern…", + save: "Speichern", + cancel: "Abbrechen", + close: "Schließen", + ok: "OK", + yes: "Ja", + no: "Nein", + error: "Fehler", + warning: "Warnung", + success: "Erfolg", + generate: "Generieren", + result: "Ergebnis", + provider: "Anbieter", + model: "Modell", + temperature: "Temperatur", + apiKey: "API-Schlüssel (optionale Überschreibung)", + systemPrompt: "System-Prompt-Überschreibung", + advancedOptions: "Erweiterte Optionen", + examplePrompts: "Beispiel-Prompts", + manageModels: "Modelle verwalten", + selectProvider: "Anbieter auswählen", + selectModel: "Modell auswählen", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Leer lassen, um den globalen Schlüssel zu verwenden", + createdFiles: "Erstellte Dateien", + viewSteps: "Schritte anzeigen ({n})", + waitingForLogs: "Warte auf Protokolle…", + errorsDetected: "Fehler erkannt", + }, + settings: { + title: "Konfiguration", + subtitle: "Verwalten Sie Ihre Intelligenz-Engines, Modelle und System-Prompts.", + tabs: { + general: "Allgemein", + providers: "Anbieter", + models: "Modelle", + prompts: "Prompts", + secrets: "Geheimnisse", + }, + }, + general: { + title: "Allgemeine Einstellungen", + sections: { + networkApi: "Netzwerk und API", + preferredIntelligence: "Bevorzugte Intelligenz", + appearance: "Erscheinungsbild", + language: "Sprache", + }, + fields: { + backendUrl: "Backend-URL", + backendUrlHint: "Die Adresse Ihres Unity Generator Backend-Dienstes", + outputBasePath: "Basispfad (Ausgabe)", + outputBasePathHint: "Wo generierte Dateien gespeichert werden", + unityEditorPath: "Unity-Editor-Pfad (optional)", + unityEditorPathHint: "Vollständiger Pfad zur ausführbaren Datei des Unity-Editors", + preferredLlm: "Standard-Text/Logik-Anbieter", + preferredLlmModel: "Standard-Text/Logik-Modell", + preferredImage: "Standard-Bildgenerierungs-Anbieter", + preferredImageModel: "Standard-Bildgenerierungs-Modell", + preferredAudio: "Standard-Sprach-(TTS)-Anbieter", + preferredAudioModel: "Standard-Sprach-(TTS)-Modell", + preferredMusic: "Standard-Musik-Anbieter", + preferredMusicModel: "Standard-Musik-Modell", + theme: "Design", + language: "Oberflächensprache", + }, + actions: { saveAll: "Alle Änderungen speichern" }, + status: { + saving: "Speichern…", + saved: "Einstellungen erfolgreich gespeichert.", + modelNotRegistered: 'Das Modell "{model}" ist für den Anbieter "{provider}" nicht registriert.', + networkError: "Netzwerkfehler: Backend nicht erreichbar.", + }, + }, + code: { + title: "Unity C#-Code", + subtitle: "Generieren Sie Unity C#-Skripte mit KI.", + fields: { prompt: "Prompt", maxTokens: "Maximale Token" }, + actions: { generate: "Code generieren" }, + activeProject: "Aktives Projekt: {name}", + autoSave: "Automatisch im Projekt speichern", + }, + text: { + title: "Textgenerierung", + subtitle: "Generieren Sie Textinhalte mit KI.", + fields: { prompt: "Prompt", length: "Länge" }, + actions: { generate: "Text generieren" }, + }, + image: { + title: "Bildgenerierung", + subtitle: "Generieren Sie Bilder mit KI.", + fields: { + prompt: "Prompt", + aspectRatio: "Seitenverhältnis", + quality: "Qualität", + textureName: "Texturname", + textureType: "Texturtyp", + }, + actions: { generate: "Bild generieren", saveToUnity: "In Unity speichern" }, + }, + sprites: { + title: "2D-Sprites", + badge: "Optimiert für Pixel Art", + subtitle: "Generieren Sie 2D-Sprites für Unity.", + fields: { + prompt: "Prompt", + resolution: "Auflösung", + paletteSize: "Palettengröße", + autoCrop: "Transparente Ränder automatisch zuschneiden", + colors: "{n} Farben", + }, + preview: "Vorschaubereich", + actions: { generate: "Sprites generieren" }, + }, + audio: { + title: "Audiogenerierung", + subtitle: "Generieren Sie Audio-Assets mit KI.", + generationType: "Generierungstyp", + speechTts: "Sprache (TTS)", + atmosphericMusic: "Atmosphärische Musik", + fields: { + prompt: "Prompt", + musicDescription: "Musikbeschreibung", + speechPrompt: "Sprach-Prompt", + voiceOptional: "Stimme (optional)", + musicModel: "Musikmodell", + stability: "Stabilität", + audioClipName: "Audio-Clip-Name", + audioFormat: "Audioformat", + }, + actions: { generate: "Audio generieren", saveToUnity: "In Unity speichern" }, + }, + scenes: { + title: "Szenen-Ersteller", + subtitle: "Generieren Sie Unity-Szenen mit KI.", + fields: { prompt: "Szenenbeschreibung" }, + actions: { generate: "Szene erstellen" }, + mediaReady: "Medien bereit zum Importieren", + mediaReadyText: "{type} \"{name}\" ist bereit, in Unity importiert zu werden. Überprüfen Sie den Prompt und klicken Sie auf \"Szene erstellen\", um fortzufahren.", + mediaTypeImage: "Bild", + mediaTypeAudio: "Audio", + }, + unityUi: { + title: "Unity UI", + subtitle: "Generieren Sie Unity-UI-Komponenten mit KI.", + fields: { + uiSystem: "UI-System", + elementType: "Elementtyp", + prompt: "UI-Element-Beschreibung", + outputFormat: "Ausgabeformat", + anchorPreset: "Anker-Preset", + colourTheme: "Farbthema (optional)", + includeAnimations: "Animationen einschließen", + }, + actions: { generate: "UI generieren" }, + }, + unityPhysics: { + title: "Unity Physik", + subtitle: "Generieren Sie Unity-Physikkonfiguration — Rigidbodies, Collider, Schwerkraft und mehr.", + fields: { + physicsBackend: "Physik-Engine", + simulationMode: "Simulationsmodus", + gravityPreset: "Schwerkraft-Preset", + prompt: "Physikbeschreibung", + includeRigidbody: "Rigidbody-Konfiguration einschließen", + includeColliders: "Collider-Konfiguration einschließen", + includeLayers: "Physik-Layer einschließen", + }, + actions: { generate: "Physikkonfiguration generieren" }, + quickActions: "Schnellaktionen", + }, + unityProject: { + title: "Unity-Projekt", + subtitle: "Generieren Sie eine vollständige Unity-Projektstruktur.", + engineSettings: "Unity-Engine-Einstellungen", + fields: { + projectName: "Projektname", + unityTemplate: "Unity-Vorlage", + unityVersion: "Unity-Version", + targetPlatform: "Zielplattform", + generateDefaultScene: "Standardszene generieren", + autoInstallPackages: "UPM-Pakete automatisch installieren", + setupUrp: "URP konfigurieren", + upmPackages: "UPM-Pakete (kommagetrennt)", + sceneName: "Szenenname", + unityEditorPath: "Unity-Editor-Pfad (optional)", + timeout: "Zeitlimit (Sekunden)", + resultJson: "Ergebnis (JSON)", + }, + actions: { + generate: "Projekt generieren", + finalize: "Projekt abschließen", + openFolder: "Ausgabeordner öffnen", + addVersion: "Unity-Version hinzufügen", + downloadZip: "Abgeschlossenes Projekt herunterladen (.zip)", + }, + dialogs: { + addVersion: { + title: "Unity-Version hinzufügen", + versionId: "Versions-ID", + label: "Bezeichnung (optional)", + }, + }, + }, + theme: { light: "Hell", dark: "Dunkel", system: "System" }, + languages: { + en: "Englisch", + es: "Spanisch", + ca: "Katalanisch", + eu: "Baskisch", + oc: "Okzitanisch", + uk: "Ukrainisch", + pt: "Portugiesisch", + gl: "Galizisch", + fr: "Französisch", + it: "Italienisch", + pl: "Polnisch", + zh: "Chinesisch", + ar: "Arabisch", + de: "Deutsch", + tr: "Türkisch", + }, +} as const; diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 901503c4..8d780216 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1,320 +1,446 @@ -/** - * English (en) locale — default language. - * All other locales must mirror this structure exactly. - */ -export default { - // ── App shell ────────────────────────────────────────────────────────────── - app: { - title: "Unity Generator", - status: { - online: "Online", - offline: "Offline", - }, - nav: { - settings: "Settings", - scenes: "Scenes", - code: "Code", - text: "Text", - image: "Image", - sprites: "Sprites", - audio: "Audio", - unityUi: "Unity UI", - unityPhysics: "Unity Physics", - unityProject: "Unity Project", - }, - actions: { - repository: "Repository", - }, - }, - - // ── Common / shared ──────────────────────────────────────────────────────── - common: { - loading: "Loading…", - saving: "Saving…", - save: "Save", - cancel: "Cancel", - close: "Close", - ok: "OK", - yes: "Yes", - no: "No", - error: "Error", - warning: "Warning", - success: "Success", - generate: "Generate", - result: "Result", - provider: "Provider", - model: "Model", - temperature: "Temperature", - apiKey: "API Key (Optional Override)", - systemPrompt: "System Prompt Override", - advancedOptions: "Advanced Options", - examplePrompts: "Example Prompts", - manageModels: "Manage models", - selectProvider: "Select Provider", - selectModel: "Select Model", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Leave empty to use global key", - createdFiles: "Created Files", - viewSteps: "View Steps ({n})", - waitingForLogs: "Waiting for logs…", - errorsDetected: "Errors Detected", - }, - - // ── Settings panel ───────────────────────────────────────────────────────── - settings: { - title: "Configuration", - subtitle: "Manage your intelligence engines, models, and system prompts.", - tabs: { - general: "General", - providers: "Providers", - models: "Models", - prompts: "Prompts", - secrets: "Secrets", - }, - }, - - // ── General settings section ─────────────────────────────────────────────── - general: { - title: "General Preferences", - sections: { - networkApi: "Network & API", - preferredIntelligence: "Preferred Intelligence", - appearance: "Appearance", - language: "Language", - }, - fields: { - backendUrl: "Backend URL", - backendUrlHint: "The address of your Unity Generator backend service", - outputBasePath: "Base path (output)", - outputBasePathHint: "Where generated files are saved", - unityEditorPath: "Unity Editor Path (optional)", - unityEditorPathHint: "Full path to the Unity Editor executable", - preferredLlm: "Default Text / Logic Provider", - preferredLlmModel: "Default Text / Logic Model", - preferredImage: "Default Image Generation Provider", - preferredImageModel: "Default Image Generation Model", - preferredAudio: "Default Speech (TTS) Provider", - preferredAudioModel: "Default Speech (TTS) Model", - preferredMusic: "Default Music Generation Provider", - preferredMusicModel: "Default Music Generation Model", - theme: "Theme", - language: "Interface Language", - }, - actions: { - saveAll: "Save All Changes", - }, - status: { - saving: "Saving…", - saved: "Preferences saved successfully.", - modelNotRegistered: 'Model "{model}" is not registered for provider "{provider}".', - networkError: "Network error: Failed to reach backend.", - }, - }, - - // ── Code panel ───────────────────────────────────────────────────────────── - code: { - title: "Unity C# Code", - subtitle: "Generate Unity C# scripts using AI.", - fields: { - prompt: "Prompt", - maxTokens: "Max Tokens", - }, - actions: { - generate: "Generate Code", - }, - activeProject: "Active Project: {name}", - autoSave: "Auto-save to project", - }, - - // ── Text panel ───────────────────────────────────────────────────────────── - text: { - title: "Text Generation", - subtitle: "Generate text content using AI.", - fields: { - prompt: "Prompt", - length: "Length", - }, - actions: { - generate: "Generate Text", - }, - }, - - // ── Image panel ──────────────────────────────────────────────────────────── - image: { - title: "Image Generation", - subtitle: "Generate images using AI.", - fields: { - prompt: "Prompt", - aspectRatio: "Aspect Ratio", - quality: "Quality", - textureName: "Texture Name", - textureType: "Texture Type", - }, - actions: { - generate: "Generate Image", - saveToUnity: "Save to Unity", - }, - }, - - // ── Sprites panel ────────────────────────────────────────────────────────── - sprites: { - title: "2D Sprites", - badge: "Pixel Art Optimized", - subtitle: "Generate 2D sprite assets for Unity.", - fields: { - prompt: "Prompt", - resolution: "Resolution", - paletteSize: "Palette Size", - autoCrop: "Auto-Crop Transparent Edges", - colors: "{n} colors", - }, - preview: "Preview Area", - actions: { - generate: "Generate Sprites", - }, - }, - - // ── Audio panel ──────────────────────────────────────────────────────────── - audio: { - title: "Audio Generation", - subtitle: "Generate audio assets using AI.", - generationType: "Generation Type", - speechTts: "Speech (TTS)", - atmosphericMusic: "Atmospheric Music", - fields: { - prompt: "Prompt", - musicDescription: "Music Description", - speechPrompt: "Speech Prompt", - voiceOptional: "Voice (optional)", - musicModel: "Music Model", - stability: "Stability", - audioClipName: "Audio Clip Name", - audioFormat: "Audio Format", - }, - actions: { - generate: "Generate Audio", - saveToUnity: "Save to Unity", - }, - }, - - // ── Scenes panel ────────────────────────────────────────────────────────── - scenes: { - title: "Scene Creator", - subtitle: "Generate Unity scenes using AI.", - fields: { - prompt: "Scene Description", - }, - actions: { - generate: "Create Scene", - }, - mediaReady: "Media Ready to Import", - mediaReadyText: "{type} \"{name}\" is ready to be imported to Unity. Review the prompt below and click \"Create Scene\" to proceed.", - mediaTypeImage: "Image", - mediaTypeAudio: "Audio", - }, - - // ── Unity UI panel ───────────────────────────────────────────────────────── - unityUi: { - title: "Unity UI", - subtitle: "Generate Unity UI components using AI.", - fields: { - uiSystem: "UI System", - elementType: "Element Type", - prompt: "UI Element Description", - outputFormat: "Output Format", - anchorPreset: "Anchor Preset", - colourTheme: "Colour Theme (optional)", - includeAnimations: "Include animations", - }, - actions: { - generate: "Generate UI", - }, - }, - - // ── Unity Physics panel ──────────────────────────────────────────────────── - unityPhysics: { - title: "Unity Physics", - subtitle: "Generate Unity physics configuration — Rigidbodies, colliders, gravity, and more.", - fields: { - physicsBackend: "Physics Backend", - simulationMode: "Simulation Mode", - gravityPreset: "Gravity Preset", - prompt: "Physics Description", - includeRigidbody: "Include Rigidbody setup", - includeColliders: "Include Collider setup", - includeLayers: "Include Physics Layers", - }, - actions: { - generate: "Generate Physics Config", - }, - quickActions: "Quick Actions", - }, - - // ── Unity Project panel ──────────────────────────────────────────────────── - unityProject: { - title: "Unity Project", - subtitle: "Generate a complete Unity project structure.", - engineSettings: "Unity Engine Settings", - fields: { - projectName: "Project Name", - unityTemplate: "Unity Template", - unityVersion: "Unity Version", - targetPlatform: "Target Platform", - generateDefaultScene: "Generate Default Scene", - autoInstallPackages: "Auto-Install UPM Packages", - setupUrp: "Setup URP", - upmPackages: "UPM Packages (comma-separated)", - sceneName: "Scene Name", - unityEditorPath: "Unity Editor Path (optional)", - timeout: "Timeout (seconds)", - resultJson: "Result (JSON)", - }, - actions: { - generate: "Generate Project", - finalize: "Finalize Project", - openFolder: "Open Output Folder", - addVersion: "Add Unity Version", - downloadZip: "Download Finalized Project (.zip)", - }, - dialogs: { - addVersion: { - title: "Add Unity Version", - versionId: "Version ID", - label: "Label (optional)", - }, - }, - }, - - // ── Theme toggle ─────────────────────────────────────────────────────────── - theme: { - light: "Light", - dark: "Dark", - system: "System", - }, - - // ── Language names (shown in selector) ──────────────────────────────────── - languages: { - en: "English", - es: "Spanish", - ca: "Catalan", - eu: "Basque", - oc: "Occitan", - uk: "Ukrainian", - pt: "Portuguese", - gl: "Galician", - fr: "French", - it: "Italian", - pl: "Polish", - zh: "Chinese", - ar: "Arabic", - de: "German", - hi: "Hindi", - bn: "Bengali", - ur: "Urdu", - id: "Indonesian", - ja: "Japanese", - vi: "Vietnamese", - ko: "Korean", - tr: "Turkish", - }, -} as const; +/** + * English (en) locale — default language. + * All other locales must mirror this structure exactly. + */ +export default { + // ── App shell ────────────────────────────────────────────────────────────── + app: { + title: "Unity Generator", + status: { + online: "Online", + offline: "Offline", + }, + nav: { + settings: "Settings", + wizard: "Wizard", + scenes: "Scenes", + code: "Code", + text: "Text", + image: "Image", + sprites: "Sprites", + audio: "Audio", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Physics", + unityProject: "Unity Project", + logs: "Logs", + }, + actions: { + repository: "Repository", + }, + }, + + // ── Common / shared ──────────────────────────────────────────────────────── + common: { + loading: "Loading…", + saving: "Saving…", + save: "Save", + cancel: "Cancel", + close: "Close", + ok: "OK", + yes: "Yes", + no: "No", + error: "Error", + warning: "Warning", + success: "Success", + generate: "Generate", + result: "Result", + provider: "Provider", + model: "Model", + temperature: "Temperature", + apiKey: "API Key (Optional Override)", + systemPrompt: "System Prompt Override", + advancedOptions: "Advanced Options", + examplePrompts: "Example Prompts", + manageModels: "Manage models", + selectProvider: "Select Provider", + selectModel: "Select Model", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Leave empty to use global key", + createdFiles: "Created Files", + viewSteps: "View Steps ({n})", + waitingForLogs: "Waiting for logs…", + errorsDetected: "Errors Detected", + enhancePrompt: "Enhance prompt", + samplePrompt: "Sample prompt", + }, + + // ── Settings panel ───────────────────────────────────────────────────────── + settings: { + title: "Configuration", + subtitle: "Manage your intelligence engines, models, and system prompts.", + tabs: { + general: "General", + providers: "Providers", + models: "Models", + prompts: "Prompts", + secrets: "Secrets", + }, + }, + + // ── General settings section ─────────────────────────────────────────────── + general: { + title: "General Preferences", + sections: { + networkApi: "Network & API", + preferredIntelligence: "Preferred Intelligence", + appearance: "Appearance", + language: "Language", + }, + fields: { + backendUrl: "Backend URL", + backendUrlHint: "The address of your Unity Generator backend service", + outputBasePath: "Base path (output)", + outputBasePathHint: "Where generated files are saved", + unityEditorPath: "Unity Editor Path (optional)", + unityEditorPathHint: "Full path to the Unity Editor executable", + preferredLlm: "Default Text / Logic Provider", + preferredLlmModel: "Default Text / Logic Model", + preferredImage: "Default Image Generation Provider", + preferredImageModel: "Default Image Generation Model", + preferredAudio: "Default Speech (TTS) Provider", + preferredAudioModel: "Default Speech (TTS) Model", + preferredMusic: "Default Music Generation Provider", + preferredMusicModel: "Default Music Generation Model", + theme: "Theme", + language: "Interface Language", + }, + actions: { + saveAll: "Save All Changes", + }, + status: { + saving: "Saving…", + saved: "Preferences saved successfully.", + modelNotRegistered: 'Model "{model}" is not registered for provider "{provider}".', + networkError: "Network error: Failed to reach backend.", + }, + }, + + // ── Code panel ───────────────────────────────────────────────────────────── + code: { + title: "Unity C# Code", + subtitle: "Generate Unity C# scripts using AI.", + fields: { + prompt: "Prompt", + maxTokens: "Max Tokens", + }, + actions: { + generate: "Generate Code", + }, + activeProject: "Active Project: {name}", + autoSave: "Auto-save to project", + }, + + // ── Text panel ───────────────────────────────────────────────────────────── + text: { + title: "Text Generation", + subtitle: "Generate text content using AI.", + fields: { + prompt: "Prompt", + length: "Length", + }, + actions: { + generate: "Generate Text", + }, + }, + + // ── Image panel ──────────────────────────────────────────────────────────── + image: { + title: "Image Generation", + subtitle: "Generate images using AI.", + fields: { + prompt: "Prompt", + aspectRatio: "Aspect Ratio", + quality: "Quality", + textureName: "Texture Name", + textureType: "Texture Type", + }, + actions: { + generate: "Generate Image", + saveToUnity: "Save to Unity", + }, + }, + + // ── Sprites panel ────────────────────────────────────────────────────────── + sprites: { + title: "2D Sprites", + badge: "Pixel Art Optimized", + subtitle: "Generate 2D sprite assets for Unity.", + fields: { + prompt: "Prompt", + resolution: "Resolution", + paletteSize: "Palette Size", + autoCrop: "Auto-Crop Transparent Edges", + colors: "{n} colors", + }, + preview: "Preview Area", + actions: { + generate: "Generate Sprites", + }, + }, + + // ── Audio panel ──────────────────────────────────────────────────────────── + audio: { + title: "Audio Generation", + subtitle: "Generate audio assets using AI.", + generationType: "Generation Type", + speechTts: "Speech (TTS)", + atmosphericMusic: "Atmospheric Music", + fields: { + prompt: "Prompt", + musicDescription: "Music Description", + speechPrompt: "Speech Prompt", + voiceOptional: "Voice (optional)", + musicModel: "Music Model", + stability: "Stability", + audioClipName: "Audio Clip Name", + audioFormat: "Audio Format", + }, + actions: { + generate: "Generate Audio", + saveToUnity: "Save to Unity", + }, + }, + + // ── Scenes panel ────────────────────────────────────────────────────────── + scenes: { + title: "Scene Creator", + subtitle: "Generate Unity scenes using AI.", + fields: { + prompt: "Scene Description", + applyViaMcp: "Apply via MCP tools", + }, + actions: { + generate: "Create Scene", + }, + mediaReady: "Media Ready to Import", + mediaReadyText: "{type} \"{name}\" is ready to be imported to Unity. Review the prompt below and click \"Create Scene\" to proceed.", + mediaTypeImage: "Image", + mediaTypeAudio: "Audio", + mcpLiveEditor: "Unity MCP is live with Editor-backed tools available.", + mcpLiveFileOnly: "Unity MCP is live in file-only mode. Editor-gated tools will be skipped.", + }, + + // ── Unity UI panel ───────────────────────────────────────────────────────── + unityUi: { + title: "Unity UI", + subtitle: "Generate Unity UI components using AI.", + fields: { + uiSystem: "UI System", + elementType: "Element Type", + prompt: "UI Element Description", + outputFormat: "Output Format", + anchorPreset: "Anchor Preset", + colourTheme: "Colour Theme (optional)", + includeAnimations: "Include animations", + applyViaMcp: "Apply via MCP tools", + }, + actions: { + generate: "Generate UI", + }, + mcpLiveEditor: "Unity MCP is live with Editor-backed tools available.", + mcpLiveFileOnly: "Unity MCP is live in file-only mode. File-only UI is a compatibility-surrogate.", + }, + + // ── Unity Physics panel ──────────────────────────────────────────────────── + unityPhysics: { + title: "Unity Physics", + subtitle: "Generate Unity physics configuration — Rigidbodies, colliders, gravity, and more.", + fields: { + physicsBackend: "Physics Backend", + simulationMode: "Simulation Mode", + gravityPreset: "Gravity Preset", + prompt: "Physics Description", + includeRigidbody: "Include Rigidbody setup", + includeColliders: "Include Collider setup", + includeLayers: "Include Physics Layers", + applyViaMcp: "Apply via MCP tools", + }, + actions: { + generate: "Generate Physics Config", + }, + quickActions: "Quick Actions", + mcpLiveEditor: "Unity MCP is live with Editor-backed tools available.", + mcpLiveFileOnly: "Unity MCP is live in file-only mode. File-only physics is a compatibility-surrogate.", + }, + + wizard: { + title: "Unity Wizard", + subtitle: "One goal, multi-tool orchestration across the full MCP surface.", + mcpReady: "Unity MCP is available for wizard orchestration.", + actions: { run: "Run Wizard" }, + }, + + unityMcpHub: { + title: "Unity MCP Hub", + subtitle: "Focused panels for engine automation — not an 89-tool explorer.", + statusOnline: "Unity MCP server is reachable.", + statusOffline: "Unity MCP server is not configured or unreachable.", + toolCount: "{n} typed tools registered", + cards: { + wizard: "Wizard", + scenes: "Scenes", + lighting: "Lighting", + camera: "Camera", + materials: "Materials", + animations: "Animations", + ui: "Unity UI", + physics: "Physics", + }, + descriptions: { + wizard: "Multi-step orchestration with the full tool surface.", + scenes: "Create and edit scenes and GameObjects.", + lighting: "Directional, point, spot, and area lights.", + camera: "Main and gameplay camera setups.", + materials: "Materials and texture assignment.", + animations: "Animators and Timeline assets.", + ui: "Canvas and UI Toolkit layouts.", + physics: "PhysX and DOTS physics configuration.", + }, + }, + + unityLighting: { + title: "Unity Lighting", + subtitle: "Configure lights via unity_light_* MCP tools.", + quickActions: "Quick Actions", + promptPlaceholder: "Describe the lighting setup you want...", + applyViaMcp: "Apply via MCP tools", + mcpLiveEditor: "Unity MCP is live with Editor-backed lighting tools.", + mcpLiveFileOnly: "Unity MCP is live in file-only mode for lighting surrogates.", + actions: { generate: "Generate Lighting" }, + }, + + unityCamera: { + title: "Unity Camera", + subtitle: "Configure cameras via unity_camera_* MCP tools.", + quickActions: "Quick Actions", + promptPlaceholder: "Describe the camera setup you want...", + applyViaMcp: "Apply via MCP tools", + mcpLiveEditor: "Unity MCP is live with Editor-backed camera tools.", + mcpLiveFileOnly: "Unity MCP is live in file-only mode for camera surrogates.", + actions: { generate: "Generate Camera Setup" }, + }, + + unityMaterials: { + title: "Unity Materials", + subtitle: "Create materials via unity_create_material and related tools.", + quickActions: "Quick Actions", + promptPlaceholder: "Describe the materials you want...", + applyViaMcp: "Apply via MCP tools", + mcpLiveEditor: "Unity MCP is live with material tools.", + mcpLiveFileOnly: "Unity MCP is live in file-only mode for material surrogates.", + actions: { generate: "Generate Materials" }, + }, + + unityAnimation: { + title: "Unity Animations", + subtitle: "Animators and timelines via animation MCP tools.", + quickActions: "Quick Actions", + promptPlaceholder: "Describe the animation or timeline you want...", + applyViaMcp: "Apply via MCP tools", + mcpLiveEditor: "Unity MCP is live with animation tools.", + mcpLiveFileOnly: "Unity MCP is live in file-only mode for animation surrogates.", + actions: { generate: "Generate Animation" }, + }, + + video: { + title: "Video", + subtitle: "Generate cinematic video prompts and clips (provider stub until connected).", + duration: "Duration (seconds)", + resolution: "Resolution", + aspectRatio: "Aspect ratio", + actions: { generate: "Generate Video" }, + }, + + logs: { + title: "Logs", + subtitle: "Backend generation log tail.", + refresh: "Refresh", + loadFailed: "Could not load logs.", + }, + + // ── Unity Project panel ──────────────────────────────────────────────────── + unityProject: { + title: "Unity Project", + subtitle: "Generate a complete Unity project structure.", + engineSettings: "Unity Engine Settings", + fields: { + projectName: "Project Name", + unityTemplate: "Unity Template", + unityVersion: "Unity Version", + targetPlatform: "Target Platform", + generateDefaultScene: "Generate Default Scene", + autoInstallPackages: "Auto-Install UPM Packages", + setupUrp: "Setup URP", + upmPackages: "UPM Packages (comma-separated)", + sceneName: "Scene Name", + unityEditorPath: "Unity Editor Path (optional)", + timeout: "Timeout (seconds)", + automationMode: "Automation mode", + resultJson: "Result (JSON)", + }, + mcp: { + title: "Unity MCP integration", + editorReady: "Unity MCP server is reachable and Unity Editor is connected.", + editorRequired: + "Unity MCP server is reachable, but Unity Editor is not connected. Open your project in Unity Editor, then refresh.", + notConfigured: + "Unity MCP server is not reachable from the backend. Ensure unity-mcp is on PATH or set UNITY_MCP_COMMAND.", + standaloneNote: + "Unity Generator runs as a standalone app (not inside Unity). MCP automates an open Unity Editor session.", + refresh: "Refresh MCP status", + }, + actions: { + generate: "Generate Project", + finalize: "Finalize Project", + openFolder: "Open Output Folder", + addVersion: "Add Unity Version", + downloadZip: "Download Finalized Project (.zip)", + }, + dialogs: { + addVersion: { + title: "Add Unity Version", + versionId: "Version ID", + label: "Label (optional)", + }, + }, + }, + + // ── Theme toggle ─────────────────────────────────────────────────────────── + theme: { + light: "Light", + dark: "Dark", + system: "System", + }, + + // ── Language names (shown in selector) ──────────────────────────────────── + languages: { + en: "English", + es: "Spanish", + ca: "Catalan", + eu: "Basque", + oc: "Occitan", + uk: "Ukrainian", + pt: "Portuguese", + gl: "Galician", + fr: "French", + it: "Italian", + pl: "Polish", + zh: "Chinese", + ar: "Arabic", + de: "German", + hi: "Hindi", + bn: "Bengali", + ur: "Urdu", + id: "Indonesian", + ja: "Japanese", + vi: "Vietnamese", + ko: "Korean", + tr: "Turkish", + }, +} as const; diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 6cb8e27b..65d70d13 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -1,304 +1,312 @@ -/** - * Spanish (es) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { - online: "En línea", - offline: "Sin conexión", - }, - nav: { - settings: "Ajustes", - scenes: "Escenas", - code: "Código", - text: "Texto", - image: "Imagen", - sprites: "Sprites", - audio: "Audio", - unityUi: "Unity UI", - unityPhysics: "Unity Física", - unityProject: "Proyecto Unity", - }, - actions: { - repository: "Repositorio", - }, - }, - - common: { - loading: "Cargando…", - saving: "Guardando…", - save: "Guardar", - cancel: "Cancelar", - close: "Cerrar", - ok: "Aceptar", - yes: "Sí", - no: "No", - error: "Error", - warning: "Advertencia", - success: "Éxito", - generate: "Generar", - result: "Resultado", - provider: "Proveedor", - model: "Modelo", - temperature: "Temperatura", - apiKey: "Clave API (anulación opcional)", - systemPrompt: "Anulación del prompt de sistema", - advancedOptions: "Opciones avanzadas", - examplePrompts: "Ejemplos de prompts", - manageModels: "Gestionar modelos", - selectProvider: "Seleccionar proveedor", - selectModel: "Seleccionar modelo", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Dejar vacío para usar la clave global", - createdFiles: "Archivos creados", - viewSteps: "Ver pasos ({n})", - waitingForLogs: "Esperando registros…", - errorsDetected: "Errores detectados", - }, - - settings: { - title: "Configuración", - subtitle: "Gestiona tus motores de inteligencia, modelos y prompts de sistema.", - tabs: { - general: "General", - providers: "Proveedores", - models: "Modelos", - prompts: "Prompts", - secrets: "Secretos", - }, - }, - - general: { - title: "Preferencias generales", - sections: { - networkApi: "Red y API", - preferredIntelligence: "Inteligencia preferida", - appearance: "Apariencia", - language: "Idioma", - }, - fields: { - backendUrl: "URL del backend", - backendUrlHint: "La dirección de tu servicio backend de Unity Generator", - outputBasePath: "Ruta base (salida)", - outputBasePathHint: "Dónde se guardan los archivos generados", - unityEditorPath: "Ruta del editor Unity (opcional)", - unityEditorPathHint: "Ruta completa al ejecutable del editor Unity", - preferredLlm: "Proveedor de texto / lógica predeterminado", - preferredLlmModel: "Modelo de texto / lógica predeterminado", - preferredImage: "Proveedor de generación de imágenes predeterminado", - preferredImageModel: "Modelo de generación de imágenes predeterminado", - preferredAudio: "Proveedor de voz (TTS) predeterminado", - preferredAudioModel: "Modelo de voz (TTS) predeterminado", - preferredMusic: "Proveedor de música predeterminado", - preferredMusicModel: "Modelo de música predeterminado", - theme: "Tema", - language: "Idioma de la interfaz", - }, - actions: { - saveAll: "Guardar cambios", - }, - status: { - saving: "Guardando…", - saved: "Preferencias guardadas correctamente.", - modelNotRegistered: 'El modelo "{model}" no está registrado para el proveedor "{provider}".', - networkError: "Error de red: no se puede conectar con el backend.", - }, - }, - - code: { - title: "Código C# para Unity", - subtitle: "Genera scripts C# para Unity usando IA.", - fields: { - prompt: "Prompt", - maxTokens: "Tokens máximos", - }, - actions: { - generate: "Generar código", - }, - activeProject: "Proyecto activo: {name}", - autoSave: "Guardar automáticamente en el proyecto", - }, - - text: { - title: "Generación de texto", - subtitle: "Genera contenido de texto usando IA.", - fields: { - prompt: "Prompt", - length: "Longitud", - }, - actions: { - generate: "Generar texto", - }, - }, - - image: { - title: "Generación de imágenes", - subtitle: "Genera imágenes usando IA.", - fields: { - prompt: "Prompt", - aspectRatio: "Relación de aspecto", - quality: "Calidad", - textureName: "Nombre de textura", - textureType: "Tipo de textura", - }, - actions: { - generate: "Generar imagen", - saveToUnity: "Guardar en Unity", - }, - }, - - sprites: { - title: "Sprites 2D", - badge: "Optimizado para Pixel Art", - subtitle: "Genera sprites 2D para Unity.", - fields: { - prompt: "Prompt", - resolution: "Resolución", - paletteSize: "Tamaño de paleta", - autoCrop: "Recorte automático de bordes transparentes", - colors: "{n} colores", - }, - preview: "Área de previsualización", - actions: { - generate: "Generar sprites", - }, - }, - - audio: { - title: "Generación de audio", - subtitle: "Genera recursos de audio usando IA.", - generationType: "Tipo de generación", - speechTts: "Voz (TTS)", - atmosphericMusic: "Música ambiental", - fields: { - prompt: "Prompt", - musicDescription: "Descripción musical", - speechPrompt: "Prompt de voz", - voiceOptional: "Voz (opcional)", - musicModel: "Modelo de música", - stability: "Estabilidad", - audioClipName: "Nombre del clip de audio", - audioFormat: "Formato de audio", - }, - actions: { - generate: "Generar audio", - saveToUnity: "Guardar en Unity", - }, - }, - - scenes: { - title: "Creador de escenas", - subtitle: "Genera escenas de Unity usando IA.", - fields: { - prompt: "Descripción de la escena", - }, - actions: { - generate: "Crear escena", - }, - mediaReady: "Multimedia lista para importar", - mediaReadyText: "{type} \"{name}\" está lista para importarse a Unity. Revisa el prompt y haz clic en \"Crear escena\" para continuar.", - mediaTypeImage: "Imagen", - mediaTypeAudio: "Audio", - }, - - unityUi: { - title: "Unity UI", - subtitle: "Genera componentes de UI para Unity usando IA.", - fields: { - uiSystem: "Sistema de UI", - elementType: "Tipo de elemento", - prompt: "Descripción del elemento UI", - outputFormat: "Formato de salida", - anchorPreset: "Preset de anclaje", - colourTheme: "Tema de color (opcional)", - includeAnimations: "Incluir animaciones", - }, - actions: { - generate: "Generar UI", - }, - }, - - unityPhysics: { - title: "Unity Física", - subtitle: "Genera configuración de física para Unity — Rigidbodies, colisionadores, gravedad y más.", - fields: { - physicsBackend: "Motor de física", - simulationMode: "Modo de simulación", - gravityPreset: "Preset de gravedad", - prompt: "Descripción de la física", - includeRigidbody: "Incluir configuración de Rigidbody", - includeColliders: "Incluir configuración de colisionadores", - includeLayers: "Incluir capas de física", - }, - actions: { - generate: "Generar configuración de física", - }, - quickActions: "Acciones rápidas", - }, - - unityProject: { - title: "Proyecto Unity", - subtitle: "Genera una estructura completa de proyecto Unity.", - engineSettings: "Configuración del motor Unity", - fields: { - projectName: "Nombre del proyecto", - unityTemplate: "Plantilla de Unity", - unityVersion: "Versión de Unity", - targetPlatform: "Plataforma de destino", - generateDefaultScene: "Generar escena predeterminada", - autoInstallPackages: "Instalar paquetes UPM automáticamente", - setupUrp: "Configurar URP", - upmPackages: "Paquetes UPM (separados por comas)", - sceneName: "Nombre de la escena", - unityEditorPath: "Ruta del editor Unity (opcional)", - timeout: "Tiempo de espera (segundos)", - resultJson: "Resultado (JSON)", - }, - actions: { - generate: "Generar proyecto", - finalize: "Finalizar proyecto", - openFolder: "Abrir carpeta de salida", - addVersion: "Añadir versión de Unity", - downloadZip: "Descargar proyecto finalizado (.zip)", - }, - dialogs: { - addVersion: { - title: "Añadir versión de Unity", - versionId: "ID de versión", - label: "Etiqueta (opcional)", - }, - }, - }, - - theme: { - light: "Claro", - dark: "Oscuro", - system: "Sistema", - }, - - languages: { - en: "Inglés", - es: "Español", - ca: "Catalán", - eu: "Euskera", - oc: "Occitano", - uk: "Ucraniano", - pt: "Portugués", - gl: "Gallego", - fr: "Francés", - it: "Italiano", - pl: "Polaco", - zh: "Chino", - ar: "Árabe", - de: "Alemán", - hi: "Hindi", - bn: "Bengalí", - ur: "Urdu", - id: "Indonesio", - ja: "Japonés", - vi: "Vietnamita", - ko: "Coreano", - tr: "Turco", - }, -} as const; +/** + * Spanish (es) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { + online: "En línea", + offline: "Sin conexión", + }, + nav: { + settings: "Ajustes", + scenes: "Escenas", + code: "Código", + text: "Texto", + image: "Imagen", + sprites: "Sprites", + audio: "Audio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Física", + unityProject: "Proyecto Unity", + logs: "Logs", + }, + actions: { + repository: "Repositorio", + }, + }, + + common: { + loading: "Cargando…", + saving: "Guardando…", + save: "Guardar", + cancel: "Cancelar", + close: "Cerrar", + ok: "Aceptar", + yes: "Sí", + no: "No", + error: "Error", + warning: "Advertencia", + success: "Éxito", + generate: "Generar", + result: "Resultado", + provider: "Proveedor", + model: "Modelo", + temperature: "Temperatura", + apiKey: "Clave API (anulación opcional)", + systemPrompt: "Anulación del prompt de sistema", + advancedOptions: "Opciones avanzadas", + examplePrompts: "Ejemplos de prompts", + manageModels: "Gestionar modelos", + selectProvider: "Seleccionar proveedor", + selectModel: "Seleccionar modelo", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Dejar vacío para usar la clave global", + createdFiles: "Archivos creados", + viewSteps: "Ver pasos ({n})", + waitingForLogs: "Esperando registros…", + errorsDetected: "Errores detectados", + }, + + settings: { + title: "Configuración", + subtitle: "Gestiona tus motores de inteligencia, modelos y prompts de sistema.", + tabs: { + general: "General", + providers: "Proveedores", + models: "Modelos", + prompts: "Prompts", + secrets: "Secretos", + }, + }, + + general: { + title: "Preferencias generales", + sections: { + networkApi: "Red y API", + preferredIntelligence: "Inteligencia preferida", + appearance: "Apariencia", + language: "Idioma", + }, + fields: { + backendUrl: "URL del backend", + backendUrlHint: "La dirección de tu servicio backend de Unity Generator", + outputBasePath: "Ruta base (salida)", + outputBasePathHint: "Dónde se guardan los archivos generados", + unityEditorPath: "Ruta del editor Unity (opcional)", + unityEditorPathHint: "Ruta completa al ejecutable del editor Unity", + preferredLlm: "Proveedor de texto / lógica predeterminado", + preferredLlmModel: "Modelo de texto / lógica predeterminado", + preferredImage: "Proveedor de generación de imágenes predeterminado", + preferredImageModel: "Modelo de generación de imágenes predeterminado", + preferredAudio: "Proveedor de voz (TTS) predeterminado", + preferredAudioModel: "Modelo de voz (TTS) predeterminado", + preferredMusic: "Proveedor de música predeterminado", + preferredMusicModel: "Modelo de música predeterminado", + theme: "Tema", + language: "Idioma de la interfaz", + }, + actions: { + saveAll: "Guardar cambios", + }, + status: { + saving: "Guardando…", + saved: "Preferencias guardadas correctamente.", + modelNotRegistered: 'El modelo "{model}" no está registrado para el proveedor "{provider}".', + networkError: "Error de red: no se puede conectar con el backend.", + }, + }, + + code: { + title: "Código C# para Unity", + subtitle: "Genera scripts C# para Unity usando IA.", + fields: { + prompt: "Prompt", + maxTokens: "Tokens máximos", + }, + actions: { + generate: "Generar código", + }, + activeProject: "Proyecto activo: {name}", + autoSave: "Guardar automáticamente en el proyecto", + }, + + text: { + title: "Generación de texto", + subtitle: "Genera contenido de texto usando IA.", + fields: { + prompt: "Prompt", + length: "Longitud", + }, + actions: { + generate: "Generar texto", + }, + }, + + image: { + title: "Generación de imágenes", + subtitle: "Genera imágenes usando IA.", + fields: { + prompt: "Prompt", + aspectRatio: "Relación de aspecto", + quality: "Calidad", + textureName: "Nombre de textura", + textureType: "Tipo de textura", + }, + actions: { + generate: "Generar imagen", + saveToUnity: "Guardar en Unity", + }, + }, + + sprites: { + title: "Sprites 2D", + badge: "Optimizado para Pixel Art", + subtitle: "Genera sprites 2D para Unity.", + fields: { + prompt: "Prompt", + resolution: "Resolución", + paletteSize: "Tamaño de paleta", + autoCrop: "Recorte automático de bordes transparentes", + colors: "{n} colores", + }, + preview: "Área de previsualización", + actions: { + generate: "Generar sprites", + }, + }, + + audio: { + title: "Generación de audio", + subtitle: "Genera recursos de audio usando IA.", + generationType: "Tipo de generación", + speechTts: "Voz (TTS)", + atmosphericMusic: "Música ambiental", + fields: { + prompt: "Prompt", + musicDescription: "Descripción musical", + speechPrompt: "Prompt de voz", + voiceOptional: "Voz (opcional)", + musicModel: "Modelo de música", + stability: "Estabilidad", + audioClipName: "Nombre del clip de audio", + audioFormat: "Formato de audio", + }, + actions: { + generate: "Generar audio", + saveToUnity: "Guardar en Unity", + }, + }, + + scenes: { + title: "Creador de escenas", + subtitle: "Genera escenas de Unity usando IA.", + fields: { + prompt: "Descripción de la escena", + }, + actions: { + generate: "Crear escena", + }, + mediaReady: "Multimedia lista para importar", + mediaReadyText: "{type} \"{name}\" está lista para importarse a Unity. Revisa el prompt y haz clic en \"Crear escena\" para continuar.", + mediaTypeImage: "Imagen", + mediaTypeAudio: "Audio", + }, + + unityUi: { + title: "Unity UI", + subtitle: "Genera componentes de UI para Unity usando IA.", + fields: { + uiSystem: "Sistema de UI", + elementType: "Tipo de elemento", + prompt: "Descripción del elemento UI", + outputFormat: "Formato de salida", + anchorPreset: "Preset de anclaje", + colourTheme: "Tema de color (opcional)", + includeAnimations: "Incluir animaciones", + }, + actions: { + generate: "Generar UI", + }, + }, + + unityPhysics: { + title: "Unity Física", + subtitle: "Genera configuración de física para Unity — Rigidbodies, colisionadores, gravedad y más.", + fields: { + physicsBackend: "Motor de física", + simulationMode: "Modo de simulación", + gravityPreset: "Preset de gravedad", + prompt: "Descripción de la física", + includeRigidbody: "Incluir configuración de Rigidbody", + includeColliders: "Incluir configuración de colisionadores", + includeLayers: "Incluir capas de física", + }, + actions: { + generate: "Generar configuración de física", + }, + quickActions: "Acciones rápidas", + }, + + unityProject: { + title: "Proyecto Unity", + subtitle: "Genera una estructura completa de proyecto Unity.", + engineSettings: "Configuración del motor Unity", + fields: { + projectName: "Nombre del proyecto", + unityTemplate: "Plantilla de Unity", + unityVersion: "Versión de Unity", + targetPlatform: "Plataforma de destino", + generateDefaultScene: "Generar escena predeterminada", + autoInstallPackages: "Instalar paquetes UPM automáticamente", + setupUrp: "Configurar URP", + upmPackages: "Paquetes UPM (separados por comas)", + sceneName: "Nombre de la escena", + unityEditorPath: "Ruta del editor Unity (opcional)", + timeout: "Tiempo de espera (segundos)", + resultJson: "Resultado (JSON)", + }, + actions: { + generate: "Generar proyecto", + finalize: "Finalizar proyecto", + openFolder: "Abrir carpeta de salida", + addVersion: "Añadir versión de Unity", + downloadZip: "Descargar proyecto finalizado (.zip)", + }, + dialogs: { + addVersion: { + title: "Añadir versión de Unity", + versionId: "ID de versión", + label: "Etiqueta (opcional)", + }, + }, + }, + + theme: { + light: "Claro", + dark: "Oscuro", + system: "Sistema", + }, + + languages: { + en: "Inglés", + es: "Español", + ca: "Catalán", + eu: "Euskera", + oc: "Occitano", + uk: "Ucraniano", + pt: "Portugués", + gl: "Gallego", + fr: "Francés", + it: "Italiano", + pl: "Polaco", + zh: "Chino", + ar: "Árabe", + de: "Alemán", + hi: "Hindi", + bn: "Bengalí", + ur: "Urdu", + id: "Indonesio", + ja: "Japonés", + vi: "Vietnamita", + ko: "Coreano", + tr: "Turco", + }, +} as const; diff --git a/frontend/src/i18n/locales/eu.ts b/frontend/src/i18n/locales/eu.ts index 978041af..bdab3c98 100644 --- a/frontend/src/i18n/locales/eu.ts +++ b/frontend/src/i18n/locales/eu.ts @@ -1,304 +1,312 @@ -/** - * Basque (eu) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { - online: "Konektatuta", - offline: "Deskonektatuta", - }, - nav: { - settings: "Ezarpenak", - scenes: "Eszenak", - code: "Kodea", - text: "Testua", - image: "Irudia", - sprites: "Sprites", - audio: "Audioa", - unityUi: "Unity UI", - unityPhysics: "Unity Fisika", - unityProject: "Unity Proiektua", - }, - actions: { - repository: "Biltegia", - }, - }, - - common: { - loading: "Kargatzen…", - saving: "Gordetzen…", - save: "Gorde", - cancel: "Utzi", - close: "Itxi", - ok: "Ados", - yes: "Bai", - no: "Ez", - error: "Errorea", - warning: "Abisua", - success: "Arrakasta", - generate: "Sortu", - result: "Emaitza", - provider: "Hornitzailea", - model: "Eredua", - temperature: "Tenperatura", - apiKey: "API Gakoa (aukerako gainidazketa)", - systemPrompt: "Sistema-promptaren gainidazketa", - advancedOptions: "Aukera aurreratuak", - examplePrompts: "Prompt adibideak", - manageModels: "Ereduak kudeatu", - selectProvider: "Hautatu hornitzailea", - selectModel: "Hautatu eredua", - prompt: "Promptа", - leaveEmptyForGlobalKey: "Utzi hutsik gako globala erabiltzeko", - createdFiles: "Sortutako fitxategiak", - viewSteps: "Urratsak ikusi ({n})", - waitingForLogs: "Erregistroak zain…", - errorsDetected: "Erroreak hauteman dira", - }, - - settings: { - title: "Konfigurazioa", - subtitle: "Kudeatu zure adimen-motorrak, ereduak eta sistema-promptak.", - tabs: { - general: "Orokorra", - providers: "Hornitzaileak", - models: "Ereduak", - prompts: "Promptak", - secrets: "Sekretuak", - }, - }, - - general: { - title: "Hobespen orokorrak", - sections: { - networkApi: "Sarea eta API", - preferredIntelligence: "Adimen hobetsia", - appearance: "Itxura", - language: "Hizkuntza", - }, - fields: { - backendUrl: "Backend URLa", - backendUrlHint: "Zure Unity Generator backend zerbitzuaren helbidea", - outputBasePath: "Oinarrizko bidea (irteera)", - outputBasePathHint: "Sortutako fitxategiak non gordetzen diren", - unityEditorPath: "Unity editoraren bidea (aukerakoa)", - unityEditorPathHint: "Unity editoraren exekutagarriaren bide osoa", - preferredLlm: "Testu/logika hornitzaile lehenetsia", - preferredLlmModel: "Testu/logika eredu lehenetsia", - preferredImage: "Irudi-sorkuntza hornitzaile lehenetsia", - preferredImageModel: "Irudi-sorkuntza eredu lehenetsia", - preferredAudio: "Ahotsa (TTS) hornitzaile lehenetsia", - preferredAudioModel: "Ahotsa (TTS) eredu lehenetsia", - preferredMusic: "Musika hornitzaile lehenetsia", - preferredMusicModel: "Musika eredu lehenetsia", - theme: "Gaia", - language: "Interfazearen hizkuntza", - }, - actions: { - saveAll: "Aldaketa guztiak gorde", - }, - status: { - saving: "Gordetzen…", - saved: "Hobespenak ondo gorde dira.", - modelNotRegistered: '"{model}" eredua ez dago "{provider}" hornitzailean erregistratuta.', - networkError: "Sare-errorea: ezin da backendarekin konektatu.", - }, - }, - - code: { - title: "Unity C# Kodea", - subtitle: "Sortu Unity C# scriptak AI erabiliz.", - fields: { - prompt: "Promptа", - maxTokens: "Token gehienekoa", - }, - actions: { - generate: "Kodea sortu", - }, - activeProject: "Proiektu aktiboa: {name}", - autoSave: "Automatikoki gorde proiektuan", - }, - - text: { - title: "Testu-sorkuntza", - subtitle: "Sortu testu-edukia AI erabiliz.", - fields: { - prompt: "Promptа", - length: "Luzera", - }, - actions: { - generate: "Testua sortu", - }, - }, - - image: { - title: "Irudi-sorkuntza", - subtitle: "Sortu irudiak AI erabiliz.", - fields: { - prompt: "Promptа", - aspectRatio: "Alderdi-erlazioa", - quality: "Kalitatea", - textureName: "Ehunduraren izena", - textureType: "Ehundura mota", - }, - actions: { - generate: "Irudia sortu", - saveToUnity: "Unity-n gorde", - }, - }, - - sprites: { - title: "2D Sprites", - badge: "Pixel Art-erako optimizatua", - subtitle: "Sortu Unity-rako 2D sprite aktiboak.", - fields: { - prompt: "Promptа", - resolution: "Bereizmena", - paletteSize: "Paleta-tamaina", - autoCrop: "Ertz garden automatikoa moztu", - colors: "{n} kolore", - }, - preview: "Aurrebista-eremua", - actions: { - generate: "Sprites sortu", - }, - }, - - audio: { - title: "Audio-sorkuntza", - subtitle: "Sortu audio-aktiboak AI erabiliz.", - generationType: "Sorkuntza mota", - speechTts: "Ahotsa (TTS)", - atmosphericMusic: "Giro-musika", - fields: { - prompt: "Promptа", - musicDescription: "Musika-deskribapena", - speechPrompt: "Ahots-promptа", - voiceOptional: "Ahotsa (aukerakoa)", - musicModel: "Musika-eredua", - stability: "Egonkortasuna", - audioClipName: "Audio kliparen izena", - audioFormat: "Audio formatua", - }, - actions: { - generate: "Audioa sortu", - saveToUnity: "Unity-n gorde", - }, - }, - - scenes: { - title: "Eszena-sortzailea", - subtitle: "Sortu Unity eszenak AI erabiliz.", - fields: { - prompt: "Eszena-deskribapena", - }, - actions: { - generate: "Eszena sortu", - }, - mediaReady: "Multimedia inportatzeko prest", - mediaReadyText: "{type} \"{name}\" Unity-n inportatzeko prest dago. Berrikusi promptа eta egin klik \"Eszena sortu\" jarraitzeko.", - mediaTypeImage: "Irudia", - mediaTypeAudio: "Audioa", - }, - - unityUi: { - title: "Unity UI", - subtitle: "Sortu Unity UI osagaiak AI erabiliz.", - fields: { - uiSystem: "UI sistema", - elementType: "Elementu mota", - prompt: "UI elementuaren deskribapena", - outputFormat: "Irteera formatua", - anchorPreset: "Aingura aurrezarria", - colourTheme: "Kolore-gaia (aukerakoa)", - includeAnimations: "Animazioak sartu", - }, - actions: { - generate: "UI sortu", - }, - }, - - unityPhysics: { - title: "Unity Fisika", - subtitle: "Sortu Unity fisika-konfigurazioa — Rigidbodies, kolisagailuak, grabitatea eta gehiago.", - fields: { - physicsBackend: "Fisika-motorra", - simulationMode: "Simulazio modua", - gravityPreset: "Grabitate aurrezarria", - prompt: "Fisika-deskribapena", - includeRigidbody: "Rigidbody konfigurazioa sartu", - includeColliders: "Kolisagailu konfigurazioa sartu", - includeLayers: "Fisika-geruzak sartu", - }, - actions: { - generate: "Fisika-konfigurazioa sortu", - }, - quickActions: "Ekintza azkarrak", - }, - - unityProject: { - title: "Unity Proiektua", - subtitle: "Sortu Unity proiektu-egitura osoa.", - engineSettings: "Unity motor-ezarpenak", - fields: { - projectName: "Proiektuaren izena", - unityTemplate: "Unity txantiloia", - unityVersion: "Unity bertsioa", - targetPlatform: "Helburu-plataforma", - generateDefaultScene: "Lehenetsitako eszena sortu", - autoInstallPackages: "UPM paketeak automatikoki instalatu", - setupUrp: "URP konfiguratu", - upmPackages: "UPM paketeak (komaz bereizita)", - sceneName: "Eszenaren izena", - unityEditorPath: "Unity editoraren bidea (aukerakoa)", - timeout: "Denbora-muga (segundoak)", - resultJson: "Emaitza (JSON)", - }, - actions: { - generate: "Proiektua sortu", - finalize: "Proiektua amaitu", - openFolder: "Irteera-karpeta ireki", - addVersion: "Unity bertsioa gehitu", - downloadZip: "Amaitutako proiektua deskargatu (.zip)", - }, - dialogs: { - addVersion: { - title: "Unity bertsioa gehitu", - versionId: "Bertsio IDa", - label: "Etiketa (aukerakoa)", - }, - }, - }, - - theme: { - light: "Argia", - dark: "Iluna", - system: "Sistema", - }, - - languages: { - en: "Ingelesa", - es: "Gaztelania", - ca: "Katalana", - eu: "Euskara", - oc: "Okzitaniera", - uk: "Ukrainera", - pt: "Portugesa", - gl: "Galiziera", - fr: "Frantsesa", - it: "Italiera", - pl: "Poloniera", - zh: "Txinera", - ar: "Arabiera", - de: "Alemana", - hi: "Hindia", - bn: "Bengalera", - ur: "Urdua", - id: "Indonesiera", - ja: "Japoniera", - vi: "Vietnamera", - ko: "Korear", - tr: "Turkiera", - }, -} as const; +/** + * Basque (eu) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { + online: "Konektatuta", + offline: "Deskonektatuta", + }, + nav: { + settings: "Ezarpenak", + scenes: "Eszenak", + code: "Kodea", + text: "Testua", + image: "Irudia", + sprites: "Sprites", + audio: "Audioa", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Fisika", + unityProject: "Unity Proiektua", + logs: "Logs", + }, + actions: { + repository: "Biltegia", + }, + }, + + common: { + loading: "Kargatzen…", + saving: "Gordetzen…", + save: "Gorde", + cancel: "Utzi", + close: "Itxi", + ok: "Ados", + yes: "Bai", + no: "Ez", + error: "Errorea", + warning: "Abisua", + success: "Arrakasta", + generate: "Sortu", + result: "Emaitza", + provider: "Hornitzailea", + model: "Eredua", + temperature: "Tenperatura", + apiKey: "API Gakoa (aukerako gainidazketa)", + systemPrompt: "Sistema-promptaren gainidazketa", + advancedOptions: "Aukera aurreratuak", + examplePrompts: "Prompt adibideak", + manageModels: "Ereduak kudeatu", + selectProvider: "Hautatu hornitzailea", + selectModel: "Hautatu eredua", + prompt: "Promptа", + leaveEmptyForGlobalKey: "Utzi hutsik gako globala erabiltzeko", + createdFiles: "Sortutako fitxategiak", + viewSteps: "Urratsak ikusi ({n})", + waitingForLogs: "Erregistroak zain…", + errorsDetected: "Erroreak hauteman dira", + }, + + settings: { + title: "Konfigurazioa", + subtitle: "Kudeatu zure adimen-motorrak, ereduak eta sistema-promptak.", + tabs: { + general: "Orokorra", + providers: "Hornitzaileak", + models: "Ereduak", + prompts: "Promptak", + secrets: "Sekretuak", + }, + }, + + general: { + title: "Hobespen orokorrak", + sections: { + networkApi: "Sarea eta API", + preferredIntelligence: "Adimen hobetsia", + appearance: "Itxura", + language: "Hizkuntza", + }, + fields: { + backendUrl: "Backend URLa", + backendUrlHint: "Zure Unity Generator backend zerbitzuaren helbidea", + outputBasePath: "Oinarrizko bidea (irteera)", + outputBasePathHint: "Sortutako fitxategiak non gordetzen diren", + unityEditorPath: "Unity editoraren bidea (aukerakoa)", + unityEditorPathHint: "Unity editoraren exekutagarriaren bide osoa", + preferredLlm: "Testu/logika hornitzaile lehenetsia", + preferredLlmModel: "Testu/logika eredu lehenetsia", + preferredImage: "Irudi-sorkuntza hornitzaile lehenetsia", + preferredImageModel: "Irudi-sorkuntza eredu lehenetsia", + preferredAudio: "Ahotsa (TTS) hornitzaile lehenetsia", + preferredAudioModel: "Ahotsa (TTS) eredu lehenetsia", + preferredMusic: "Musika hornitzaile lehenetsia", + preferredMusicModel: "Musika eredu lehenetsia", + theme: "Gaia", + language: "Interfazearen hizkuntza", + }, + actions: { + saveAll: "Aldaketa guztiak gorde", + }, + status: { + saving: "Gordetzen…", + saved: "Hobespenak ondo gorde dira.", + modelNotRegistered: '"{model}" eredua ez dago "{provider}" hornitzailean erregistratuta.', + networkError: "Sare-errorea: ezin da backendarekin konektatu.", + }, + }, + + code: { + title: "Unity C# Kodea", + subtitle: "Sortu Unity C# scriptak AI erabiliz.", + fields: { + prompt: "Promptа", + maxTokens: "Token gehienekoa", + }, + actions: { + generate: "Kodea sortu", + }, + activeProject: "Proiektu aktiboa: {name}", + autoSave: "Automatikoki gorde proiektuan", + }, + + text: { + title: "Testu-sorkuntza", + subtitle: "Sortu testu-edukia AI erabiliz.", + fields: { + prompt: "Promptа", + length: "Luzera", + }, + actions: { + generate: "Testua sortu", + }, + }, + + image: { + title: "Irudi-sorkuntza", + subtitle: "Sortu irudiak AI erabiliz.", + fields: { + prompt: "Promptа", + aspectRatio: "Alderdi-erlazioa", + quality: "Kalitatea", + textureName: "Ehunduraren izena", + textureType: "Ehundura mota", + }, + actions: { + generate: "Irudia sortu", + saveToUnity: "Unity-n gorde", + }, + }, + + sprites: { + title: "2D Sprites", + badge: "Pixel Art-erako optimizatua", + subtitle: "Sortu Unity-rako 2D sprite aktiboak.", + fields: { + prompt: "Promptа", + resolution: "Bereizmena", + paletteSize: "Paleta-tamaina", + autoCrop: "Ertz garden automatikoa moztu", + colors: "{n} kolore", + }, + preview: "Aurrebista-eremua", + actions: { + generate: "Sprites sortu", + }, + }, + + audio: { + title: "Audio-sorkuntza", + subtitle: "Sortu audio-aktiboak AI erabiliz.", + generationType: "Sorkuntza mota", + speechTts: "Ahotsa (TTS)", + atmosphericMusic: "Giro-musika", + fields: { + prompt: "Promptа", + musicDescription: "Musika-deskribapena", + speechPrompt: "Ahots-promptа", + voiceOptional: "Ahotsa (aukerakoa)", + musicModel: "Musika-eredua", + stability: "Egonkortasuna", + audioClipName: "Audio kliparen izena", + audioFormat: "Audio formatua", + }, + actions: { + generate: "Audioa sortu", + saveToUnity: "Unity-n gorde", + }, + }, + + scenes: { + title: "Eszena-sortzailea", + subtitle: "Sortu Unity eszenak AI erabiliz.", + fields: { + prompt: "Eszena-deskribapena", + }, + actions: { + generate: "Eszena sortu", + }, + mediaReady: "Multimedia inportatzeko prest", + mediaReadyText: "{type} \"{name}\" Unity-n inportatzeko prest dago. Berrikusi promptа eta egin klik \"Eszena sortu\" jarraitzeko.", + mediaTypeImage: "Irudia", + mediaTypeAudio: "Audioa", + }, + + unityUi: { + title: "Unity UI", + subtitle: "Sortu Unity UI osagaiak AI erabiliz.", + fields: { + uiSystem: "UI sistema", + elementType: "Elementu mota", + prompt: "UI elementuaren deskribapena", + outputFormat: "Irteera formatua", + anchorPreset: "Aingura aurrezarria", + colourTheme: "Kolore-gaia (aukerakoa)", + includeAnimations: "Animazioak sartu", + }, + actions: { + generate: "UI sortu", + }, + }, + + unityPhysics: { + title: "Unity Fisika", + subtitle: "Sortu Unity fisika-konfigurazioa — Rigidbodies, kolisagailuak, grabitatea eta gehiago.", + fields: { + physicsBackend: "Fisika-motorra", + simulationMode: "Simulazio modua", + gravityPreset: "Grabitate aurrezarria", + prompt: "Fisika-deskribapena", + includeRigidbody: "Rigidbody konfigurazioa sartu", + includeColliders: "Kolisagailu konfigurazioa sartu", + includeLayers: "Fisika-geruzak sartu", + }, + actions: { + generate: "Fisika-konfigurazioa sortu", + }, + quickActions: "Ekintza azkarrak", + }, + + unityProject: { + title: "Unity Proiektua", + subtitle: "Sortu Unity proiektu-egitura osoa.", + engineSettings: "Unity motor-ezarpenak", + fields: { + projectName: "Proiektuaren izena", + unityTemplate: "Unity txantiloia", + unityVersion: "Unity bertsioa", + targetPlatform: "Helburu-plataforma", + generateDefaultScene: "Lehenetsitako eszena sortu", + autoInstallPackages: "UPM paketeak automatikoki instalatu", + setupUrp: "URP konfiguratu", + upmPackages: "UPM paketeak (komaz bereizita)", + sceneName: "Eszenaren izena", + unityEditorPath: "Unity editoraren bidea (aukerakoa)", + timeout: "Denbora-muga (segundoak)", + resultJson: "Emaitza (JSON)", + }, + actions: { + generate: "Proiektua sortu", + finalize: "Proiektua amaitu", + openFolder: "Irteera-karpeta ireki", + addVersion: "Unity bertsioa gehitu", + downloadZip: "Amaitutako proiektua deskargatu (.zip)", + }, + dialogs: { + addVersion: { + title: "Unity bertsioa gehitu", + versionId: "Bertsio IDa", + label: "Etiketa (aukerakoa)", + }, + }, + }, + + theme: { + light: "Argia", + dark: "Iluna", + system: "Sistema", + }, + + languages: { + en: "Ingelesa", + es: "Gaztelania", + ca: "Katalana", + eu: "Euskara", + oc: "Okzitaniera", + uk: "Ukrainera", + pt: "Portugesa", + gl: "Galiziera", + fr: "Frantsesa", + it: "Italiera", + pl: "Poloniera", + zh: "Txinera", + ar: "Arabiera", + de: "Alemana", + hi: "Hindia", + bn: "Bengalera", + ur: "Urdua", + id: "Indonesiera", + ja: "Japoniera", + vi: "Vietnamera", + ko: "Korear", + tr: "Turkiera", + }, +} as const; diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 982bd030..9716f3cc 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1,246 +1,254 @@ -/** - * French (fr) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "En ligne", offline: "Hors ligne" }, - nav: { - settings: "Paramètres", - scenes: "Scènes", - code: "Code", - text: "Texte", - image: "Image", - sprites: "Sprites", - audio: "Audio", - unityUi: "Unity UI", - unityPhysics: "Unity Physique", - unityProject: "Projet Unity", - }, - actions: { repository: "Dépôt" }, - }, - common: { - loading: "Chargement…", - saving: "Enregistrement…", - save: "Enregistrer", - cancel: "Annuler", - close: "Fermer", - ok: "OK", - yes: "Oui", - no: "Non", - error: "Erreur", - warning: "Avertissement", - success: "Succès", - generate: "Générer", - result: "Résultat", - provider: "Fournisseur", - model: "Modèle", - temperature: "Température", - apiKey: "Clé API (substitution optionnelle)", - systemPrompt: "Substitution du prompt système", - advancedOptions: "Options avancées", - examplePrompts: "Exemples de prompts", - manageModels: "Gérer les modèles", - selectProvider: "Sélectionner un fournisseur", - selectModel: "Sélectionner un modèle", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Laisser vide pour utiliser la clé globale", - createdFiles: "Fichiers créés", - viewSteps: "Voir les étapes ({n})", - waitingForLogs: "En attente des journaux…", - errorsDetected: "Erreurs détectées", - }, - settings: { - title: "Configuration", - subtitle: "Gérez vos moteurs d'intelligence, modèles et prompts système.", - tabs: { - general: "Général", - providers: "Fournisseurs", - models: "Modèles", - prompts: "Prompts", - secrets: "Secrets", - }, - }, - general: { - title: "Préférences générales", - sections: { - networkApi: "Réseau et API", - preferredIntelligence: "Intelligence préférée", - appearance: "Apparence", - language: "Langue", - }, - fields: { - backendUrl: "URL du backend", - backendUrlHint: "L'adresse de votre service backend Unity Generator", - outputBasePath: "Chemin de base (sortie)", - outputBasePathHint: "Où les fichiers générés sont enregistrés", - unityEditorPath: "Chemin de l'éditeur Unity (optionnel)", - unityEditorPathHint: "Chemin complet vers l'exécutable de l'éditeur Unity", - preferredLlm: "Fournisseur de texte/logique par défaut", - preferredLlmModel: "Modèle de texte/logique par défaut", - preferredImage: "Fournisseur de génération d'images par défaut", - preferredImageModel: "Modèle de génération d'images par défaut", - preferredAudio: "Fournisseur de voix (TTS) par défaut", - preferredAudioModel: "Modèle de voix (TTS) par défaut", - preferredMusic: "Fournisseur de musique par défaut", - preferredMusicModel: "Modèle de musique par défaut", - theme: "Thème", - language: "Langue de l'interface", - }, - actions: { saveAll: "Enregistrer tous les changements" }, - status: { - saving: "Enregistrement…", - saved: "Préférences enregistrées avec succès.", - modelNotRegistered: 'Le modèle "{model}" n\'est pas enregistré pour le fournisseur "{provider}".', - networkError: "Erreur réseau : impossible de contacter le backend.", - }, - }, - code: { - title: "Code C# Unity", - subtitle: "Générez des scripts C# Unity avec l'IA.", - fields: { prompt: "Prompt", maxTokens: "Tokens maximum" }, - actions: { generate: "Générer le code" }, - activeProject: "Projet actif : {name}", - autoSave: "Enregistrer automatiquement dans le projet", - }, - text: { - title: "Génération de texte", - subtitle: "Générez du contenu textuel avec l'IA.", - fields: { prompt: "Prompt", length: "Longueur" }, - actions: { generate: "Générer le texte" }, - }, - image: { - title: "Génération d'images", - subtitle: "Générez des images avec l'IA.", - fields: { - prompt: "Prompt", - aspectRatio: "Rapport d'aspect", - quality: "Qualité", - textureName: "Nom de la texture", - textureType: "Type de texture", - }, - actions: { generate: "Générer l'image", saveToUnity: "Enregistrer dans Unity" }, - }, - sprites: { - title: "Sprites 2D", - badge: "Optimisé pour le Pixel Art", - subtitle: "Générez des sprites 2D pour Unity.", - fields: { - prompt: "Prompt", - resolution: "Résolution", - paletteSize: "Taille de la palette", - autoCrop: "Recadrage automatique des bords transparents", - colors: "{n} couleurs", - }, - preview: "Zone de prévisualisation", - actions: { generate: "Générer les sprites" }, - }, - audio: { - title: "Génération audio", - subtitle: "Générez des ressources audio avec l'IA.", - generationType: "Type de génération", - speechTts: "Voix (TTS)", - atmosphericMusic: "Musique d'ambiance", - fields: { - prompt: "Prompt", - musicDescription: "Description musicale", - speechPrompt: "Prompt vocal", - voiceOptional: "Voix (optionnelle)", - musicModel: "Modèle de musique", - stability: "Stabilité", - audioClipName: "Nom du clip audio", - audioFormat: "Format audio", - }, - actions: { generate: "Générer l'audio", saveToUnity: "Enregistrer dans Unity" }, - }, - scenes: { - title: "Créateur de scènes", - subtitle: "Générez des scènes Unity avec l'IA.", - fields: { prompt: "Description de la scène" }, - actions: { generate: "Créer la scène" }, - mediaReady: "Média prêt à importer", - mediaReadyText: "{type} \"{name}\" est prêt à être importé dans Unity. Révisez le prompt et cliquez sur \"Créer la scène\" pour continuer.", - mediaTypeImage: "Image", - mediaTypeAudio: "Audio", - }, - unityUi: { - title: "Unity UI", - subtitle: "Générez des composants UI Unity avec l'IA.", - fields: { - uiSystem: "Système UI", - elementType: "Type d'élément", - prompt: "Description de l'élément UI", - outputFormat: "Format de sortie", - anchorPreset: "Preset d'ancrage", - colourTheme: "Thème de couleur (optionnel)", - includeAnimations: "Inclure les animations", - }, - actions: { generate: "Générer l'UI" }, - }, - unityPhysics: { - title: "Unity Physique", - subtitle: "Générez la configuration physique Unity — Rigidbodies, colliders, gravité et plus.", - fields: { - physicsBackend: "Moteur physique", - simulationMode: "Mode de simulation", - gravityPreset: "Preset de gravité", - prompt: "Description de la physique", - includeRigidbody: "Inclure la configuration Rigidbody", - includeColliders: "Inclure la configuration des colliders", - includeLayers: "Inclure les couches physiques", - }, - actions: { generate: "Générer la configuration physique" }, - quickActions: "Actions rapides", - }, - unityProject: { - title: "Projet Unity", - subtitle: "Générez une structure complète de projet Unity.", - engineSettings: "Paramètres du moteur Unity", - fields: { - projectName: "Nom du projet", - unityTemplate: "Modèle Unity", - unityVersion: "Version Unity", - targetPlatform: "Plateforme cible", - generateDefaultScene: "Générer la scène par défaut", - autoInstallPackages: "Installer automatiquement les paquets UPM", - setupUrp: "Configurer URP", - upmPackages: "Paquets UPM (séparés par des virgules)", - sceneName: "Nom de la scène", - unityEditorPath: "Chemin de l'éditeur Unity (optionnel)", - timeout: "Délai d'attente (secondes)", - resultJson: "Résultat (JSON)", - }, - actions: { - generate: "Générer le projet", - finalize: "Finaliser le projet", - openFolder: "Ouvrir le dossier de sortie", - addVersion: "Ajouter une version Unity", - downloadZip: "Télécharger le projet finalisé (.zip)", - }, - dialogs: { - addVersion: { - title: "Ajouter une version Unity", - versionId: "ID de version", - label: "Étiquette (optionnelle)", - }, - }, - }, - theme: { light: "Clair", dark: "Sombre", system: "Système" }, - languages: { - en: "Anglais", - es: "Espagnol", - ca: "Catalan", - eu: "Basque", - oc: "Occitan", - uk: "Ukrainien", - pt: "Portugais", - gl: "Galicien", - fr: "Français", - it: "Italien", - pl: "Polonais", - zh: "Chinois", - ar: "Arabe", - de: "Allemand", - tr: "turc", - }, -} as const; +/** + * French (fr) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "En ligne", offline: "Hors ligne" }, + nav: { + settings: "Paramètres", + scenes: "Scènes", + code: "Code", + text: "Texte", + image: "Image", + sprites: "Sprites", + audio: "Audio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Physique", + unityProject: "Projet Unity", + logs: "Logs", + }, + actions: { repository: "Dépôt" }, + }, + common: { + loading: "Chargement…", + saving: "Enregistrement…", + save: "Enregistrer", + cancel: "Annuler", + close: "Fermer", + ok: "OK", + yes: "Oui", + no: "Non", + error: "Erreur", + warning: "Avertissement", + success: "Succès", + generate: "Générer", + result: "Résultat", + provider: "Fournisseur", + model: "Modèle", + temperature: "Température", + apiKey: "Clé API (substitution optionnelle)", + systemPrompt: "Substitution du prompt système", + advancedOptions: "Options avancées", + examplePrompts: "Exemples de prompts", + manageModels: "Gérer les modèles", + selectProvider: "Sélectionner un fournisseur", + selectModel: "Sélectionner un modèle", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Laisser vide pour utiliser la clé globale", + createdFiles: "Fichiers créés", + viewSteps: "Voir les étapes ({n})", + waitingForLogs: "En attente des journaux…", + errorsDetected: "Erreurs détectées", + }, + settings: { + title: "Configuration", + subtitle: "Gérez vos moteurs d'intelligence, modèles et prompts système.", + tabs: { + general: "Général", + providers: "Fournisseurs", + models: "Modèles", + prompts: "Prompts", + secrets: "Secrets", + }, + }, + general: { + title: "Préférences générales", + sections: { + networkApi: "Réseau et API", + preferredIntelligence: "Intelligence préférée", + appearance: "Apparence", + language: "Langue", + }, + fields: { + backendUrl: "URL du backend", + backendUrlHint: "L'adresse de votre service backend Unity Generator", + outputBasePath: "Chemin de base (sortie)", + outputBasePathHint: "Où les fichiers générés sont enregistrés", + unityEditorPath: "Chemin de l'éditeur Unity (optionnel)", + unityEditorPathHint: "Chemin complet vers l'exécutable de l'éditeur Unity", + preferredLlm: "Fournisseur de texte/logique par défaut", + preferredLlmModel: "Modèle de texte/logique par défaut", + preferredImage: "Fournisseur de génération d'images par défaut", + preferredImageModel: "Modèle de génération d'images par défaut", + preferredAudio: "Fournisseur de voix (TTS) par défaut", + preferredAudioModel: "Modèle de voix (TTS) par défaut", + preferredMusic: "Fournisseur de musique par défaut", + preferredMusicModel: "Modèle de musique par défaut", + theme: "Thème", + language: "Langue de l'interface", + }, + actions: { saveAll: "Enregistrer tous les changements" }, + status: { + saving: "Enregistrement…", + saved: "Préférences enregistrées avec succès.", + modelNotRegistered: 'Le modèle "{model}" n\'est pas enregistré pour le fournisseur "{provider}".', + networkError: "Erreur réseau : impossible de contacter le backend.", + }, + }, + code: { + title: "Code C# Unity", + subtitle: "Générez des scripts C# Unity avec l'IA.", + fields: { prompt: "Prompt", maxTokens: "Tokens maximum" }, + actions: { generate: "Générer le code" }, + activeProject: "Projet actif : {name}", + autoSave: "Enregistrer automatiquement dans le projet", + }, + text: { + title: "Génération de texte", + subtitle: "Générez du contenu textuel avec l'IA.", + fields: { prompt: "Prompt", length: "Longueur" }, + actions: { generate: "Générer le texte" }, + }, + image: { + title: "Génération d'images", + subtitle: "Générez des images avec l'IA.", + fields: { + prompt: "Prompt", + aspectRatio: "Rapport d'aspect", + quality: "Qualité", + textureName: "Nom de la texture", + textureType: "Type de texture", + }, + actions: { generate: "Générer l'image", saveToUnity: "Enregistrer dans Unity" }, + }, + sprites: { + title: "Sprites 2D", + badge: "Optimisé pour le Pixel Art", + subtitle: "Générez des sprites 2D pour Unity.", + fields: { + prompt: "Prompt", + resolution: "Résolution", + paletteSize: "Taille de la palette", + autoCrop: "Recadrage automatique des bords transparents", + colors: "{n} couleurs", + }, + preview: "Zone de prévisualisation", + actions: { generate: "Générer les sprites" }, + }, + audio: { + title: "Génération audio", + subtitle: "Générez des ressources audio avec l'IA.", + generationType: "Type de génération", + speechTts: "Voix (TTS)", + atmosphericMusic: "Musique d'ambiance", + fields: { + prompt: "Prompt", + musicDescription: "Description musicale", + speechPrompt: "Prompt vocal", + voiceOptional: "Voix (optionnelle)", + musicModel: "Modèle de musique", + stability: "Stabilité", + audioClipName: "Nom du clip audio", + audioFormat: "Format audio", + }, + actions: { generate: "Générer l'audio", saveToUnity: "Enregistrer dans Unity" }, + }, + scenes: { + title: "Créateur de scènes", + subtitle: "Générez des scènes Unity avec l'IA.", + fields: { prompt: "Description de la scène" }, + actions: { generate: "Créer la scène" }, + mediaReady: "Média prêt à importer", + mediaReadyText: "{type} \"{name}\" est prêt à être importé dans Unity. Révisez le prompt et cliquez sur \"Créer la scène\" pour continuer.", + mediaTypeImage: "Image", + mediaTypeAudio: "Audio", + }, + unityUi: { + title: "Unity UI", + subtitle: "Générez des composants UI Unity avec l'IA.", + fields: { + uiSystem: "Système UI", + elementType: "Type d'élément", + prompt: "Description de l'élément UI", + outputFormat: "Format de sortie", + anchorPreset: "Preset d'ancrage", + colourTheme: "Thème de couleur (optionnel)", + includeAnimations: "Inclure les animations", + }, + actions: { generate: "Générer l'UI" }, + }, + unityPhysics: { + title: "Unity Physique", + subtitle: "Générez la configuration physique Unity — Rigidbodies, colliders, gravité et plus.", + fields: { + physicsBackend: "Moteur physique", + simulationMode: "Mode de simulation", + gravityPreset: "Preset de gravité", + prompt: "Description de la physique", + includeRigidbody: "Inclure la configuration Rigidbody", + includeColliders: "Inclure la configuration des colliders", + includeLayers: "Inclure les couches physiques", + }, + actions: { generate: "Générer la configuration physique" }, + quickActions: "Actions rapides", + }, + unityProject: { + title: "Projet Unity", + subtitle: "Générez une structure complète de projet Unity.", + engineSettings: "Paramètres du moteur Unity", + fields: { + projectName: "Nom du projet", + unityTemplate: "Modèle Unity", + unityVersion: "Version Unity", + targetPlatform: "Plateforme cible", + generateDefaultScene: "Générer la scène par défaut", + autoInstallPackages: "Installer automatiquement les paquets UPM", + setupUrp: "Configurer URP", + upmPackages: "Paquets UPM (séparés par des virgules)", + sceneName: "Nom de la scène", + unityEditorPath: "Chemin de l'éditeur Unity (optionnel)", + timeout: "Délai d'attente (secondes)", + resultJson: "Résultat (JSON)", + }, + actions: { + generate: "Générer le projet", + finalize: "Finaliser le projet", + openFolder: "Ouvrir le dossier de sortie", + addVersion: "Ajouter une version Unity", + downloadZip: "Télécharger le projet finalisé (.zip)", + }, + dialogs: { + addVersion: { + title: "Ajouter une version Unity", + versionId: "ID de version", + label: "Étiquette (optionnelle)", + }, + }, + }, + theme: { light: "Clair", dark: "Sombre", system: "Système" }, + languages: { + en: "Anglais", + es: "Espagnol", + ca: "Catalan", + eu: "Basque", + oc: "Occitan", + uk: "Ukrainien", + pt: "Portugais", + gl: "Galicien", + fr: "Français", + it: "Italien", + pl: "Polonais", + zh: "Chinois", + ar: "Arabe", + de: "Allemand", + tr: "turc", + }, +} as const; diff --git a/frontend/src/i18n/locales/gl.ts b/frontend/src/i18n/locales/gl.ts index d2dc72b6..d748bbba 100644 --- a/frontend/src/i18n/locales/gl.ts +++ b/frontend/src/i18n/locales/gl.ts @@ -1,253 +1,261 @@ -/** - * Galician (gl) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "En liña", offline: "Sen conexión" }, - nav: { - settings: "Axustes", - scenes: "Escenas", - code: "Código", - text: "Texto", - image: "Imaxe", - sprites: "Sprites", - audio: "Audio", - unityUi: "Unity UI", - unityPhysics: "Unity Física", - unityProject: "Proxecto Unity", - }, - actions: { repository: "Repositorio" }, - }, - common: { - loading: "Cargando…", - saving: "Gardando…", - save: "Gardar", - cancel: "Cancelar", - close: "Pechar", - ok: "Aceptar", - yes: "Si", - no: "Non", - error: "Erro", - warning: "Aviso", - success: "Éxito", - generate: "Xerar", - result: "Resultado", - provider: "Provedor", - model: "Modelo", - temperature: "Temperatura", - apiKey: "Clave API (substitución opcional)", - systemPrompt: "Substitución do prompt de sistema", - advancedOptions: "Opcións avanzadas", - examplePrompts: "Exemplos de prompts", - manageModels: "Xestionar modelos", - selectProvider: "Seleccionar provedor", - selectModel: "Seleccionar modelo", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Deixar baleiro para usar a clave global", - createdFiles: "Ficheiros creados", - viewSteps: "Ver pasos ({n})", - waitingForLogs: "Agardando rexistros…", - errorsDetected: "Erros detectados", - }, - settings: { - title: "Configuración", - subtitle: "Xestiona os teus motores de intelixencia, modelos e prompts de sistema.", - tabs: { - general: "Xeral", - providers: "Provedores", - models: "Modelos", - prompts: "Prompts", - secrets: "Segredos", - }, - }, - general: { - title: "Preferencias xerais", - sections: { - networkApi: "Rede e API", - preferredIntelligence: "Intelixencia preferida", - appearance: "Aparencia", - language: "Idioma", - }, - fields: { - backendUrl: "URL do backend", - backendUrlHint: "O enderezo do teu servizo backend Unity Generator", - outputBasePath: "Ruta base (saída)", - outputBasePathHint: "Onde se gardan os ficheiros xerados", - unityEditorPath: "Ruta do editor Unity (opcional)", - unityEditorPathHint: "Ruta completa ao executable do editor Unity", - preferredLlm: "Provedor de texto/lóxica predeterminado", - preferredLlmModel: "Modelo de texto/lóxica predeterminado", - preferredImage: "Provedor de xeración de imaxes predeterminado", - preferredImageModel: "Modelo de xeración de imaxes predeterminado", - preferredAudio: "Provedor de voz (TTS) predeterminado", - preferredAudioModel: "Modelo de voz (TTS) predeterminado", - preferredMusic: "Provedor de música predeterminado", - preferredMusicModel: "Modelo de música predeterminado", - theme: "Tema", - language: "Idioma da interface", - }, - actions: { saveAll: "Gardar todos os cambios" }, - status: { - saving: "Gardando…", - saved: "Preferencias gardadas correctamente.", - modelNotRegistered: 'O modelo "{model}" non está rexistrado para o provedor "{provider}".', - networkError: "Erro de rede: non se pode conectar co backend.", - }, - }, - code: { - title: "Código C# Unity", - subtitle: "Xera scripts C# para Unity con IA.", - fields: { prompt: "Prompt", maxTokens: "Tokens máximos" }, - actions: { generate: "Xerar código" }, - activeProject: "Proxecto activo: {name}", - autoSave: "Gardar automaticamente no proxecto", - }, - text: { - title: "Xeración de texto", - subtitle: "Xera contido de texto con IA.", - fields: { prompt: "Prompt", length: "Lonxitude" }, - actions: { generate: "Xerar texto" }, - }, - image: { - title: "Xeración de imaxes", - subtitle: "Xera imaxes con IA.", - fields: { - prompt: "Prompt", - aspectRatio: "Relación de aspecto", - quality: "Calidade", - textureName: "Nome da textura", - textureType: "Tipo de textura", - }, - actions: { generate: "Xerar imaxe", saveToUnity: "Gardar en Unity" }, - }, - sprites: { - title: "Sprites 2D", - badge: "Optimizado para Pixel Art", - subtitle: "Xera sprites 2D para Unity.", - fields: { - prompt: "Prompt", - resolution: "Resolución", - paletteSize: "Tamaño da paleta", - autoCrop: "Recorte automático de bordos transparentes", - colors: "{n} cores", - }, - preview: "Área de previsualización", - actions: { generate: "Xerar sprites" }, - }, - audio: { - title: "Xeración de audio", - subtitle: "Xera recursos de audio con IA.", - generationType: "Tipo de xeración", - speechTts: "Voz (TTS)", - atmosphericMusic: "Música ambiental", - fields: { - prompt: "Prompt", - musicDescription: "Descrición musical", - speechPrompt: "Prompt de voz", - voiceOptional: "Voz (opcional)", - musicModel: "Modelo de música", - stability: "Estabilidade", - audioClipName: "Nome do clip de audio", - audioFormat: "Formato de audio", - }, - actions: { generate: "Xerar audio", saveToUnity: "Gardar en Unity" }, - }, - scenes: { - title: "Creador de escenas", - subtitle: "Xera escenas Unity con IA.", - fields: { prompt: "Descrición da escena" }, - actions: { generate: "Crear escena" }, - mediaReady: "Multimedia lista para importar", - mediaReadyText: "{type} \"{name}\" está lista para importarse en Unity. Revisa o prompt e fai clic en \"Crear escena\" para continuar.", - mediaTypeImage: "Imaxe", - mediaTypeAudio: "Audio", - }, - unityUi: { - title: "Unity UI", - subtitle: "Xera compoñentes de UI para Unity con IA.", - fields: { - uiSystem: "Sistema de UI", - elementType: "Tipo de elemento", - prompt: "Descrición do elemento UI", - outputFormat: "Formato de saída", - anchorPreset: "Preset de ancoraxe", - colourTheme: "Tema de cor (opcional)", - includeAnimations: "Incluír animacións", - }, - actions: { generate: "Xerar UI" }, - }, - unityPhysics: { - title: "Unity Física", - subtitle: "Xera configuración de física Unity — Rigidbodies, colisionadores, gravidade e máis.", - fields: { - physicsBackend: "Motor de física", - simulationMode: "Modo de simulación", - gravityPreset: "Preset de gravidade", - prompt: "Descrición da física", - includeRigidbody: "Incluír configuración Rigidbody", - includeColliders: "Incluír configuración de colisionadores", - includeLayers: "Incluír capas de física", - }, - actions: { generate: "Xerar configuración de física" }, - quickActions: "Accións rápidas", - }, - unityProject: { - title: "Proxecto Unity", - subtitle: "Xera unha estrutura completa de proxecto Unity.", - engineSettings: "Configuración do motor Unity", - fields: { - projectName: "Nome do proxecto", - unityTemplate: "Modelo Unity", - unityVersion: "Versión Unity", - targetPlatform: "Plataforma de destino", - generateDefaultScene: "Xerar escena predeterminada", - autoInstallPackages: "Instalar paquetes UPM automaticamente", - setupUrp: "Configurar URP", - upmPackages: "Paquetes UPM (separados por comas)", - sceneName: "Nome da escena", - unityEditorPath: "Ruta do editor Unity (opcional)", - timeout: "Tempo de espera (segundos)", - resultJson: "Resultado (JSON)", - }, - actions: { - generate: "Xerar proxecto", - finalize: "Finalizar proxecto", - openFolder: "Abrir cartafol de saída", - addVersion: "Engadir versión Unity", - downloadZip: "Descargar proxecto finalizado (.zip)", - }, - dialogs: { - addVersion: { - title: "Engadir versión Unity", - versionId: "ID de versión", - label: "Etiqueta (opcional)", - }, - }, - }, - theme: { light: "Claro", dark: "Escuro", system: "Sistema" }, - languages: { - en: "Inglés", - es: "Castelán", - ca: "Catalán", - eu: "Éuscaro", - oc: "Occitano", - uk: "Ucraíno", - pt: "Portugués", - gl: "Galego", - fr: "Francés", - it: "Italiano", - pl: "Polaco", - zh: "Chinés", - ar: "Árabe", - de: "Alemán", - hi: "Hindi", - bn: "Bengalí", - ur: "Urdu", - id: "Indonesio", - ja: "Xaponés", - vi: "Vietnamita", - ko: "Coreano", - tr: "Turco", - }, -} as const; +/** + * Galician (gl) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "En liña", offline: "Sen conexión" }, + nav: { + settings: "Axustes", + scenes: "Escenas", + code: "Código", + text: "Texto", + image: "Imaxe", + sprites: "Sprites", + audio: "Audio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Física", + unityProject: "Proxecto Unity", + logs: "Logs", + }, + actions: { repository: "Repositorio" }, + }, + common: { + loading: "Cargando…", + saving: "Gardando…", + save: "Gardar", + cancel: "Cancelar", + close: "Pechar", + ok: "Aceptar", + yes: "Si", + no: "Non", + error: "Erro", + warning: "Aviso", + success: "Éxito", + generate: "Xerar", + result: "Resultado", + provider: "Provedor", + model: "Modelo", + temperature: "Temperatura", + apiKey: "Clave API (substitución opcional)", + systemPrompt: "Substitución do prompt de sistema", + advancedOptions: "Opcións avanzadas", + examplePrompts: "Exemplos de prompts", + manageModels: "Xestionar modelos", + selectProvider: "Seleccionar provedor", + selectModel: "Seleccionar modelo", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Deixar baleiro para usar a clave global", + createdFiles: "Ficheiros creados", + viewSteps: "Ver pasos ({n})", + waitingForLogs: "Agardando rexistros…", + errorsDetected: "Erros detectados", + }, + settings: { + title: "Configuración", + subtitle: "Xestiona os teus motores de intelixencia, modelos e prompts de sistema.", + tabs: { + general: "Xeral", + providers: "Provedores", + models: "Modelos", + prompts: "Prompts", + secrets: "Segredos", + }, + }, + general: { + title: "Preferencias xerais", + sections: { + networkApi: "Rede e API", + preferredIntelligence: "Intelixencia preferida", + appearance: "Aparencia", + language: "Idioma", + }, + fields: { + backendUrl: "URL do backend", + backendUrlHint: "O enderezo do teu servizo backend Unity Generator", + outputBasePath: "Ruta base (saída)", + outputBasePathHint: "Onde se gardan os ficheiros xerados", + unityEditorPath: "Ruta do editor Unity (opcional)", + unityEditorPathHint: "Ruta completa ao executable do editor Unity", + preferredLlm: "Provedor de texto/lóxica predeterminado", + preferredLlmModel: "Modelo de texto/lóxica predeterminado", + preferredImage: "Provedor de xeración de imaxes predeterminado", + preferredImageModel: "Modelo de xeración de imaxes predeterminado", + preferredAudio: "Provedor de voz (TTS) predeterminado", + preferredAudioModel: "Modelo de voz (TTS) predeterminado", + preferredMusic: "Provedor de música predeterminado", + preferredMusicModel: "Modelo de música predeterminado", + theme: "Tema", + language: "Idioma da interface", + }, + actions: { saveAll: "Gardar todos os cambios" }, + status: { + saving: "Gardando…", + saved: "Preferencias gardadas correctamente.", + modelNotRegistered: 'O modelo "{model}" non está rexistrado para o provedor "{provider}".', + networkError: "Erro de rede: non se pode conectar co backend.", + }, + }, + code: { + title: "Código C# Unity", + subtitle: "Xera scripts C# para Unity con IA.", + fields: { prompt: "Prompt", maxTokens: "Tokens máximos" }, + actions: { generate: "Xerar código" }, + activeProject: "Proxecto activo: {name}", + autoSave: "Gardar automaticamente no proxecto", + }, + text: { + title: "Xeración de texto", + subtitle: "Xera contido de texto con IA.", + fields: { prompt: "Prompt", length: "Lonxitude" }, + actions: { generate: "Xerar texto" }, + }, + image: { + title: "Xeración de imaxes", + subtitle: "Xera imaxes con IA.", + fields: { + prompt: "Prompt", + aspectRatio: "Relación de aspecto", + quality: "Calidade", + textureName: "Nome da textura", + textureType: "Tipo de textura", + }, + actions: { generate: "Xerar imaxe", saveToUnity: "Gardar en Unity" }, + }, + sprites: { + title: "Sprites 2D", + badge: "Optimizado para Pixel Art", + subtitle: "Xera sprites 2D para Unity.", + fields: { + prompt: "Prompt", + resolution: "Resolución", + paletteSize: "Tamaño da paleta", + autoCrop: "Recorte automático de bordos transparentes", + colors: "{n} cores", + }, + preview: "Área de previsualización", + actions: { generate: "Xerar sprites" }, + }, + audio: { + title: "Xeración de audio", + subtitle: "Xera recursos de audio con IA.", + generationType: "Tipo de xeración", + speechTts: "Voz (TTS)", + atmosphericMusic: "Música ambiental", + fields: { + prompt: "Prompt", + musicDescription: "Descrición musical", + speechPrompt: "Prompt de voz", + voiceOptional: "Voz (opcional)", + musicModel: "Modelo de música", + stability: "Estabilidade", + audioClipName: "Nome do clip de audio", + audioFormat: "Formato de audio", + }, + actions: { generate: "Xerar audio", saveToUnity: "Gardar en Unity" }, + }, + scenes: { + title: "Creador de escenas", + subtitle: "Xera escenas Unity con IA.", + fields: { prompt: "Descrición da escena" }, + actions: { generate: "Crear escena" }, + mediaReady: "Multimedia lista para importar", + mediaReadyText: "{type} \"{name}\" está lista para importarse en Unity. Revisa o prompt e fai clic en \"Crear escena\" para continuar.", + mediaTypeImage: "Imaxe", + mediaTypeAudio: "Audio", + }, + unityUi: { + title: "Unity UI", + subtitle: "Xera compoñentes de UI para Unity con IA.", + fields: { + uiSystem: "Sistema de UI", + elementType: "Tipo de elemento", + prompt: "Descrición do elemento UI", + outputFormat: "Formato de saída", + anchorPreset: "Preset de ancoraxe", + colourTheme: "Tema de cor (opcional)", + includeAnimations: "Incluír animacións", + }, + actions: { generate: "Xerar UI" }, + }, + unityPhysics: { + title: "Unity Física", + subtitle: "Xera configuración de física Unity — Rigidbodies, colisionadores, gravidade e máis.", + fields: { + physicsBackend: "Motor de física", + simulationMode: "Modo de simulación", + gravityPreset: "Preset de gravidade", + prompt: "Descrición da física", + includeRigidbody: "Incluír configuración Rigidbody", + includeColliders: "Incluír configuración de colisionadores", + includeLayers: "Incluír capas de física", + }, + actions: { generate: "Xerar configuración de física" }, + quickActions: "Accións rápidas", + }, + unityProject: { + title: "Proxecto Unity", + subtitle: "Xera unha estrutura completa de proxecto Unity.", + engineSettings: "Configuración do motor Unity", + fields: { + projectName: "Nome do proxecto", + unityTemplate: "Modelo Unity", + unityVersion: "Versión Unity", + targetPlatform: "Plataforma de destino", + generateDefaultScene: "Xerar escena predeterminada", + autoInstallPackages: "Instalar paquetes UPM automaticamente", + setupUrp: "Configurar URP", + upmPackages: "Paquetes UPM (separados por comas)", + sceneName: "Nome da escena", + unityEditorPath: "Ruta do editor Unity (opcional)", + timeout: "Tempo de espera (segundos)", + resultJson: "Resultado (JSON)", + }, + actions: { + generate: "Xerar proxecto", + finalize: "Finalizar proxecto", + openFolder: "Abrir cartafol de saída", + addVersion: "Engadir versión Unity", + downloadZip: "Descargar proxecto finalizado (.zip)", + }, + dialogs: { + addVersion: { + title: "Engadir versión Unity", + versionId: "ID de versión", + label: "Etiqueta (opcional)", + }, + }, + }, + theme: { light: "Claro", dark: "Escuro", system: "Sistema" }, + languages: { + en: "Inglés", + es: "Castelán", + ca: "Catalán", + eu: "Éuscaro", + oc: "Occitano", + uk: "Ucraíno", + pt: "Portugués", + gl: "Galego", + fr: "Francés", + it: "Italiano", + pl: "Polaco", + zh: "Chinés", + ar: "Árabe", + de: "Alemán", + hi: "Hindi", + bn: "Bengalí", + ur: "Urdu", + id: "Indonesio", + ja: "Xaponés", + vi: "Vietnamita", + ko: "Coreano", + tr: "Turco", + }, +} as const; diff --git a/frontend/src/i18n/locales/hi.ts b/frontend/src/i18n/locales/hi.ts index d10d10e2..87d412c6 100644 --- a/frontend/src/i18n/locales/hi.ts +++ b/frontend/src/i18n/locales/hi.ts @@ -1,113 +1,121 @@ -/** - * Hindi (hi) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "ऑनलाइन", offline: "ऑफलाइन" }, - nav: { - settings: "सेटिंग्स", scenes: "दृश्य", code: "कोड", text: "पाठ", - image: "छवि", sprites: "स्प्राइट्स", audio: "ऑडियो", - unityUi: "Unity UI", unityPhysics: "Unity भौतिकी", unityProject: "Unity प्रोजेक्ट", - }, - actions: { repository: "रिपॉजिटरी" }, - }, - common: { - loading: "लोड हो रहा है…", saving: "सहेजा जा रहा है…", save: "सहेजें", - cancel: "रद्द करें", close: "बंद करें", ok: "ठीक है", yes: "हाँ", no: "नहीं", - error: "त्रुटि", warning: "चेतावनी", success: "सफलता", generate: "उत्पन्न करें", - result: "परिणाम", provider: "प्रदाता", model: "मॉडल", temperature: "तापमान", - apiKey: "API कुंजी (वैकल्पिक ओवरराइड)", systemPrompt: "सिस्टम प्रॉम्प्ट ओवरराइड", - advancedOptions: "उन्नत विकल्प", examplePrompts: "उदाहरण प्रॉम्प्ट", - manageModels: "मॉडल प्रबंधित करें", selectProvider: "प्रदाता चुनें", - selectModel: "मॉडल चुनें", prompt: "प्रॉम्प्ट", - leaveEmptyForGlobalKey: "वैश्विक कुंजी उपयोग के लिए खाली छोड़ें", - createdFiles: "बनाई गई फ़ाइलें", viewSteps: "चरण देखें ({n})", - waitingForLogs: "लॉग की प्रतीक्षा…", errorsDetected: "त्रुटियाँ मिलीं", - }, - settings: { - title: "कॉन्फ़िगरेशन", - subtitle: "अपने इंटेलिजेंस इंजन, मॉडल और सिस्टम प्रॉम्प्ट प्रबंधित करें।", - tabs: { general: "सामान्य", providers: "प्रदाता", models: "मॉडल", prompts: "प्रॉम्प्ट", secrets: "रहस्य" }, - }, - general: { - title: "सामान्य प्राथमिकताएँ", - sections: { networkApi: "नेटवर्क और API", preferredIntelligence: "पसंदीदा इंटेलिजेंस", appearance: "दिखावट", language: "भाषा" }, - fields: { - backendUrl: "बैकएंड URL", backendUrlHint: "आपकी Unity Generator बैकएंड सेवा का पता", - outputBasePath: "आधार पथ (आउटपुट)", outputBasePathHint: "जनरेट की गई फ़ाइलें कहाँ सहेजी जाती हैं", - unityEditorPath: "Unity एडिटर पथ (वैकल्पिक)", unityEditorPathHint: "Unity एडिटर एक्जीक्यूटेबल का पूरा पथ", - preferredLlm: "डिफ़ॉल्ट टेक्स्ट/लॉजिक प्रदाता", preferredLlmModel: "डिफ़ॉल्ट टेक्स्ट/लॉजिक मॉडल", - preferredImage: "डिफ़ॉल्ट इमेज जनरेशन प्रदाता", preferredImageModel: "डिफ़ॉल्ट इमेज जनरेशन मॉडल", - preferredAudio: "डिफ़ॉल्ट वॉयस (TTS) प्रदाता", preferredAudioModel: "डिफ़ॉल्ट वॉयस (TTS) मॉडल", - preferredMusic: "डिफ़ॉल्ट म्यूज़िक प्रदाता", preferredMusicModel: "डिफ़ॉल्ट म्यूज़िक मॉडल", - theme: "थीम", language: "इंटरफ़ेस भाषा", - }, - actions: { saveAll: "सभी परिवर्तन सहेजें" }, - status: { - saving: "सहेजा जा रहा है…", saved: "प्राथमिकताएँ सफलतापूर्वक सहेजी गईं।", - modelNotRegistered: 'मॉडल "{model}" प्रदाता "{provider}" के लिए पंजीकृत नहीं है।', - networkError: "नेटवर्क त्रुटि: बैकएंड से कनेक्ट नहीं हो सका।", - }, - }, - code: { - title: "Unity C# कोड", subtitle: "AI का उपयोग करके Unity C# स्क्रिप्ट जनरेट करें।", - fields: { prompt: "प्रॉम्प्ट", maxTokens: "अधिकतम टोकन" }, - actions: { generate: "कोड जनरेट करें" }, - activeProject: "सक्रिय प्रोजेक्ट: {name}", autoSave: "प्रोजेक्ट में स्वतः सहेजें", - }, - text: { - title: "टेक्स्ट जनरेशन", subtitle: "AI का उपयोग करके टेक्स्ट सामग्री जनरेट करें।", - fields: { prompt: "प्रॉम्प्ट", length: "लंबाई" }, - actions: { generate: "टेक्स्ट जनरेट करें" }, - }, - image: { - title: "इमेज जनरेशन", subtitle: "AI का उपयोग करके इमेज जनरेट करें।", - fields: { prompt: "प्रॉम्प्ट", aspectRatio: "आस्पेक्ट रेशियो", quality: "गुणवत्ता", textureName: "टेक्सचर नाम", textureType: "टेक्सचर प्रकार" }, - actions: { generate: "इमेज जनरेट करें", saveToUnity: "Unity में सहेजें" }, - }, - sprites: { - title: "2D स्प्राइट्स", badge: "पिक्सेल आर्ट के लिए अनुकूलित", subtitle: "Unity के लिए 2D स्प्राइट जनरेट करें।", - fields: { prompt: "प्रॉम्प्ट", resolution: "रिज़ॉल्यूशन", paletteSize: "पैलेट आकार", autoCrop: "पारदर्शी किनारे स्वतः काटें", colors: "{n} रंग" }, - preview: "पूर्वावलोकन क्षेत्र", actions: { generate: "स्प्राइट जनरेट करें" }, - }, - audio: { - title: "ऑडियो जनरेशन", subtitle: "AI का उपयोग करके ऑडियो संसाधन जनरेट करें।", - generationType: "जनरेशन प्रकार", speechTts: "वाणी (TTS)", atmosphericMusic: "वातावरणीय संगीत", - fields: { prompt: "प्रॉम्प्ट", musicDescription: "संगीत विवरण", speechPrompt: "वाणी प्रॉम्प्ट", voiceOptional: "आवाज़ (वैकल्पिक)", musicModel: "म्यूज़िक मॉडल", stability: "स्थिरता", audioClipName: "ऑडियो क्लिप नाम", audioFormat: "ऑडियो फ़ॉर्मेट" }, - actions: { generate: "ऑडियो जनरेट करें", saveToUnity: "Unity में सहेजें" }, - }, - scenes: { - title: "दृश्य निर्माता", subtitle: "AI का उपयोग करके Unity दृश्य जनरेट करें।", - fields: { prompt: "दृश्य विवरण" }, actions: { generate: "दृश्य बनाएँ" }, - mediaReady: "मीडिया आयात के लिए तैयार", - mediaReadyText: "{type} \"{name}\" Unity में आयात के लिए तैयार है। प्रॉम्प्ट की समीक्षा करें और जारी रखने के लिए \"दृश्य बनाएँ\" पर क्लिक करें।", - mediaTypeImage: "छवि", mediaTypeAudio: "ऑडियो", - }, - unityUi: { - title: "Unity UI", subtitle: "AI का उपयोग करके Unity UI घटक जनरेट करें।", - fields: { uiSystem: "UI सिस्टम", elementType: "तत्व प्रकार", prompt: "UI तत्व विवरण", outputFormat: "आउटपुट फ़ॉर्मेट", anchorPreset: "एंकर प्रीसेट", colourTheme: "रंग थीम (वैकल्पिक)", includeAnimations: "एनिमेशन शामिल करें" }, - actions: { generate: "UI जनरेट करें" }, - }, - unityPhysics: { - title: "Unity भौतिकी", subtitle: "Unity भौतिकी कॉन्फ़िगरेशन जनरेट करें — Rigidbodies, Colliders, गुरुत्वाकर्षण और अधिक।", - fields: { physicsBackend: "भौतिकी इंजन", simulationMode: "सिमुलेशन मोड", gravityPreset: "गुरुत्वाकर्षण प्रीसेट", prompt: "भौतिकी विवरण", includeRigidbody: "Rigidbody कॉन्फ़िगरेशन शामिल करें", includeColliders: "Collider कॉन्फ़िगरेशन शामिल करें", includeLayers: "भौतिकी परतें शामिल करें" }, - actions: { generate: "भौतिकी कॉन्फ़िगरेशन जनरेट करें" }, - quickActions: "त्वरित क्रियाएँ", - }, - unityProject: { - title: "Unity प्रोजेक्ट", subtitle: "पूर्ण Unity प्रोजेक्ट संरचना जनरेट करें।", - engineSettings: "Unity इंजन सेटिंग्स", - fields: { projectName: "प्रोजेक्ट नाम", unityTemplate: "Unity टेम्पलेट", unityVersion: "Unity संस्करण", targetPlatform: "लक्ष्य प्लेटफ़ॉर्म", generateDefaultScene: "डिफ़ॉल्ट दृश्य जनरेट करें", autoInstallPackages: "UPM पैकेज स्वतः इंस्टॉल करें", setupUrp: "URP कॉन्फ़िगर करें", upmPackages: "UPM पैकेज (अल्पविराम से अलग)", sceneName: "दृश्य नाम", unityEditorPath: "Unity एडिटर पथ (वैकल्पिक)", timeout: "टाइमआउट (सेकंड)", resultJson: "परिणाम (JSON)" }, - actions: { generate: "प्रोजेक्ट जनरेट करें", finalize: "प्रोजेक्ट अंतिम करें", openFolder: "आउटपुट फ़ोल्डर खोलें", addVersion: "Unity संस्करण जोड़ें", downloadZip: "पूर्ण प्रोजेक्ट डाउनलोड करें (.zip)" }, - dialogs: { addVersion: { title: "Unity संस्करण जोड़ें", versionId: "संस्करण ID", label: "लेबल (वैकल्पिक)" } }, - }, - theme: { light: "हल्का", dark: "गहरा", system: "सिस्टम" }, - languages: { - en: "अंग्रेज़ी", es: "स्पेनिश", ca: "कातालान", eu: "बास्क", oc: "ऑक्सिटान", uk: "यूक्रेनी", - pt: "पुर्तगाली", gl: "गैलिशियन", fr: "फ्रेंच", it: "इतालवी", pl: "पोलिश", zh: "चीनी", - ar: "अरबी", de: "जर्मन", hi: "हिंदी", bn: "बंगाली", ur: "उर्दू", - id: "इंडोनेशियाई", ja: "जापानी", vi: "वियतनामी", ko: "कोरियाई", - tr: "तुर्की", - }, -} as const; +/** + * Hindi (hi) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "ऑनलाइन", offline: "ऑफलाइन" }, + nav: { + settings: "सेटिंग्स", scenes: "दृश्य", code: "कोड", text: "पाठ", + image: "छवि", sprites: "स्प्राइट्स", audio: "ऑडियो", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", unityPhysics: "Unity भौतिकी", unityProject: "Unity प्रोजेक्ट", + logs: "Logs", + }, + actions: { repository: "रिपॉजिटरी" }, + }, + common: { + loading: "लोड हो रहा है…", saving: "सहेजा जा रहा है…", save: "सहेजें", + cancel: "रद्द करें", close: "बंद करें", ok: "ठीक है", yes: "हाँ", no: "नहीं", + error: "त्रुटि", warning: "चेतावनी", success: "सफलता", generate: "उत्पन्न करें", + result: "परिणाम", provider: "प्रदाता", model: "मॉडल", temperature: "तापमान", + apiKey: "API कुंजी (वैकल्पिक ओवरराइड)", systemPrompt: "सिस्टम प्रॉम्प्ट ओवरराइड", + advancedOptions: "उन्नत विकल्प", examplePrompts: "उदाहरण प्रॉम्प्ट", + manageModels: "मॉडल प्रबंधित करें", selectProvider: "प्रदाता चुनें", + selectModel: "मॉडल चुनें", prompt: "प्रॉम्प्ट", + leaveEmptyForGlobalKey: "वैश्विक कुंजी उपयोग के लिए खाली छोड़ें", + createdFiles: "बनाई गई फ़ाइलें", viewSteps: "चरण देखें ({n})", + waitingForLogs: "लॉग की प्रतीक्षा…", errorsDetected: "त्रुटियाँ मिलीं", + }, + settings: { + title: "कॉन्फ़िगरेशन", + subtitle: "अपने इंटेलिजेंस इंजन, मॉडल और सिस्टम प्रॉम्प्ट प्रबंधित करें।", + tabs: { general: "सामान्य", providers: "प्रदाता", models: "मॉडल", prompts: "प्रॉम्प्ट", secrets: "रहस्य" }, + }, + general: { + title: "सामान्य प्राथमिकताएँ", + sections: { networkApi: "नेटवर्क और API", preferredIntelligence: "पसंदीदा इंटेलिजेंस", appearance: "दिखावट", language: "भाषा" }, + fields: { + backendUrl: "बैकएंड URL", backendUrlHint: "आपकी Unity Generator बैकएंड सेवा का पता", + outputBasePath: "आधार पथ (आउटपुट)", outputBasePathHint: "जनरेट की गई फ़ाइलें कहाँ सहेजी जाती हैं", + unityEditorPath: "Unity एडिटर पथ (वैकल्पिक)", unityEditorPathHint: "Unity एडिटर एक्जीक्यूटेबल का पूरा पथ", + preferredLlm: "डिफ़ॉल्ट टेक्स्ट/लॉजिक प्रदाता", preferredLlmModel: "डिफ़ॉल्ट टेक्स्ट/लॉजिक मॉडल", + preferredImage: "डिफ़ॉल्ट इमेज जनरेशन प्रदाता", preferredImageModel: "डिफ़ॉल्ट इमेज जनरेशन मॉडल", + preferredAudio: "डिफ़ॉल्ट वॉयस (TTS) प्रदाता", preferredAudioModel: "डिफ़ॉल्ट वॉयस (TTS) मॉडल", + preferredMusic: "डिफ़ॉल्ट म्यूज़िक प्रदाता", preferredMusicModel: "डिफ़ॉल्ट म्यूज़िक मॉडल", + theme: "थीम", language: "इंटरफ़ेस भाषा", + }, + actions: { saveAll: "सभी परिवर्तन सहेजें" }, + status: { + saving: "सहेजा जा रहा है…", saved: "प्राथमिकताएँ सफलतापूर्वक सहेजी गईं।", + modelNotRegistered: 'मॉडल "{model}" प्रदाता "{provider}" के लिए पंजीकृत नहीं है।', + networkError: "नेटवर्क त्रुटि: बैकएंड से कनेक्ट नहीं हो सका।", + }, + }, + code: { + title: "Unity C# कोड", subtitle: "AI का उपयोग करके Unity C# स्क्रिप्ट जनरेट करें।", + fields: { prompt: "प्रॉम्प्ट", maxTokens: "अधिकतम टोकन" }, + actions: { generate: "कोड जनरेट करें" }, + activeProject: "सक्रिय प्रोजेक्ट: {name}", autoSave: "प्रोजेक्ट में स्वतः सहेजें", + }, + text: { + title: "टेक्स्ट जनरेशन", subtitle: "AI का उपयोग करके टेक्स्ट सामग्री जनरेट करें।", + fields: { prompt: "प्रॉम्प्ट", length: "लंबाई" }, + actions: { generate: "टेक्स्ट जनरेट करें" }, + }, + image: { + title: "इमेज जनरेशन", subtitle: "AI का उपयोग करके इमेज जनरेट करें।", + fields: { prompt: "प्रॉम्प्ट", aspectRatio: "आस्पेक्ट रेशियो", quality: "गुणवत्ता", textureName: "टेक्सचर नाम", textureType: "टेक्सचर प्रकार" }, + actions: { generate: "इमेज जनरेट करें", saveToUnity: "Unity में सहेजें" }, + }, + sprites: { + title: "2D स्प्राइट्स", badge: "पिक्सेल आर्ट के लिए अनुकूलित", subtitle: "Unity के लिए 2D स्प्राइट जनरेट करें।", + fields: { prompt: "प्रॉम्प्ट", resolution: "रिज़ॉल्यूशन", paletteSize: "पैलेट आकार", autoCrop: "पारदर्शी किनारे स्वतः काटें", colors: "{n} रंग" }, + preview: "पूर्वावलोकन क्षेत्र", actions: { generate: "स्प्राइट जनरेट करें" }, + }, + audio: { + title: "ऑडियो जनरेशन", subtitle: "AI का उपयोग करके ऑडियो संसाधन जनरेट करें।", + generationType: "जनरेशन प्रकार", speechTts: "वाणी (TTS)", atmosphericMusic: "वातावरणीय संगीत", + fields: { prompt: "प्रॉम्प्ट", musicDescription: "संगीत विवरण", speechPrompt: "वाणी प्रॉम्प्ट", voiceOptional: "आवाज़ (वैकल्पिक)", musicModel: "म्यूज़िक मॉडल", stability: "स्थिरता", audioClipName: "ऑडियो क्लिप नाम", audioFormat: "ऑडियो फ़ॉर्मेट" }, + actions: { generate: "ऑडियो जनरेट करें", saveToUnity: "Unity में सहेजें" }, + }, + scenes: { + title: "दृश्य निर्माता", subtitle: "AI का उपयोग करके Unity दृश्य जनरेट करें।", + fields: { prompt: "दृश्य विवरण" }, actions: { generate: "दृश्य बनाएँ" }, + mediaReady: "मीडिया आयात के लिए तैयार", + mediaReadyText: "{type} \"{name}\" Unity में आयात के लिए तैयार है। प्रॉम्प्ट की समीक्षा करें और जारी रखने के लिए \"दृश्य बनाएँ\" पर क्लिक करें।", + mediaTypeImage: "छवि", mediaTypeAudio: "ऑडियो", + }, + unityUi: { + title: "Unity UI", subtitle: "AI का उपयोग करके Unity UI घटक जनरेट करें।", + fields: { uiSystem: "UI सिस्टम", elementType: "तत्व प्रकार", prompt: "UI तत्व विवरण", outputFormat: "आउटपुट फ़ॉर्मेट", anchorPreset: "एंकर प्रीसेट", colourTheme: "रंग थीम (वैकल्पिक)", includeAnimations: "एनिमेशन शामिल करें" }, + actions: { generate: "UI जनरेट करें" }, + }, + unityPhysics: { + title: "Unity भौतिकी", subtitle: "Unity भौतिकी कॉन्फ़िगरेशन जनरेट करें — Rigidbodies, Colliders, गुरुत्वाकर्षण और अधिक।", + fields: { physicsBackend: "भौतिकी इंजन", simulationMode: "सिमुलेशन मोड", gravityPreset: "गुरुत्वाकर्षण प्रीसेट", prompt: "भौतिकी विवरण", includeRigidbody: "Rigidbody कॉन्फ़िगरेशन शामिल करें", includeColliders: "Collider कॉन्फ़िगरेशन शामिल करें", includeLayers: "भौतिकी परतें शामिल करें" }, + actions: { generate: "भौतिकी कॉन्फ़िगरेशन जनरेट करें" }, + quickActions: "त्वरित क्रियाएँ", + }, + unityProject: { + title: "Unity प्रोजेक्ट", subtitle: "पूर्ण Unity प्रोजेक्ट संरचना जनरेट करें।", + engineSettings: "Unity इंजन सेटिंग्स", + fields: { projectName: "प्रोजेक्ट नाम", unityTemplate: "Unity टेम्पलेट", unityVersion: "Unity संस्करण", targetPlatform: "लक्ष्य प्लेटफ़ॉर्म", generateDefaultScene: "डिफ़ॉल्ट दृश्य जनरेट करें", autoInstallPackages: "UPM पैकेज स्वतः इंस्टॉल करें", setupUrp: "URP कॉन्फ़िगर करें", upmPackages: "UPM पैकेज (अल्पविराम से अलग)", sceneName: "दृश्य नाम", unityEditorPath: "Unity एडिटर पथ (वैकल्पिक)", timeout: "टाइमआउट (सेकंड)", resultJson: "परिणाम (JSON)" }, + actions: { generate: "प्रोजेक्ट जनरेट करें", finalize: "प्रोजेक्ट अंतिम करें", openFolder: "आउटपुट फ़ोल्डर खोलें", addVersion: "Unity संस्करण जोड़ें", downloadZip: "पूर्ण प्रोजेक्ट डाउनलोड करें (.zip)" }, + dialogs: { addVersion: { title: "Unity संस्करण जोड़ें", versionId: "संस्करण ID", label: "लेबल (वैकल्पिक)" } }, + }, + theme: { light: "हल्का", dark: "गहरा", system: "सिस्टम" }, + languages: { + en: "अंग्रेज़ी", es: "स्पेनिश", ca: "कातालान", eu: "बास्क", oc: "ऑक्सिटान", uk: "यूक्रेनी", + pt: "पुर्तगाली", gl: "गैलिशियन", fr: "फ्रेंच", it: "इतालवी", pl: "पोलिश", zh: "चीनी", + ar: "अरबी", de: "जर्मन", hi: "हिंदी", bn: "बंगाली", ur: "उर्दू", + id: "इंडोनेशियाई", ja: "जापानी", vi: "वियतनामी", ko: "कोरियाई", + tr: "तुर्की", + }, +} as const; diff --git a/frontend/src/i18n/locales/id.ts b/frontend/src/i18n/locales/id.ts index 01b1cf64..b105a5a2 100644 --- a/frontend/src/i18n/locales/id.ts +++ b/frontend/src/i18n/locales/id.ts @@ -1,112 +1,120 @@ -/** - * Indonesian (id) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "Online", offline: "Offline" }, - nav: { - settings: "Pengaturan", scenes: "Adegan", code: "Kode", text: "Teks", - image: "Gambar", sprites: "Sprites", audio: "Audio", - unityUi: "Unity UI", unityPhysics: "Unity Fisika", unityProject: "Proyek Unity", - }, - actions: { repository: "Repositori" }, - }, - common: { - loading: "Memuat…", saving: "Menyimpan…", save: "Simpan", - cancel: "Batal", close: "Tutup", ok: "OK", yes: "Ya", no: "Tidak", - error: "Kesalahan", warning: "Peringatan", success: "Berhasil", generate: "Buat", - result: "Hasil", provider: "Penyedia", model: "Model", temperature: "Suhu", - apiKey: "Kunci API (penggantian opsional)", systemPrompt: "Penggantian prompt sistem", - advancedOptions: "Opsi lanjutan", examplePrompts: "Contoh prompt", - manageModels: "Kelola model", selectProvider: "Pilih penyedia", - selectModel: "Pilih model", prompt: "Prompt", - leaveEmptyForGlobalKey: "Kosongkan untuk menggunakan kunci global", - createdFiles: "File yang dibuat", viewSteps: "Lihat langkah ({n})", - waitingForLogs: "Menunggu log…", errorsDetected: "Kesalahan terdeteksi", - }, - settings: { - title: "Konfigurasi", - subtitle: "Kelola mesin kecerdasan, model, dan prompt sistem Anda.", - tabs: { general: "Umum", providers: "Penyedia", models: "Model", prompts: "Prompt", secrets: "Rahasia" }, - }, - general: { - title: "Preferensi umum", - sections: { networkApi: "Jaringan & API", preferredIntelligence: "Kecerdasan pilihan", appearance: "Tampilan", language: "Bahasa" }, - fields: { - backendUrl: "URL backend", backendUrlHint: "Alamat layanan backend Unity Generator Anda", - outputBasePath: "Jalur dasar (output)", outputBasePathHint: "Tempat file yang dihasilkan disimpan", - unityEditorPath: "Jalur editor Unity (opsional)", unityEditorPathHint: "Jalur lengkap ke executable editor Unity", - preferredLlm: "Penyedia teks/logika default", preferredLlmModel: "Model teks/logika default", - preferredImage: "Penyedia pembuatan gambar default", preferredImageModel: "Model pembuatan gambar default", - preferredAudio: "Penyedia suara (TTS) default", preferredAudioModel: "Model suara (TTS) default", - preferredMusic: "Penyedia musik default", preferredMusicModel: "Model musik default", - theme: "Tema", language: "Bahasa antarmuka", - }, - actions: { saveAll: "Simpan semua perubahan" }, - status: { - saving: "Menyimpan…", saved: "Preferensi berhasil disimpan.", - modelNotRegistered: 'Model "{model}" tidak terdaftar untuk penyedia "{provider}".', - networkError: "Kesalahan jaringan: tidak dapat menghubungi backend.", - }, - }, - code: { - title: "Kode C# Unity", subtitle: "Buat skrip C# Unity menggunakan AI.", - fields: { prompt: "Prompt", maxTokens: "Token maksimum" }, - actions: { generate: "Buat kode" }, - activeProject: "Proyek aktif: {name}", autoSave: "Simpan otomatis ke proyek", - }, - text: { - title: "Pembuatan teks", subtitle: "Buat konten teks menggunakan AI.", - fields: { prompt: "Prompt", length: "Panjang" }, actions: { generate: "Buat teks" }, - }, - image: { - title: "Pembuatan gambar", subtitle: "Buat gambar menggunakan AI.", - fields: { prompt: "Prompt", aspectRatio: "Rasio aspek", quality: "Kualitas", textureName: "Nama tekstur", textureType: "Jenis tekstur" }, - actions: { generate: "Buat gambar", saveToUnity: "Simpan ke Unity" }, - }, - sprites: { - title: "Sprites 2D", badge: "Dioptimalkan untuk Pixel Art", subtitle: "Buat sprite 2D untuk Unity.", - fields: { prompt: "Prompt", resolution: "Resolusi", paletteSize: "Ukuran palet", autoCrop: "Potong tepi transparan otomatis", colors: "{n} warna" }, - preview: "Area pratinjau", actions: { generate: "Buat sprites" }, - }, - audio: { - title: "Pembuatan audio", subtitle: "Buat aset audio menggunakan AI.", - generationType: "Jenis pembuatan", speechTts: "Ucapan (TTS)", atmosphericMusic: "Musik atmosfer", - fields: { prompt: "Prompt", musicDescription: "Deskripsi musik", speechPrompt: "Prompt ucapan", voiceOptional: "Suara (opsional)", musicModel: "Model musik", stability: "Stabilitas", audioClipName: "Nama klip audio", audioFormat: "Format audio" }, - actions: { generate: "Buat audio", saveToUnity: "Simpan ke Unity" }, - }, - scenes: { - title: "Pembuat adegan", subtitle: "Buat adegan Unity menggunakan AI.", - fields: { prompt: "Deskripsi adegan" }, actions: { generate: "Buat adegan" }, - mediaReady: "Media siap diimpor", - mediaReadyText: "{type} \"{name}\" siap diimpor ke Unity. Tinjau prompt dan klik \"Buat adegan\" untuk melanjutkan.", - mediaTypeImage: "Gambar", mediaTypeAudio: "Audio", - }, - unityUi: { - title: "Unity UI", subtitle: "Buat komponen UI Unity menggunakan AI.", - fields: { uiSystem: "Sistem UI", elementType: "Jenis elemen", prompt: "Deskripsi elemen UI", outputFormat: "Format output", anchorPreset: "Preset jangkar", colourTheme: "Tema warna (opsional)", includeAnimations: "Sertakan animasi" }, - actions: { generate: "Buat UI" }, - }, - unityPhysics: { - title: "Unity Fisika", subtitle: "Buat konfigurasi fisika Unity — Rigidbodies, collider, gravitasi, dan lainnya.", - fields: { physicsBackend: "Mesin fisika", simulationMode: "Mode simulasi", gravityPreset: "Preset gravitasi", prompt: "Deskripsi fisika", includeRigidbody: "Sertakan konfigurasi Rigidbody", includeColliders: "Sertakan konfigurasi collider", includeLayers: "Sertakan lapisan fisika" }, - actions: { generate: "Buat konfigurasi fisika" }, - quickActions: "Tindakan cepat", - }, - unityProject: { - title: "Proyek Unity", subtitle: "Buat struktur proyek Unity yang lengkap.", - engineSettings: "Pengaturan mesin Unity", - fields: { projectName: "Nama proyek", unityTemplate: "Template Unity", unityVersion: "Versi Unity", targetPlatform: "Platform target", generateDefaultScene: "Buat adegan default", autoInstallPackages: "Instal paket UPM otomatis", setupUrp: "Konfigurasi URP", upmPackages: "Paket UPM (dipisahkan koma)", sceneName: "Nama adegan", unityEditorPath: "Jalur editor Unity (opsional)", timeout: "Batas waktu (detik)", resultJson: "Hasil (JSON)" }, - actions: { generate: "Buat proyek", finalize: "Selesaikan proyek", openFolder: "Buka folder output", addVersion: "Tambah versi Unity", downloadZip: "Unduh proyek selesai (.zip)" }, - dialogs: { addVersion: { title: "Tambah versi Unity", versionId: "ID versi", label: "Label (opsional)" } }, - }, - theme: { light: "Terang", dark: "Gelap", system: "Sistem" }, - languages: { - en: "Inggris", es: "Spanyol", ca: "Katalan", eu: "Basque", oc: "Oksitan", uk: "Ukraina", - pt: "Portugis", gl: "Galisia", fr: "Prancis", it: "Italia", pl: "Polandia", zh: "Tionghoa", - ar: "Arab", de: "Jerman", hi: "Hindi", bn: "Bengali", ur: "Urdu", - id: "Indonesia", ja: "Jepang", vi: "Vietnam", ko: "Korea", - tr: "Turki", - }, -} as const; +/** + * Indonesian (id) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "Online", offline: "Offline" }, + nav: { + settings: "Pengaturan", scenes: "Adegan", code: "Kode", text: "Teks", + image: "Gambar", sprites: "Sprites", audio: "Audio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", unityPhysics: "Unity Fisika", unityProject: "Proyek Unity", + logs: "Logs", + }, + actions: { repository: "Repositori" }, + }, + common: { + loading: "Memuat…", saving: "Menyimpan…", save: "Simpan", + cancel: "Batal", close: "Tutup", ok: "OK", yes: "Ya", no: "Tidak", + error: "Kesalahan", warning: "Peringatan", success: "Berhasil", generate: "Buat", + result: "Hasil", provider: "Penyedia", model: "Model", temperature: "Suhu", + apiKey: "Kunci API (penggantian opsional)", systemPrompt: "Penggantian prompt sistem", + advancedOptions: "Opsi lanjutan", examplePrompts: "Contoh prompt", + manageModels: "Kelola model", selectProvider: "Pilih penyedia", + selectModel: "Pilih model", prompt: "Prompt", + leaveEmptyForGlobalKey: "Kosongkan untuk menggunakan kunci global", + createdFiles: "File yang dibuat", viewSteps: "Lihat langkah ({n})", + waitingForLogs: "Menunggu log…", errorsDetected: "Kesalahan terdeteksi", + }, + settings: { + title: "Konfigurasi", + subtitle: "Kelola mesin kecerdasan, model, dan prompt sistem Anda.", + tabs: { general: "Umum", providers: "Penyedia", models: "Model", prompts: "Prompt", secrets: "Rahasia" }, + }, + general: { + title: "Preferensi umum", + sections: { networkApi: "Jaringan & API", preferredIntelligence: "Kecerdasan pilihan", appearance: "Tampilan", language: "Bahasa" }, + fields: { + backendUrl: "URL backend", backendUrlHint: "Alamat layanan backend Unity Generator Anda", + outputBasePath: "Jalur dasar (output)", outputBasePathHint: "Tempat file yang dihasilkan disimpan", + unityEditorPath: "Jalur editor Unity (opsional)", unityEditorPathHint: "Jalur lengkap ke executable editor Unity", + preferredLlm: "Penyedia teks/logika default", preferredLlmModel: "Model teks/logika default", + preferredImage: "Penyedia pembuatan gambar default", preferredImageModel: "Model pembuatan gambar default", + preferredAudio: "Penyedia suara (TTS) default", preferredAudioModel: "Model suara (TTS) default", + preferredMusic: "Penyedia musik default", preferredMusicModel: "Model musik default", + theme: "Tema", language: "Bahasa antarmuka", + }, + actions: { saveAll: "Simpan semua perubahan" }, + status: { + saving: "Menyimpan…", saved: "Preferensi berhasil disimpan.", + modelNotRegistered: 'Model "{model}" tidak terdaftar untuk penyedia "{provider}".', + networkError: "Kesalahan jaringan: tidak dapat menghubungi backend.", + }, + }, + code: { + title: "Kode C# Unity", subtitle: "Buat skrip C# Unity menggunakan AI.", + fields: { prompt: "Prompt", maxTokens: "Token maksimum" }, + actions: { generate: "Buat kode" }, + activeProject: "Proyek aktif: {name}", autoSave: "Simpan otomatis ke proyek", + }, + text: { + title: "Pembuatan teks", subtitle: "Buat konten teks menggunakan AI.", + fields: { prompt: "Prompt", length: "Panjang" }, actions: { generate: "Buat teks" }, + }, + image: { + title: "Pembuatan gambar", subtitle: "Buat gambar menggunakan AI.", + fields: { prompt: "Prompt", aspectRatio: "Rasio aspek", quality: "Kualitas", textureName: "Nama tekstur", textureType: "Jenis tekstur" }, + actions: { generate: "Buat gambar", saveToUnity: "Simpan ke Unity" }, + }, + sprites: { + title: "Sprites 2D", badge: "Dioptimalkan untuk Pixel Art", subtitle: "Buat sprite 2D untuk Unity.", + fields: { prompt: "Prompt", resolution: "Resolusi", paletteSize: "Ukuran palet", autoCrop: "Potong tepi transparan otomatis", colors: "{n} warna" }, + preview: "Area pratinjau", actions: { generate: "Buat sprites" }, + }, + audio: { + title: "Pembuatan audio", subtitle: "Buat aset audio menggunakan AI.", + generationType: "Jenis pembuatan", speechTts: "Ucapan (TTS)", atmosphericMusic: "Musik atmosfer", + fields: { prompt: "Prompt", musicDescription: "Deskripsi musik", speechPrompt: "Prompt ucapan", voiceOptional: "Suara (opsional)", musicModel: "Model musik", stability: "Stabilitas", audioClipName: "Nama klip audio", audioFormat: "Format audio" }, + actions: { generate: "Buat audio", saveToUnity: "Simpan ke Unity" }, + }, + scenes: { + title: "Pembuat adegan", subtitle: "Buat adegan Unity menggunakan AI.", + fields: { prompt: "Deskripsi adegan" }, actions: { generate: "Buat adegan" }, + mediaReady: "Media siap diimpor", + mediaReadyText: "{type} \"{name}\" siap diimpor ke Unity. Tinjau prompt dan klik \"Buat adegan\" untuk melanjutkan.", + mediaTypeImage: "Gambar", mediaTypeAudio: "Audio", + }, + unityUi: { + title: "Unity UI", subtitle: "Buat komponen UI Unity menggunakan AI.", + fields: { uiSystem: "Sistem UI", elementType: "Jenis elemen", prompt: "Deskripsi elemen UI", outputFormat: "Format output", anchorPreset: "Preset jangkar", colourTheme: "Tema warna (opsional)", includeAnimations: "Sertakan animasi" }, + actions: { generate: "Buat UI" }, + }, + unityPhysics: { + title: "Unity Fisika", subtitle: "Buat konfigurasi fisika Unity — Rigidbodies, collider, gravitasi, dan lainnya.", + fields: { physicsBackend: "Mesin fisika", simulationMode: "Mode simulasi", gravityPreset: "Preset gravitasi", prompt: "Deskripsi fisika", includeRigidbody: "Sertakan konfigurasi Rigidbody", includeColliders: "Sertakan konfigurasi collider", includeLayers: "Sertakan lapisan fisika" }, + actions: { generate: "Buat konfigurasi fisika" }, + quickActions: "Tindakan cepat", + }, + unityProject: { + title: "Proyek Unity", subtitle: "Buat struktur proyek Unity yang lengkap.", + engineSettings: "Pengaturan mesin Unity", + fields: { projectName: "Nama proyek", unityTemplate: "Template Unity", unityVersion: "Versi Unity", targetPlatform: "Platform target", generateDefaultScene: "Buat adegan default", autoInstallPackages: "Instal paket UPM otomatis", setupUrp: "Konfigurasi URP", upmPackages: "Paket UPM (dipisahkan koma)", sceneName: "Nama adegan", unityEditorPath: "Jalur editor Unity (opsional)", timeout: "Batas waktu (detik)", resultJson: "Hasil (JSON)" }, + actions: { generate: "Buat proyek", finalize: "Selesaikan proyek", openFolder: "Buka folder output", addVersion: "Tambah versi Unity", downloadZip: "Unduh proyek selesai (.zip)" }, + dialogs: { addVersion: { title: "Tambah versi Unity", versionId: "ID versi", label: "Label (opsional)" } }, + }, + theme: { light: "Terang", dark: "Gelap", system: "Sistem" }, + languages: { + en: "Inggris", es: "Spanyol", ca: "Katalan", eu: "Basque", oc: "Oksitan", uk: "Ukraina", + pt: "Portugis", gl: "Galisia", fr: "Prancis", it: "Italia", pl: "Polandia", zh: "Tionghoa", + ar: "Arab", de: "Jerman", hi: "Hindi", bn: "Bengali", ur: "Urdu", + id: "Indonesia", ja: "Jepang", vi: "Vietnam", ko: "Korea", + tr: "Turki", + }, +} as const; diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 32402d36..f13546f8 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -1,246 +1,254 @@ -/** - * Italian (it) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "Online", offline: "Offline" }, - nav: { - settings: "Impostazioni", - scenes: "Scene", - code: "Codice", - text: "Testo", - image: "Immagine", - sprites: "Sprites", - audio: "Audio", - unityUi: "Unity UI", - unityPhysics: "Unity Fisica", - unityProject: "Progetto Unity", - }, - actions: { repository: "Repository" }, - }, - common: { - loading: "Caricamento…", - saving: "Salvataggio…", - save: "Salva", - cancel: "Annulla", - close: "Chiudi", - ok: "OK", - yes: "Sì", - no: "No", - error: "Errore", - warning: "Avviso", - success: "Successo", - generate: "Genera", - result: "Risultato", - provider: "Provider", - model: "Modello", - temperature: "Temperatura", - apiKey: "Chiave API (sostituzione opzionale)", - systemPrompt: "Sostituzione del prompt di sistema", - advancedOptions: "Opzioni avanzate", - examplePrompts: "Esempi di prompt", - manageModels: "Gestisci modelli", - selectProvider: "Seleziona provider", - selectModel: "Seleziona modello", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Lascia vuoto per usare la chiave globale", - createdFiles: "File creati", - viewSteps: "Visualizza passaggi ({n})", - waitingForLogs: "In attesa dei log…", - errorsDetected: "Errori rilevati", - }, - settings: { - title: "Configurazione", - subtitle: "Gestisci i tuoi motori di intelligenza, modelli e prompt di sistema.", - tabs: { - general: "Generale", - providers: "Provider", - models: "Modelli", - prompts: "Prompt", - secrets: "Segreti", - }, - }, - general: { - title: "Preferenze generali", - sections: { - networkApi: "Rete e API", - preferredIntelligence: "Intelligenza preferita", - appearance: "Aspetto", - language: "Lingua", - }, - fields: { - backendUrl: "URL del backend", - backendUrlHint: "L'indirizzo del tuo servizio backend Unity Generator", - outputBasePath: "Percorso base (output)", - outputBasePathHint: "Dove vengono salvati i file generati", - unityEditorPath: "Percorso editor Unity (opzionale)", - unityEditorPathHint: "Percorso completo all'eseguibile dell'editor Unity", - preferredLlm: "Provider testo/logica predefinito", - preferredLlmModel: "Modello testo/logica predefinito", - preferredImage: "Provider generazione immagini predefinito", - preferredImageModel: "Modello generazione immagini predefinito", - preferredAudio: "Provider voce (TTS) predefinito", - preferredAudioModel: "Modello voce (TTS) predefinito", - preferredMusic: "Provider musica predefinito", - preferredMusicModel: "Modello musica predefinito", - theme: "Tema", - language: "Lingua dell'interfaccia", - }, - actions: { saveAll: "Salva tutte le modifiche" }, - status: { - saving: "Salvataggio…", - saved: "Preferenze salvate con successo.", - modelNotRegistered: 'Il modello "{model}" non è registrato per il provider "{provider}".', - networkError: "Errore di rete: impossibile contattare il backend.", - }, - }, - code: { - title: "Codice C# Unity", - subtitle: "Genera script C# Unity con l'IA.", - fields: { prompt: "Prompt", maxTokens: "Token massimi" }, - actions: { generate: "Genera codice" }, - activeProject: "Progetto attivo: {name}", - autoSave: "Salva automaticamente nel progetto", - }, - text: { - title: "Generazione testo", - subtitle: "Genera contenuto testuale con l'IA.", - fields: { prompt: "Prompt", length: "Lunghezza" }, - actions: { generate: "Genera testo" }, - }, - image: { - title: "Generazione immagini", - subtitle: "Genera immagini con l'IA.", - fields: { - prompt: "Prompt", - aspectRatio: "Rapporto d'aspetto", - quality: "Qualità", - textureName: "Nome texture", - textureType: "Tipo texture", - }, - actions: { generate: "Genera immagine", saveToUnity: "Salva in Unity" }, - }, - sprites: { - title: "Sprites 2D", - badge: "Ottimizzato per Pixel Art", - subtitle: "Genera sprite 2D per Unity.", - fields: { - prompt: "Prompt", - resolution: "Risoluzione", - paletteSize: "Dimensione palette", - autoCrop: "Ritaglio automatico bordi trasparenti", - colors: "{n} colori", - }, - preview: "Area di anteprima", - actions: { generate: "Genera sprites" }, - }, - audio: { - title: "Generazione audio", - subtitle: "Genera risorse audio con l'IA.", - generationType: "Tipo di generazione", - speechTts: "Voce (TTS)", - atmosphericMusic: "Musica ambientale", - fields: { - prompt: "Prompt", - musicDescription: "Descrizione musicale", - speechPrompt: "Prompt vocale", - voiceOptional: "Voce (opzionale)", - musicModel: "Modello musica", - stability: "Stabilità", - audioClipName: "Nome clip audio", - audioFormat: "Formato audio", - }, - actions: { generate: "Genera audio", saveToUnity: "Salva in Unity" }, - }, - scenes: { - title: "Creatore di scene", - subtitle: "Genera scene Unity con l'IA.", - fields: { prompt: "Descrizione della scena" }, - actions: { generate: "Crea scena" }, - mediaReady: "Media pronto per l'importazione", - mediaReadyText: "{type} \"{name}\" è pronto per essere importato in Unity. Rivedi il prompt e clicca \"Crea scena\" per continuare.", - mediaTypeImage: "Immagine", - mediaTypeAudio: "Audio", - }, - unityUi: { - title: "Unity UI", - subtitle: "Genera componenti UI Unity con l'IA.", - fields: { - uiSystem: "Sistema UI", - elementType: "Tipo elemento", - prompt: "Descrizione elemento UI", - outputFormat: "Formato output", - anchorPreset: "Preset ancoraggio", - colourTheme: "Tema colore (opzionale)", - includeAnimations: "Includi animazioni", - }, - actions: { generate: "Genera UI" }, - }, - unityPhysics: { - title: "Unity Fisica", - subtitle: "Genera configurazione fisica Unity — Rigidbodies, collider, gravità e altro.", - fields: { - physicsBackend: "Motore fisico", - simulationMode: "Modalità simulazione", - gravityPreset: "Preset gravità", - prompt: "Descrizione fisica", - includeRigidbody: "Includi configurazione Rigidbody", - includeColliders: "Includi configurazione collider", - includeLayers: "Includi layer fisici", - }, - actions: { generate: "Genera configurazione fisica" }, - quickActions: "Azioni rapide", - }, - unityProject: { - title: "Progetto Unity", - subtitle: "Genera una struttura completa di progetto Unity.", - engineSettings: "Impostazioni motore Unity", - fields: { - projectName: "Nome progetto", - unityTemplate: "Template Unity", - unityVersion: "Versione Unity", - targetPlatform: "Piattaforma target", - generateDefaultScene: "Genera scena predefinita", - autoInstallPackages: "Installa automaticamente pacchetti UPM", - setupUrp: "Configura URP", - upmPackages: "Pacchetti UPM (separati da virgole)", - sceneName: "Nome scena", - unityEditorPath: "Percorso editor Unity (opzionale)", - timeout: "Timeout (secondi)", - resultJson: "Risultato (JSON)", - }, - actions: { - generate: "Genera progetto", - finalize: "Finalizza progetto", - openFolder: "Apri cartella output", - addVersion: "Aggiungi versione Unity", - downloadZip: "Scarica progetto finalizzato (.zip)", - }, - dialogs: { - addVersion: { - title: "Aggiungi versione Unity", - versionId: "ID versione", - label: "Etichetta (opzionale)", - }, - }, - }, - theme: { light: "Chiaro", dark: "Scuro", system: "Sistema" }, - languages: { - en: "Inglese", - es: "Spagnolo", - ca: "Catalano", - eu: "Basco", - oc: "Occitano", - uk: "Ucraino", - pt: "Portoghese", - gl: "Galiziano", - fr: "Francese", - it: "Italiano", - pl: "Polacco", - zh: "Cinese", - ar: "Arabo", - de: "Tedesco", - tr: "Turco", - }, -} as const; +/** + * Italian (it) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "Online", offline: "Offline" }, + nav: { + settings: "Impostazioni", + scenes: "Scene", + code: "Codice", + text: "Testo", + image: "Immagine", + sprites: "Sprites", + audio: "Audio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Fisica", + unityProject: "Progetto Unity", + logs: "Logs", + }, + actions: { repository: "Repository" }, + }, + common: { + loading: "Caricamento…", + saving: "Salvataggio…", + save: "Salva", + cancel: "Annulla", + close: "Chiudi", + ok: "OK", + yes: "Sì", + no: "No", + error: "Errore", + warning: "Avviso", + success: "Successo", + generate: "Genera", + result: "Risultato", + provider: "Provider", + model: "Modello", + temperature: "Temperatura", + apiKey: "Chiave API (sostituzione opzionale)", + systemPrompt: "Sostituzione del prompt di sistema", + advancedOptions: "Opzioni avanzate", + examplePrompts: "Esempi di prompt", + manageModels: "Gestisci modelli", + selectProvider: "Seleziona provider", + selectModel: "Seleziona modello", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Lascia vuoto per usare la chiave globale", + createdFiles: "File creati", + viewSteps: "Visualizza passaggi ({n})", + waitingForLogs: "In attesa dei log…", + errorsDetected: "Errori rilevati", + }, + settings: { + title: "Configurazione", + subtitle: "Gestisci i tuoi motori di intelligenza, modelli e prompt di sistema.", + tabs: { + general: "Generale", + providers: "Provider", + models: "Modelli", + prompts: "Prompt", + secrets: "Segreti", + }, + }, + general: { + title: "Preferenze generali", + sections: { + networkApi: "Rete e API", + preferredIntelligence: "Intelligenza preferita", + appearance: "Aspetto", + language: "Lingua", + }, + fields: { + backendUrl: "URL del backend", + backendUrlHint: "L'indirizzo del tuo servizio backend Unity Generator", + outputBasePath: "Percorso base (output)", + outputBasePathHint: "Dove vengono salvati i file generati", + unityEditorPath: "Percorso editor Unity (opzionale)", + unityEditorPathHint: "Percorso completo all'eseguibile dell'editor Unity", + preferredLlm: "Provider testo/logica predefinito", + preferredLlmModel: "Modello testo/logica predefinito", + preferredImage: "Provider generazione immagini predefinito", + preferredImageModel: "Modello generazione immagini predefinito", + preferredAudio: "Provider voce (TTS) predefinito", + preferredAudioModel: "Modello voce (TTS) predefinito", + preferredMusic: "Provider musica predefinito", + preferredMusicModel: "Modello musica predefinito", + theme: "Tema", + language: "Lingua dell'interfaccia", + }, + actions: { saveAll: "Salva tutte le modifiche" }, + status: { + saving: "Salvataggio…", + saved: "Preferenze salvate con successo.", + modelNotRegistered: 'Il modello "{model}" non è registrato per il provider "{provider}".', + networkError: "Errore di rete: impossibile contattare il backend.", + }, + }, + code: { + title: "Codice C# Unity", + subtitle: "Genera script C# Unity con l'IA.", + fields: { prompt: "Prompt", maxTokens: "Token massimi" }, + actions: { generate: "Genera codice" }, + activeProject: "Progetto attivo: {name}", + autoSave: "Salva automaticamente nel progetto", + }, + text: { + title: "Generazione testo", + subtitle: "Genera contenuto testuale con l'IA.", + fields: { prompt: "Prompt", length: "Lunghezza" }, + actions: { generate: "Genera testo" }, + }, + image: { + title: "Generazione immagini", + subtitle: "Genera immagini con l'IA.", + fields: { + prompt: "Prompt", + aspectRatio: "Rapporto d'aspetto", + quality: "Qualità", + textureName: "Nome texture", + textureType: "Tipo texture", + }, + actions: { generate: "Genera immagine", saveToUnity: "Salva in Unity" }, + }, + sprites: { + title: "Sprites 2D", + badge: "Ottimizzato per Pixel Art", + subtitle: "Genera sprite 2D per Unity.", + fields: { + prompt: "Prompt", + resolution: "Risoluzione", + paletteSize: "Dimensione palette", + autoCrop: "Ritaglio automatico bordi trasparenti", + colors: "{n} colori", + }, + preview: "Area di anteprima", + actions: { generate: "Genera sprites" }, + }, + audio: { + title: "Generazione audio", + subtitle: "Genera risorse audio con l'IA.", + generationType: "Tipo di generazione", + speechTts: "Voce (TTS)", + atmosphericMusic: "Musica ambientale", + fields: { + prompt: "Prompt", + musicDescription: "Descrizione musicale", + speechPrompt: "Prompt vocale", + voiceOptional: "Voce (opzionale)", + musicModel: "Modello musica", + stability: "Stabilità", + audioClipName: "Nome clip audio", + audioFormat: "Formato audio", + }, + actions: { generate: "Genera audio", saveToUnity: "Salva in Unity" }, + }, + scenes: { + title: "Creatore di scene", + subtitle: "Genera scene Unity con l'IA.", + fields: { prompt: "Descrizione della scena" }, + actions: { generate: "Crea scena" }, + mediaReady: "Media pronto per l'importazione", + mediaReadyText: "{type} \"{name}\" è pronto per essere importato in Unity. Rivedi il prompt e clicca \"Crea scena\" per continuare.", + mediaTypeImage: "Immagine", + mediaTypeAudio: "Audio", + }, + unityUi: { + title: "Unity UI", + subtitle: "Genera componenti UI Unity con l'IA.", + fields: { + uiSystem: "Sistema UI", + elementType: "Tipo elemento", + prompt: "Descrizione elemento UI", + outputFormat: "Formato output", + anchorPreset: "Preset ancoraggio", + colourTheme: "Tema colore (opzionale)", + includeAnimations: "Includi animazioni", + }, + actions: { generate: "Genera UI" }, + }, + unityPhysics: { + title: "Unity Fisica", + subtitle: "Genera configurazione fisica Unity — Rigidbodies, collider, gravità e altro.", + fields: { + physicsBackend: "Motore fisico", + simulationMode: "Modalità simulazione", + gravityPreset: "Preset gravità", + prompt: "Descrizione fisica", + includeRigidbody: "Includi configurazione Rigidbody", + includeColliders: "Includi configurazione collider", + includeLayers: "Includi layer fisici", + }, + actions: { generate: "Genera configurazione fisica" }, + quickActions: "Azioni rapide", + }, + unityProject: { + title: "Progetto Unity", + subtitle: "Genera una struttura completa di progetto Unity.", + engineSettings: "Impostazioni motore Unity", + fields: { + projectName: "Nome progetto", + unityTemplate: "Template Unity", + unityVersion: "Versione Unity", + targetPlatform: "Piattaforma target", + generateDefaultScene: "Genera scena predefinita", + autoInstallPackages: "Installa automaticamente pacchetti UPM", + setupUrp: "Configura URP", + upmPackages: "Pacchetti UPM (separati da virgole)", + sceneName: "Nome scena", + unityEditorPath: "Percorso editor Unity (opzionale)", + timeout: "Timeout (secondi)", + resultJson: "Risultato (JSON)", + }, + actions: { + generate: "Genera progetto", + finalize: "Finalizza progetto", + openFolder: "Apri cartella output", + addVersion: "Aggiungi versione Unity", + downloadZip: "Scarica progetto finalizzato (.zip)", + }, + dialogs: { + addVersion: { + title: "Aggiungi versione Unity", + versionId: "ID versione", + label: "Etichetta (opzionale)", + }, + }, + }, + theme: { light: "Chiaro", dark: "Scuro", system: "Sistema" }, + languages: { + en: "Inglese", + es: "Spagnolo", + ca: "Catalano", + eu: "Basco", + oc: "Occitano", + uk: "Ucraino", + pt: "Portoghese", + gl: "Galiziano", + fr: "Francese", + it: "Italiano", + pl: "Polacco", + zh: "Cinese", + ar: "Arabo", + de: "Tedesco", + tr: "Turco", + }, +} as const; diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 8f4d865e..0d7e36da 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -1,112 +1,120 @@ -/** - * Japanese (ja) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "オンライン", offline: "オフライン" }, - nav: { - settings: "設定", scenes: "シーン", code: "コード", text: "テキスト", - image: "画像", sprites: "スプライト", audio: "オーディオ", - unityUi: "Unity UI", unityPhysics: "Unity 物理", unityProject: "Unity プロジェクト", - }, - actions: { repository: "リポジトリ" }, - }, - common: { - loading: "読み込み中…", saving: "保存中…", save: "保存", - cancel: "キャンセル", close: "閉じる", ok: "OK", yes: "はい", no: "いいえ", - error: "エラー", warning: "警告", success: "成功", generate: "生成", - result: "結果", provider: "プロバイダー", model: "モデル", temperature: "温度", - apiKey: "APIキー(任意のオーバーライド)", systemPrompt: "システムプロンプトのオーバーライド", - advancedOptions: "詳細オプション", examplePrompts: "プロンプト例", - manageModels: "モデルを管理", selectProvider: "プロバイダーを選択", - selectModel: "モデルを選択", prompt: "プロンプト", - leaveEmptyForGlobalKey: "グローバルキーを使用するには空白のままにしてください", - createdFiles: "作成されたファイル", viewSteps: "ステップを表示 ({n})", - waitingForLogs: "ログを待機中…", errorsDetected: "エラーが検出されました", - }, - settings: { - title: "設定", - subtitle: "インテリジェンスエンジン、モデル、システムプロンプトを管理します。", - tabs: { general: "一般", providers: "プロバイダー", models: "モデル", prompts: "プロンプト", secrets: "シークレット" }, - }, - general: { - title: "一般設定", - sections: { networkApi: "ネットワークとAPI", preferredIntelligence: "優先インテリジェンス", appearance: "外観", language: "言語" }, - fields: { - backendUrl: "バックエンドURL", backendUrlHint: "Unity Generatorバックエンドサービスのアドレス", - outputBasePath: "ベースパス(出力)", outputBasePathHint: "生成されたファイルの保存場所", - unityEditorPath: "Unityエディターパス(任意)", unityEditorPathHint: "Unityエディター実行ファイルへのフルパス", - preferredLlm: "デフォルトのテキスト/ロジックプロバイダー", preferredLlmModel: "デフォルトのテキスト/ロジックモデル", - preferredImage: "デフォルトの画像生成プロバイダー", preferredImageModel: "デフォルトの画像生成モデル", - preferredAudio: "デフォルトの音声(TTS)プロバイダー", preferredAudioModel: "デフォルトの音声(TTS)モデル", - preferredMusic: "デフォルトの音楽プロバイダー", preferredMusicModel: "デフォルトの音楽モデル", - theme: "テーマ", language: "インターフェース言語", - }, - actions: { saveAll: "すべての変更を保存" }, - status: { - saving: "保存中…", saved: "設定が正常に保存されました。", - modelNotRegistered: 'モデル"{model}"はプロバイダー"{provider}"に登録されていません。', - networkError: "ネットワークエラー:バックエンドに接続できません。", - }, - }, - code: { - title: "Unity C#コード", subtitle: "AIを使用してUnity C#スクリプトを生成します。", - fields: { prompt: "プロンプト", maxTokens: "最大トークン数" }, - actions: { generate: "コードを生成" }, - activeProject: "アクティブプロジェクト: {name}", autoSave: "プロジェクトに自動保存", - }, - text: { - title: "テキスト生成", subtitle: "AIを使用してテキストコンテンツを生成します。", - fields: { prompt: "プロンプト", length: "長さ" }, actions: { generate: "テキストを生成" }, - }, - image: { - title: "画像生成", subtitle: "AIを使用して画像を生成します。", - fields: { prompt: "プロンプト", aspectRatio: "アスペクト比", quality: "品質", textureName: "テクスチャ名", textureType: "テクスチャタイプ" }, - actions: { generate: "画像を生成", saveToUnity: "Unityに保存" }, - }, - sprites: { - title: "2Dスプライト", badge: "ピクセルアート向けに最適化", subtitle: "Unity用の2Dスプライトを生成します。", - fields: { prompt: "プロンプト", resolution: "解像度", paletteSize: "パレットサイズ", autoCrop: "透明な端を自動トリミング", colors: "{n}色" }, - preview: "プレビューエリア", actions: { generate: "スプライトを生成" }, - }, - audio: { - title: "オーディオ生成", subtitle: "AIを使用してオーディオアセットを生成します。", - generationType: "生成タイプ", speechTts: "音声(TTS)", atmosphericMusic: "雰囲気音楽", - fields: { prompt: "プロンプト", musicDescription: "音楽の説明", speechPrompt: "音声プロンプト", voiceOptional: "ボイス(任意)", musicModel: "音楽モデル", stability: "安定性", audioClipName: "オーディオクリップ名", audioFormat: "オーディオフォーマット" }, - actions: { generate: "オーディオを生成", saveToUnity: "Unityに保存" }, - }, - scenes: { - title: "シーンクリエーター", subtitle: "AIを使用してUnityシーンを生成します。", - fields: { prompt: "シーンの説明" }, actions: { generate: "シーンを作成" }, - mediaReady: "メディアのインポート準備完了", - mediaReadyText: "{type}「{name}」はUnityにインポートする準備ができています。プロンプトを確認して「シーンを作成」をクリックして続行してください。", - mediaTypeImage: "画像", mediaTypeAudio: "オーディオ", - }, - unityUi: { - title: "Unity UI", subtitle: "AIを使用してUnity UIコンポーネントを生成します。", - fields: { uiSystem: "UIシステム", elementType: "要素タイプ", prompt: "UI要素の説明", outputFormat: "出力フォーマット", anchorPreset: "アンカープリセット", colourTheme: "カラーテーマ(任意)", includeAnimations: "アニメーションを含める" }, - actions: { generate: "UIを生成" }, - }, - unityPhysics: { - title: "Unity物理", subtitle: "Unity物理設定を生成します — Rigidbodies、コライダー、重力など。", - fields: { physicsBackend: "物理エンジン", simulationMode: "シミュレーションモード", gravityPreset: "重力プリセット", prompt: "物理の説明", includeRigidbody: "Rigidbody設定を含める", includeColliders: "コライダー設定を含める", includeLayers: "物理レイヤーを含める" }, - actions: { generate: "物理設定を生成" }, - quickActions: "クイックアクション", - }, - unityProject: { - title: "Unityプロジェクト", subtitle: "完全なUnityプロジェクト構造を生成します。", - engineSettings: "Unityエンジン設定", - fields: { projectName: "プロジェクト名", unityTemplate: "Unityテンプレート", unityVersion: "Unityバージョン", targetPlatform: "ターゲットプラットフォーム", generateDefaultScene: "デフォルトシーンを生成", autoInstallPackages: "UPMパッケージを自動インストール", setupUrp: "URPを設定", upmPackages: "UPMパッケージ(カンマ区切り)", sceneName: "シーン名", unityEditorPath: "Unityエディターパス(任意)", timeout: "タイムアウト(秒)", resultJson: "結果(JSON)" }, - actions: { generate: "プロジェクトを生成", finalize: "プロジェクトを完成", openFolder: "出力フォルダーを開く", addVersion: "Unityバージョンを追加", downloadZip: "完成したプロジェクトをダウンロード(.zip)" }, - dialogs: { addVersion: { title: "Unityバージョンを追加", versionId: "バージョンID", label: "ラベル(任意)" } }, - }, - theme: { light: "ライト", dark: "ダーク", system: "システム" }, - languages: { - en: "英語", es: "スペイン語", ca: "カタルーニャ語", eu: "バスク語", oc: "オック語", uk: "ウクライナ語", - pt: "ポルトガル語", gl: "ガリシア語", fr: "フランス語", it: "イタリア語", pl: "ポーランド語", zh: "中国語", - ar: "アラビア語", de: "ドイツ語", hi: "ヒンディー語", bn: "ベンガル語", ur: "ウルドゥー語", - id: "インドネシア語", ja: "日本語", vi: "ベトナム語", ko: "韓国語", - tr: "トルコ語", - }, -} as const; +/** + * Japanese (ja) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "オンライン", offline: "オフライン" }, + nav: { + settings: "設定", scenes: "シーン", code: "コード", text: "テキスト", + image: "画像", sprites: "スプライト", audio: "オーディオ", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", unityPhysics: "Unity 物理", unityProject: "Unity プロジェクト", + logs: "Logs", + }, + actions: { repository: "リポジトリ" }, + }, + common: { + loading: "読み込み中…", saving: "保存中…", save: "保存", + cancel: "キャンセル", close: "閉じる", ok: "OK", yes: "はい", no: "いいえ", + error: "エラー", warning: "警告", success: "成功", generate: "生成", + result: "結果", provider: "プロバイダー", model: "モデル", temperature: "温度", + apiKey: "APIキー(任意のオーバーライド)", systemPrompt: "システムプロンプトのオーバーライド", + advancedOptions: "詳細オプション", examplePrompts: "プロンプト例", + manageModels: "モデルを管理", selectProvider: "プロバイダーを選択", + selectModel: "モデルを選択", prompt: "プロンプト", + leaveEmptyForGlobalKey: "グローバルキーを使用するには空白のままにしてください", + createdFiles: "作成されたファイル", viewSteps: "ステップを表示 ({n})", + waitingForLogs: "ログを待機中…", errorsDetected: "エラーが検出されました", + }, + settings: { + title: "設定", + subtitle: "インテリジェンスエンジン、モデル、システムプロンプトを管理します。", + tabs: { general: "一般", providers: "プロバイダー", models: "モデル", prompts: "プロンプト", secrets: "シークレット" }, + }, + general: { + title: "一般設定", + sections: { networkApi: "ネットワークとAPI", preferredIntelligence: "優先インテリジェンス", appearance: "外観", language: "言語" }, + fields: { + backendUrl: "バックエンドURL", backendUrlHint: "Unity Generatorバックエンドサービスのアドレス", + outputBasePath: "ベースパス(出力)", outputBasePathHint: "生成されたファイルの保存場所", + unityEditorPath: "Unityエディターパス(任意)", unityEditorPathHint: "Unityエディター実行ファイルへのフルパス", + preferredLlm: "デフォルトのテキスト/ロジックプロバイダー", preferredLlmModel: "デフォルトのテキスト/ロジックモデル", + preferredImage: "デフォルトの画像生成プロバイダー", preferredImageModel: "デフォルトの画像生成モデル", + preferredAudio: "デフォルトの音声(TTS)プロバイダー", preferredAudioModel: "デフォルトの音声(TTS)モデル", + preferredMusic: "デフォルトの音楽プロバイダー", preferredMusicModel: "デフォルトの音楽モデル", + theme: "テーマ", language: "インターフェース言語", + }, + actions: { saveAll: "すべての変更を保存" }, + status: { + saving: "保存中…", saved: "設定が正常に保存されました。", + modelNotRegistered: 'モデル"{model}"はプロバイダー"{provider}"に登録されていません。', + networkError: "ネットワークエラー:バックエンドに接続できません。", + }, + }, + code: { + title: "Unity C#コード", subtitle: "AIを使用してUnity C#スクリプトを生成します。", + fields: { prompt: "プロンプト", maxTokens: "最大トークン数" }, + actions: { generate: "コードを生成" }, + activeProject: "アクティブプロジェクト: {name}", autoSave: "プロジェクトに自動保存", + }, + text: { + title: "テキスト生成", subtitle: "AIを使用してテキストコンテンツを生成します。", + fields: { prompt: "プロンプト", length: "長さ" }, actions: { generate: "テキストを生成" }, + }, + image: { + title: "画像生成", subtitle: "AIを使用して画像を生成します。", + fields: { prompt: "プロンプト", aspectRatio: "アスペクト比", quality: "品質", textureName: "テクスチャ名", textureType: "テクスチャタイプ" }, + actions: { generate: "画像を生成", saveToUnity: "Unityに保存" }, + }, + sprites: { + title: "2Dスプライト", badge: "ピクセルアート向けに最適化", subtitle: "Unity用の2Dスプライトを生成します。", + fields: { prompt: "プロンプト", resolution: "解像度", paletteSize: "パレットサイズ", autoCrop: "透明な端を自動トリミング", colors: "{n}色" }, + preview: "プレビューエリア", actions: { generate: "スプライトを生成" }, + }, + audio: { + title: "オーディオ生成", subtitle: "AIを使用してオーディオアセットを生成します。", + generationType: "生成タイプ", speechTts: "音声(TTS)", atmosphericMusic: "雰囲気音楽", + fields: { prompt: "プロンプト", musicDescription: "音楽の説明", speechPrompt: "音声プロンプト", voiceOptional: "ボイス(任意)", musicModel: "音楽モデル", stability: "安定性", audioClipName: "オーディオクリップ名", audioFormat: "オーディオフォーマット" }, + actions: { generate: "オーディオを生成", saveToUnity: "Unityに保存" }, + }, + scenes: { + title: "シーンクリエーター", subtitle: "AIを使用してUnityシーンを生成します。", + fields: { prompt: "シーンの説明" }, actions: { generate: "シーンを作成" }, + mediaReady: "メディアのインポート準備完了", + mediaReadyText: "{type}「{name}」はUnityにインポートする準備ができています。プロンプトを確認して「シーンを作成」をクリックして続行してください。", + mediaTypeImage: "画像", mediaTypeAudio: "オーディオ", + }, + unityUi: { + title: "Unity UI", subtitle: "AIを使用してUnity UIコンポーネントを生成します。", + fields: { uiSystem: "UIシステム", elementType: "要素タイプ", prompt: "UI要素の説明", outputFormat: "出力フォーマット", anchorPreset: "アンカープリセット", colourTheme: "カラーテーマ(任意)", includeAnimations: "アニメーションを含める" }, + actions: { generate: "UIを生成" }, + }, + unityPhysics: { + title: "Unity物理", subtitle: "Unity物理設定を生成します — Rigidbodies、コライダー、重力など。", + fields: { physicsBackend: "物理エンジン", simulationMode: "シミュレーションモード", gravityPreset: "重力プリセット", prompt: "物理の説明", includeRigidbody: "Rigidbody設定を含める", includeColliders: "コライダー設定を含める", includeLayers: "物理レイヤーを含める" }, + actions: { generate: "物理設定を生成" }, + quickActions: "クイックアクション", + }, + unityProject: { + title: "Unityプロジェクト", subtitle: "完全なUnityプロジェクト構造を生成します。", + engineSettings: "Unityエンジン設定", + fields: { projectName: "プロジェクト名", unityTemplate: "Unityテンプレート", unityVersion: "Unityバージョン", targetPlatform: "ターゲットプラットフォーム", generateDefaultScene: "デフォルトシーンを生成", autoInstallPackages: "UPMパッケージを自動インストール", setupUrp: "URPを設定", upmPackages: "UPMパッケージ(カンマ区切り)", sceneName: "シーン名", unityEditorPath: "Unityエディターパス(任意)", timeout: "タイムアウト(秒)", resultJson: "結果(JSON)" }, + actions: { generate: "プロジェクトを生成", finalize: "プロジェクトを完成", openFolder: "出力フォルダーを開く", addVersion: "Unityバージョンを追加", downloadZip: "完成したプロジェクトをダウンロード(.zip)" }, + dialogs: { addVersion: { title: "Unityバージョンを追加", versionId: "バージョンID", label: "ラベル(任意)" } }, + }, + theme: { light: "ライト", dark: "ダーク", system: "システム" }, + languages: { + en: "英語", es: "スペイン語", ca: "カタルーニャ語", eu: "バスク語", oc: "オック語", uk: "ウクライナ語", + pt: "ポルトガル語", gl: "ガリシア語", fr: "フランス語", it: "イタリア語", pl: "ポーランド語", zh: "中国語", + ar: "アラビア語", de: "ドイツ語", hi: "ヒンディー語", bn: "ベンガル語", ur: "ウルドゥー語", + id: "インドネシア語", ja: "日本語", vi: "ベトナム語", ko: "韓国語", + tr: "トルコ語", + }, +} as const; diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index 66b00abd..5d846d09 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -1,112 +1,120 @@ -/** - * Korean (ko) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "온라인", offline: "오프라인" }, - nav: { - settings: "설정", scenes: "씬", code: "코드", text: "텍스트", - image: "이미지", sprites: "스프라이트", audio: "오디오", - unityUi: "Unity UI", unityPhysics: "Unity 물리", unityProject: "Unity 프로젝트", - }, - actions: { repository: "저장소" }, - }, - common: { - loading: "로딩 중…", saving: "저장 중…", save: "저장", - cancel: "취소", close: "닫기", ok: "확인", yes: "예", no: "아니오", - error: "오류", warning: "경고", success: "성공", generate: "생성", - result: "결과", provider: "공급자", model: "모델", temperature: "온도", - apiKey: "API 키 (선택적 재정의)", systemPrompt: "시스템 프롬프트 재정의", - advancedOptions: "고급 옵션", examplePrompts: "프롬프트 예시", - manageModels: "모델 관리", selectProvider: "공급자 선택", - selectModel: "모델 선택", prompt: "프롬프트", - leaveEmptyForGlobalKey: "전역 키를 사용하려면 비워두세요", - createdFiles: "생성된 파일", viewSteps: "단계 보기 ({n})", - waitingForLogs: "로그 대기 중…", errorsDetected: "오류 감지됨", - }, - settings: { - title: "설정", - subtitle: "인텔리전스 엔진, 모델 및 시스템 프롬프트를 관리합니다.", - tabs: { general: "일반", providers: "공급자", models: "모델", prompts: "프롬프트", secrets: "비밀" }, - }, - general: { - title: "일반 설정", - sections: { networkApi: "네트워크 및 API", preferredIntelligence: "선호 인텔리전스", appearance: "외관", language: "언어" }, - fields: { - backendUrl: "백엔드 URL", backendUrlHint: "Unity Generator 백엔드 서비스 주소", - outputBasePath: "기본 경로 (출력)", outputBasePathHint: "생성된 파일이 저장되는 위치", - unityEditorPath: "Unity 에디터 경로 (선택)", unityEditorPathHint: "Unity 에디터 실행 파일의 전체 경로", - preferredLlm: "기본 텍스트/로직 공급자", preferredLlmModel: "기본 텍스트/로직 모델", - preferredImage: "기본 이미지 생성 공급자", preferredImageModel: "기본 이미지 생성 모델", - preferredAudio: "기본 음성 (TTS) 공급자", preferredAudioModel: "기본 음성 (TTS) 모델", - preferredMusic: "기본 음악 공급자", preferredMusicModel: "기본 음악 모델", - theme: "테마", language: "인터페이스 언어", - }, - actions: { saveAll: "모든 변경사항 저장" }, - status: { - saving: "저장 중…", saved: "설정이 성공적으로 저장되었습니다.", - modelNotRegistered: '모델 "{model}"은(는) 공급자 "{provider}"에 등록되지 않았습니다.', - networkError: "네트워크 오류: 백엔드에 연결할 수 없습니다.", - }, - }, - code: { - title: "Unity C# 코드", subtitle: "AI를 사용하여 Unity C# 스크립트를 생성합니다.", - fields: { prompt: "프롬프트", maxTokens: "최대 토큰" }, - actions: { generate: "코드 생성" }, - activeProject: "활성 프로젝트: {name}", autoSave: "프로젝트에 자동 저장", - }, - text: { - title: "텍스트 생성", subtitle: "AI를 사용하여 텍스트 콘텐츠를 생성합니다.", - fields: { prompt: "프롬프트", length: "길이" }, actions: { generate: "텍스트 생성" }, - }, - image: { - title: "이미지 생성", subtitle: "AI를 사용하여 이미지를 생성합니다.", - fields: { prompt: "프롬프트", aspectRatio: "화면 비율", quality: "품질", textureName: "텍스처 이름", textureType: "텍스처 유형" }, - actions: { generate: "이미지 생성", saveToUnity: "Unity에 저장" }, - }, - sprites: { - title: "2D 스프라이트", badge: "픽셀 아트에 최적화", subtitle: "Unity용 2D 스프라이트를 생성합니다.", - fields: { prompt: "프롬프트", resolution: "해상도", paletteSize: "팔레트 크기", autoCrop: "투명 가장자리 자동 자르기", colors: "{n}가지 색상" }, - preview: "미리보기 영역", actions: { generate: "스프라이트 생성" }, - }, - audio: { - title: "오디오 생성", subtitle: "AI를 사용하여 오디오 에셋을 생성합니다.", - generationType: "생성 유형", speechTts: "음성 (TTS)", atmosphericMusic: "분위기 음악", - fields: { prompt: "프롬프트", musicDescription: "음악 설명", speechPrompt: "음성 프롬프트", voiceOptional: "음성 (선택)", musicModel: "음악 모델", stability: "안정성", audioClipName: "오디오 클립 이름", audioFormat: "오디오 형식" }, - actions: { generate: "오디오 생성", saveToUnity: "Unity에 저장" }, - }, - scenes: { - title: "씬 생성기", subtitle: "AI를 사용하여 Unity 씬을 생성합니다.", - fields: { prompt: "씬 설명" }, actions: { generate: "씬 생성" }, - mediaReady: "미디어 가져오기 준비 완료", - mediaReadyText: "{type} \"{name}\"을(를) Unity로 가져올 준비가 되었습니다. 프롬프트를 검토하고 \"씬 생성\"을 클릭하여 계속하세요.", - mediaTypeImage: "이미지", mediaTypeAudio: "오디오", - }, - unityUi: { - title: "Unity UI", subtitle: "AI를 사용하여 Unity UI 컴포넌트를 생성합니다.", - fields: { uiSystem: "UI 시스템", elementType: "요소 유형", prompt: "UI 요소 설명", outputFormat: "출력 형식", anchorPreset: "앵커 프리셋", colourTheme: "색상 테마 (선택)", includeAnimations: "애니메이션 포함" }, - actions: { generate: "UI 생성" }, - }, - unityPhysics: { - title: "Unity 물리", subtitle: "Unity 물리 설정 생성 — Rigidbodies, 콜라이더, 중력 등.", - fields: { physicsBackend: "물리 엔진", simulationMode: "시뮬레이션 모드", gravityPreset: "중력 프리셋", prompt: "물리 설명", includeRigidbody: "Rigidbody 설정 포함", includeColliders: "콜라이더 설정 포함", includeLayers: "물리 레이어 포함" }, - actions: { generate: "물리 설정 생성" }, - quickActions: "빠른 작업", - }, - unityProject: { - title: "Unity 프로젝트", subtitle: "완전한 Unity 프로젝트 구조를 생성합니다.", - engineSettings: "Unity 엔진 설정", - fields: { projectName: "프로젝트 이름", unityTemplate: "Unity 템플릿", unityVersion: "Unity 버전", targetPlatform: "대상 플랫폼", generateDefaultScene: "기본 씬 생성", autoInstallPackages: "UPM 패키지 자동 설치", setupUrp: "URP 설정", upmPackages: "UPM 패키지 (쉼표로 구분)", sceneName: "씬 이름", unityEditorPath: "Unity 에디터 경로 (선택)", timeout: "타임아웃 (초)", resultJson: "결과 (JSON)" }, - actions: { generate: "프로젝트 생성", finalize: "프로젝트 완성", openFolder: "출력 폴더 열기", addVersion: "Unity 버전 추가", downloadZip: "완성된 프로젝트 다운로드 (.zip)" }, - dialogs: { addVersion: { title: "Unity 버전 추가", versionId: "버전 ID", label: "레이블 (선택)" } }, - }, - theme: { light: "라이트", dark: "다크", system: "시스템" }, - languages: { - en: "영어", es: "스페인어", ca: "카탈루냐어", eu: "바스크어", oc: "오크어", uk: "우크라이나어", - pt: "포르투갈어", gl: "갈리시아어", fr: "프랑스어", it: "이탈리아어", pl: "폴란드어", zh: "중국어", - ar: "아랍어", de: "독일어", hi: "힌디어", bn: "벵골어", ur: "우르두어", - id: "인도네시아어", ja: "일본어", vi: "베트남어", ko: "한국어", - tr: "터키어", - }, -} as const; +/** + * Korean (ko) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "온라인", offline: "오프라인" }, + nav: { + settings: "설정", scenes: "씬", code: "코드", text: "텍스트", + image: "이미지", sprites: "스프라이트", audio: "오디오", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", unityPhysics: "Unity 물리", unityProject: "Unity 프로젝트", + logs: "Logs", + }, + actions: { repository: "저장소" }, + }, + common: { + loading: "로딩 중…", saving: "저장 중…", save: "저장", + cancel: "취소", close: "닫기", ok: "확인", yes: "예", no: "아니오", + error: "오류", warning: "경고", success: "성공", generate: "생성", + result: "결과", provider: "공급자", model: "모델", temperature: "온도", + apiKey: "API 키 (선택적 재정의)", systemPrompt: "시스템 프롬프트 재정의", + advancedOptions: "고급 옵션", examplePrompts: "프롬프트 예시", + manageModels: "모델 관리", selectProvider: "공급자 선택", + selectModel: "모델 선택", prompt: "프롬프트", + leaveEmptyForGlobalKey: "전역 키를 사용하려면 비워두세요", + createdFiles: "생성된 파일", viewSteps: "단계 보기 ({n})", + waitingForLogs: "로그 대기 중…", errorsDetected: "오류 감지됨", + }, + settings: { + title: "설정", + subtitle: "인텔리전스 엔진, 모델 및 시스템 프롬프트를 관리합니다.", + tabs: { general: "일반", providers: "공급자", models: "모델", prompts: "프롬프트", secrets: "비밀" }, + }, + general: { + title: "일반 설정", + sections: { networkApi: "네트워크 및 API", preferredIntelligence: "선호 인텔리전스", appearance: "외관", language: "언어" }, + fields: { + backendUrl: "백엔드 URL", backendUrlHint: "Unity Generator 백엔드 서비스 주소", + outputBasePath: "기본 경로 (출력)", outputBasePathHint: "생성된 파일이 저장되는 위치", + unityEditorPath: "Unity 에디터 경로 (선택)", unityEditorPathHint: "Unity 에디터 실행 파일의 전체 경로", + preferredLlm: "기본 텍스트/로직 공급자", preferredLlmModel: "기본 텍스트/로직 모델", + preferredImage: "기본 이미지 생성 공급자", preferredImageModel: "기본 이미지 생성 모델", + preferredAudio: "기본 음성 (TTS) 공급자", preferredAudioModel: "기본 음성 (TTS) 모델", + preferredMusic: "기본 음악 공급자", preferredMusicModel: "기본 음악 모델", + theme: "테마", language: "인터페이스 언어", + }, + actions: { saveAll: "모든 변경사항 저장" }, + status: { + saving: "저장 중…", saved: "설정이 성공적으로 저장되었습니다.", + modelNotRegistered: '모델 "{model}"은(는) 공급자 "{provider}"에 등록되지 않았습니다.', + networkError: "네트워크 오류: 백엔드에 연결할 수 없습니다.", + }, + }, + code: { + title: "Unity C# 코드", subtitle: "AI를 사용하여 Unity C# 스크립트를 생성합니다.", + fields: { prompt: "프롬프트", maxTokens: "최대 토큰" }, + actions: { generate: "코드 생성" }, + activeProject: "활성 프로젝트: {name}", autoSave: "프로젝트에 자동 저장", + }, + text: { + title: "텍스트 생성", subtitle: "AI를 사용하여 텍스트 콘텐츠를 생성합니다.", + fields: { prompt: "프롬프트", length: "길이" }, actions: { generate: "텍스트 생성" }, + }, + image: { + title: "이미지 생성", subtitle: "AI를 사용하여 이미지를 생성합니다.", + fields: { prompt: "프롬프트", aspectRatio: "화면 비율", quality: "품질", textureName: "텍스처 이름", textureType: "텍스처 유형" }, + actions: { generate: "이미지 생성", saveToUnity: "Unity에 저장" }, + }, + sprites: { + title: "2D 스프라이트", badge: "픽셀 아트에 최적화", subtitle: "Unity용 2D 스프라이트를 생성합니다.", + fields: { prompt: "프롬프트", resolution: "해상도", paletteSize: "팔레트 크기", autoCrop: "투명 가장자리 자동 자르기", colors: "{n}가지 색상" }, + preview: "미리보기 영역", actions: { generate: "스프라이트 생성" }, + }, + audio: { + title: "오디오 생성", subtitle: "AI를 사용하여 오디오 에셋을 생성합니다.", + generationType: "생성 유형", speechTts: "음성 (TTS)", atmosphericMusic: "분위기 음악", + fields: { prompt: "프롬프트", musicDescription: "음악 설명", speechPrompt: "음성 프롬프트", voiceOptional: "음성 (선택)", musicModel: "음악 모델", stability: "안정성", audioClipName: "오디오 클립 이름", audioFormat: "오디오 형식" }, + actions: { generate: "오디오 생성", saveToUnity: "Unity에 저장" }, + }, + scenes: { + title: "씬 생성기", subtitle: "AI를 사용하여 Unity 씬을 생성합니다.", + fields: { prompt: "씬 설명" }, actions: { generate: "씬 생성" }, + mediaReady: "미디어 가져오기 준비 완료", + mediaReadyText: "{type} \"{name}\"을(를) Unity로 가져올 준비가 되었습니다. 프롬프트를 검토하고 \"씬 생성\"을 클릭하여 계속하세요.", + mediaTypeImage: "이미지", mediaTypeAudio: "오디오", + }, + unityUi: { + title: "Unity UI", subtitle: "AI를 사용하여 Unity UI 컴포넌트를 생성합니다.", + fields: { uiSystem: "UI 시스템", elementType: "요소 유형", prompt: "UI 요소 설명", outputFormat: "출력 형식", anchorPreset: "앵커 프리셋", colourTheme: "색상 테마 (선택)", includeAnimations: "애니메이션 포함" }, + actions: { generate: "UI 생성" }, + }, + unityPhysics: { + title: "Unity 물리", subtitle: "Unity 물리 설정 생성 — Rigidbodies, 콜라이더, 중력 등.", + fields: { physicsBackend: "물리 엔진", simulationMode: "시뮬레이션 모드", gravityPreset: "중력 프리셋", prompt: "물리 설명", includeRigidbody: "Rigidbody 설정 포함", includeColliders: "콜라이더 설정 포함", includeLayers: "물리 레이어 포함" }, + actions: { generate: "물리 설정 생성" }, + quickActions: "빠른 작업", + }, + unityProject: { + title: "Unity 프로젝트", subtitle: "완전한 Unity 프로젝트 구조를 생성합니다.", + engineSettings: "Unity 엔진 설정", + fields: { projectName: "프로젝트 이름", unityTemplate: "Unity 템플릿", unityVersion: "Unity 버전", targetPlatform: "대상 플랫폼", generateDefaultScene: "기본 씬 생성", autoInstallPackages: "UPM 패키지 자동 설치", setupUrp: "URP 설정", upmPackages: "UPM 패키지 (쉼표로 구분)", sceneName: "씬 이름", unityEditorPath: "Unity 에디터 경로 (선택)", timeout: "타임아웃 (초)", resultJson: "결과 (JSON)" }, + actions: { generate: "프로젝트 생성", finalize: "프로젝트 완성", openFolder: "출력 폴더 열기", addVersion: "Unity 버전 추가", downloadZip: "완성된 프로젝트 다운로드 (.zip)" }, + dialogs: { addVersion: { title: "Unity 버전 추가", versionId: "버전 ID", label: "레이블 (선택)" } }, + }, + theme: { light: "라이트", dark: "다크", system: "시스템" }, + languages: { + en: "영어", es: "스페인어", ca: "카탈루냐어", eu: "바스크어", oc: "오크어", uk: "우크라이나어", + pt: "포르투갈어", gl: "갈리시아어", fr: "프랑스어", it: "이탈리아어", pl: "폴란드어", zh: "중국어", + ar: "아랍어", de: "독일어", hi: "힌디어", bn: "벵골어", ur: "우르두어", + id: "인도네시아어", ja: "일본어", vi: "베트남어", ko: "한국어", + tr: "터키어", + }, +} as const; diff --git a/frontend/src/i18n/locales/oc.ts b/frontend/src/i18n/locales/oc.ts index b94a8bdd..8eac91fa 100644 --- a/frontend/src/i18n/locales/oc.ts +++ b/frontend/src/i18n/locales/oc.ts @@ -1,253 +1,261 @@ -/** - * Occitan (oc) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "En linha", offline: "Fòra linha" }, - nav: { - settings: "Paramètres", - scenes: "Escènas", - code: "Còdi", - text: "Tèxt", - image: "Imatge", - sprites: "Sprites", - audio: "Àudio", - unityUi: "Unity UI", - unityPhysics: "Unity Fisica", - unityProject: "Projècte Unity", - }, - actions: { repository: "Depaus" }, - }, - common: { - loading: "Cargament…", - saving: "Enregistrament…", - save: "Enregistrar", - cancel: "Anullar", - close: "Tampar", - ok: "D'acòrd", - yes: "Òc", - no: "Non", - error: "Error", - warning: "Avertiment", - success: "Succès", - generate: "Generar", - result: "Resultat", - provider: "Provesidor", - model: "Modèl", - temperature: "Temperatura", - apiKey: "Clau API (substitucion facultativa)", - systemPrompt: "Substitucion del prompt sistèma", - advancedOptions: "Opcions avançadas", - examplePrompts: "Exemples de prompts", - manageModels: "Gerir los modèls", - selectProvider: "Causir un provesidor", - selectModel: "Causir un modèl", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Laissar vuenh per utilizar la clau globala", - createdFiles: "Fichièrs creats", - viewSteps: "Veire las etapas ({n})", - waitingForLogs: "En espèra dels jornals…", - errorsDetected: "Errors detectadas", - }, - settings: { - title: "Configuracion", - subtitle: "Gerir vòstres motors d'intelligéncia, modèls e prompts sistèma.", - tabs: { - general: "General", - providers: "Provesidors", - models: "Modèls", - prompts: "Prompts", - secrets: "Secrets", - }, - }, - general: { - title: "Preferéncias generalas", - sections: { - networkApi: "Ret e API", - preferredIntelligence: "Intelligéncia preferida", - appearance: "Aparéncia", - language: "Lenga", - }, - fields: { - backendUrl: "URL del backend", - backendUrlHint: "L'adreça de vòstre servici backend Unity Generator", - outputBasePath: "Camin de basa (sortida)", - outputBasePathHint: "Ont son enregistrats los fichièrs generats", - unityEditorPath: "Camin de l'editor Unity (facultatiu)", - unityEditorPathHint: "Camin complet cap a l'executable de l'editor Unity", - preferredLlm: "Provesidor de tèxt/logica per defaut", - preferredLlmModel: "Modèl de tèxt/logica per defaut", - preferredImage: "Provesidor de generacion d'imatges per defaut", - preferredImageModel: "Modèl de generacion d'imatges per defaut", - preferredAudio: "Provesidor de vòtz (TTS) per defaut", - preferredAudioModel: "Modèl de vòtz (TTS) per defaut", - preferredMusic: "Provesidor de musica per defaut", - preferredMusicModel: "Modèl de musica per defaut", - theme: "Tèma", - language: "Lenga de l'interfàcia", - }, - actions: { saveAll: "Enregistrar cambiaments" }, - status: { - saving: "Enregistrament…", - saved: "Preferéncias enregistradas amb succès.", - modelNotRegistered: 'Lo modèl "{model}" es pas enregistrat pel provesidor "{provider}".', - networkError: "Error de ret : impossible de contactar lo backend.", - }, - }, - code: { - title: "Còdi C# Unity", - subtitle: "Generar scripts C# Unity amb IA.", - fields: { prompt: "Prompt", maxTokens: "Tokens màxims" }, - actions: { generate: "Generar lo còdi" }, - activeProject: "Projècte actiu : {name}", - autoSave: "Enregistrar automaticament al projècte", - }, - text: { - title: "Generacion de tèxt", - subtitle: "Generar contengut de tèxt amb IA.", - fields: { prompt: "Prompt", length: "Longor" }, - actions: { generate: "Generar lo tèxt" }, - }, - image: { - title: "Generacion d'imatges", - subtitle: "Generar imatges amb IA.", - fields: { - prompt: "Prompt", - aspectRatio: "Ràpòrt d'aspècte", - quality: "Qualitat", - textureName: "Nom de la textura", - textureType: "Tipe de textura", - }, - actions: { generate: "Generar l'imatge", saveToUnity: "Enregistrar dins Unity" }, - }, - sprites: { - title: "Sprites 2D", - badge: "Optimizat per Pixel Art", - subtitle: "Generar sprites 2D per Unity.", - fields: { - prompt: "Prompt", - resolution: "Resolucion", - paletteSize: "Talha de la paleta", - autoCrop: "Retalhar automaticament las voras transparentas", - colors: "{n} colors", - }, - preview: "Zòna de previsualizar", - actions: { generate: "Generar los sprites" }, - }, - audio: { - title: "Generacion d'àudio", - subtitle: "Generar ressorsas àudio amb IA.", - generationType: "Tipe de generacion", - speechTts: "Vòtz (TTS)", - atmosphericMusic: "Musica d'atmosfèra", - fields: { - prompt: "Prompt", - musicDescription: "Descripcion musicala", - speechPrompt: "Prompt de vòtz", - voiceOptional: "Vòtz (facultativa)", - musicModel: "Modèl de musica", - stability: "Estabilitat", - audioClipName: "Nom del clip àudio", - audioFormat: "Format àudio", - }, - actions: { generate: "Generar l'àudio", saveToUnity: "Enregistrar dins Unity" }, - }, - scenes: { - title: "Creaire d'escènas", - subtitle: "Generar escènas Unity amb IA.", - fields: { prompt: "Descripcion de l'escèna" }, - actions: { generate: "Crear l'escèna" }, - mediaReady: "Mèdia prèst a importar", - mediaReadyText: "{type} \"{name}\" es prèst a èsser importat dins Unity. Revisatz lo prompt e clicatz \"Crear l'escèna\" per continuar.", - mediaTypeImage: "Imatge", - mediaTypeAudio: "Àudio", - }, - unityUi: { - title: "Unity UI", - subtitle: "Generar components UI Unity amb IA.", - fields: { - uiSystem: "Sistèma UI", - elementType: "Tipe d'element", - prompt: "Descripcion de l'element UI", - outputFormat: "Format de sortida", - anchorPreset: "Preset d'ancoratge", - colourTheme: "Tèma de color (facultatiu)", - includeAnimations: "Includir animacions", - }, - actions: { generate: "Generar l'UI" }, - }, - unityPhysics: { - title: "Unity Fisica", - subtitle: "Generar la configuracion de fisica Unity — Rigidbodies, colisionadors, gravitacion e mai.", - fields: { - physicsBackend: "Motor de fisica", - simulationMode: "Mòde de simulacion", - gravityPreset: "Preset de gravitacion", - prompt: "Descripcion de la fisica", - includeRigidbody: "Includir la configuracion Rigidbody", - includeColliders: "Includir la configuracion dels colisionadors", - includeLayers: "Includir las capas de fisica", - }, - actions: { generate: "Generar la configuracion de fisica" }, - quickActions: "Accions ràpidas", - }, - unityProject: { - title: "Projècte Unity", - subtitle: "Generar una estructura complèta de projècte Unity.", - engineSettings: "Paramètres del motor Unity", - fields: { - projectName: "Nom del projècte", - unityTemplate: "Modèl Unity", - unityVersion: "Version Unity", - targetPlatform: "Plataforma cibla", - generateDefaultScene: "Generar l'escèna per defaut", - autoInstallPackages: "Installar automaticament los paquets UPM", - setupUrp: "Configurar URP", - upmPackages: "Paquets UPM (separats per virgulas)", - sceneName: "Nom de l'escèna", - unityEditorPath: "Camin de l'editor Unity (facultatiu)", - timeout: "Delai maximal (segondas)", - resultJson: "Resultat (JSON)", - }, - actions: { - generate: "Generar lo projècte", - finalize: "Finalizar lo projècte", - openFolder: "Dobrir lo dorsièr de sortida", - addVersion: "Apondre una version Unity", - downloadZip: "Telecargar lo projècte finalizat (.zip)", - }, - dialogs: { - addVersion: { - title: "Apondre una version Unity", - versionId: "ID de version", - label: "Etiqueta (facultativa)", - }, - }, - }, - theme: { light: "Clar", dark: "Escur", system: "Sistèma" }, - languages: { - en: "Anglés", - es: "Espanhòl", - ca: "Catalan", - eu: "Basque", - oc: "Occitan", - uk: "Ucranien", - pt: "Portugués", - gl: "Galician", - fr: "Francés", - it: "Italian", - pl: "Polonés", - zh: "Chinés", - ar: "Arabe", - de: "Alemand", - hi: "Hindi", - bn: "Bengalí", - ur: "Urdu", - id: "Indonesien", - ja: "Japonés", - vi: "Vietnamian", - ko: "Corean", - tr: "Turc", - }, -} as const; +/** + * Occitan (oc) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "En linha", offline: "Fòra linha" }, + nav: { + settings: "Paramètres", + scenes: "Escènas", + code: "Còdi", + text: "Tèxt", + image: "Imatge", + sprites: "Sprites", + audio: "Àudio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Fisica", + unityProject: "Projècte Unity", + logs: "Logs", + }, + actions: { repository: "Depaus" }, + }, + common: { + loading: "Cargament…", + saving: "Enregistrament…", + save: "Enregistrar", + cancel: "Anullar", + close: "Tampar", + ok: "D'acòrd", + yes: "Òc", + no: "Non", + error: "Error", + warning: "Avertiment", + success: "Succès", + generate: "Generar", + result: "Resultat", + provider: "Provesidor", + model: "Modèl", + temperature: "Temperatura", + apiKey: "Clau API (substitucion facultativa)", + systemPrompt: "Substitucion del prompt sistèma", + advancedOptions: "Opcions avançadas", + examplePrompts: "Exemples de prompts", + manageModels: "Gerir los modèls", + selectProvider: "Causir un provesidor", + selectModel: "Causir un modèl", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Laissar vuenh per utilizar la clau globala", + createdFiles: "Fichièrs creats", + viewSteps: "Veire las etapas ({n})", + waitingForLogs: "En espèra dels jornals…", + errorsDetected: "Errors detectadas", + }, + settings: { + title: "Configuracion", + subtitle: "Gerir vòstres motors d'intelligéncia, modèls e prompts sistèma.", + tabs: { + general: "General", + providers: "Provesidors", + models: "Modèls", + prompts: "Prompts", + secrets: "Secrets", + }, + }, + general: { + title: "Preferéncias generalas", + sections: { + networkApi: "Ret e API", + preferredIntelligence: "Intelligéncia preferida", + appearance: "Aparéncia", + language: "Lenga", + }, + fields: { + backendUrl: "URL del backend", + backendUrlHint: "L'adreça de vòstre servici backend Unity Generator", + outputBasePath: "Camin de basa (sortida)", + outputBasePathHint: "Ont son enregistrats los fichièrs generats", + unityEditorPath: "Camin de l'editor Unity (facultatiu)", + unityEditorPathHint: "Camin complet cap a l'executable de l'editor Unity", + preferredLlm: "Provesidor de tèxt/logica per defaut", + preferredLlmModel: "Modèl de tèxt/logica per defaut", + preferredImage: "Provesidor de generacion d'imatges per defaut", + preferredImageModel: "Modèl de generacion d'imatges per defaut", + preferredAudio: "Provesidor de vòtz (TTS) per defaut", + preferredAudioModel: "Modèl de vòtz (TTS) per defaut", + preferredMusic: "Provesidor de musica per defaut", + preferredMusicModel: "Modèl de musica per defaut", + theme: "Tèma", + language: "Lenga de l'interfàcia", + }, + actions: { saveAll: "Enregistrar cambiaments" }, + status: { + saving: "Enregistrament…", + saved: "Preferéncias enregistradas amb succès.", + modelNotRegistered: 'Lo modèl "{model}" es pas enregistrat pel provesidor "{provider}".', + networkError: "Error de ret : impossible de contactar lo backend.", + }, + }, + code: { + title: "Còdi C# Unity", + subtitle: "Generar scripts C# Unity amb IA.", + fields: { prompt: "Prompt", maxTokens: "Tokens màxims" }, + actions: { generate: "Generar lo còdi" }, + activeProject: "Projècte actiu : {name}", + autoSave: "Enregistrar automaticament al projècte", + }, + text: { + title: "Generacion de tèxt", + subtitle: "Generar contengut de tèxt amb IA.", + fields: { prompt: "Prompt", length: "Longor" }, + actions: { generate: "Generar lo tèxt" }, + }, + image: { + title: "Generacion d'imatges", + subtitle: "Generar imatges amb IA.", + fields: { + prompt: "Prompt", + aspectRatio: "Ràpòrt d'aspècte", + quality: "Qualitat", + textureName: "Nom de la textura", + textureType: "Tipe de textura", + }, + actions: { generate: "Generar l'imatge", saveToUnity: "Enregistrar dins Unity" }, + }, + sprites: { + title: "Sprites 2D", + badge: "Optimizat per Pixel Art", + subtitle: "Generar sprites 2D per Unity.", + fields: { + prompt: "Prompt", + resolution: "Resolucion", + paletteSize: "Talha de la paleta", + autoCrop: "Retalhar automaticament las voras transparentas", + colors: "{n} colors", + }, + preview: "Zòna de previsualizar", + actions: { generate: "Generar los sprites" }, + }, + audio: { + title: "Generacion d'àudio", + subtitle: "Generar ressorsas àudio amb IA.", + generationType: "Tipe de generacion", + speechTts: "Vòtz (TTS)", + atmosphericMusic: "Musica d'atmosfèra", + fields: { + prompt: "Prompt", + musicDescription: "Descripcion musicala", + speechPrompt: "Prompt de vòtz", + voiceOptional: "Vòtz (facultativa)", + musicModel: "Modèl de musica", + stability: "Estabilitat", + audioClipName: "Nom del clip àudio", + audioFormat: "Format àudio", + }, + actions: { generate: "Generar l'àudio", saveToUnity: "Enregistrar dins Unity" }, + }, + scenes: { + title: "Creaire d'escènas", + subtitle: "Generar escènas Unity amb IA.", + fields: { prompt: "Descripcion de l'escèna" }, + actions: { generate: "Crear l'escèna" }, + mediaReady: "Mèdia prèst a importar", + mediaReadyText: "{type} \"{name}\" es prèst a èsser importat dins Unity. Revisatz lo prompt e clicatz \"Crear l'escèna\" per continuar.", + mediaTypeImage: "Imatge", + mediaTypeAudio: "Àudio", + }, + unityUi: { + title: "Unity UI", + subtitle: "Generar components UI Unity amb IA.", + fields: { + uiSystem: "Sistèma UI", + elementType: "Tipe d'element", + prompt: "Descripcion de l'element UI", + outputFormat: "Format de sortida", + anchorPreset: "Preset d'ancoratge", + colourTheme: "Tèma de color (facultatiu)", + includeAnimations: "Includir animacions", + }, + actions: { generate: "Generar l'UI" }, + }, + unityPhysics: { + title: "Unity Fisica", + subtitle: "Generar la configuracion de fisica Unity — Rigidbodies, colisionadors, gravitacion e mai.", + fields: { + physicsBackend: "Motor de fisica", + simulationMode: "Mòde de simulacion", + gravityPreset: "Preset de gravitacion", + prompt: "Descripcion de la fisica", + includeRigidbody: "Includir la configuracion Rigidbody", + includeColliders: "Includir la configuracion dels colisionadors", + includeLayers: "Includir las capas de fisica", + }, + actions: { generate: "Generar la configuracion de fisica" }, + quickActions: "Accions ràpidas", + }, + unityProject: { + title: "Projècte Unity", + subtitle: "Generar una estructura complèta de projècte Unity.", + engineSettings: "Paramètres del motor Unity", + fields: { + projectName: "Nom del projècte", + unityTemplate: "Modèl Unity", + unityVersion: "Version Unity", + targetPlatform: "Plataforma cibla", + generateDefaultScene: "Generar l'escèna per defaut", + autoInstallPackages: "Installar automaticament los paquets UPM", + setupUrp: "Configurar URP", + upmPackages: "Paquets UPM (separats per virgulas)", + sceneName: "Nom de l'escèna", + unityEditorPath: "Camin de l'editor Unity (facultatiu)", + timeout: "Delai maximal (segondas)", + resultJson: "Resultat (JSON)", + }, + actions: { + generate: "Generar lo projècte", + finalize: "Finalizar lo projècte", + openFolder: "Dobrir lo dorsièr de sortida", + addVersion: "Apondre una version Unity", + downloadZip: "Telecargar lo projècte finalizat (.zip)", + }, + dialogs: { + addVersion: { + title: "Apondre una version Unity", + versionId: "ID de version", + label: "Etiqueta (facultativa)", + }, + }, + }, + theme: { light: "Clar", dark: "Escur", system: "Sistèma" }, + languages: { + en: "Anglés", + es: "Espanhòl", + ca: "Catalan", + eu: "Basque", + oc: "Occitan", + uk: "Ucranien", + pt: "Portugués", + gl: "Galician", + fr: "Francés", + it: "Italian", + pl: "Polonés", + zh: "Chinés", + ar: "Arabe", + de: "Alemand", + hi: "Hindi", + bn: "Bengalí", + ur: "Urdu", + id: "Indonesien", + ja: "Japonés", + vi: "Vietnamian", + ko: "Corean", + tr: "Turc", + }, +} as const; diff --git a/frontend/src/i18n/locales/pl.ts b/frontend/src/i18n/locales/pl.ts index d82a93d5..0723bdf3 100644 --- a/frontend/src/i18n/locales/pl.ts +++ b/frontend/src/i18n/locales/pl.ts @@ -1,246 +1,254 @@ -/** - * Polish (pl) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "Online", offline: "Offline" }, - nav: { - settings: "Ustawienia", - scenes: "Sceny", - code: "Kod", - text: "Tekst", - image: "Obraz", - sprites: "Sprites", - audio: "Audio", - unityUi: "Unity UI", - unityPhysics: "Unity Fizyka", - unityProject: "Projekt Unity", - }, - actions: { repository: "Repozytorium" }, - }, - common: { - loading: "Ładowanie…", - saving: "Zapisywanie…", - save: "Zapisz", - cancel: "Anuluj", - close: "Zamknij", - ok: "OK", - yes: "Tak", - no: "Nie", - error: "Błąd", - warning: "Ostrzeżenie", - success: "Sukces", - generate: "Generuj", - result: "Wynik", - provider: "Dostawca", - model: "Model", - temperature: "Temperatura", - apiKey: "Klucz API (opcjonalne nadpisanie)", - systemPrompt: "Nadpisanie promptu systemowego", - advancedOptions: "Opcje zaawansowane", - examplePrompts: "Przykładowe prompty", - manageModels: "Zarządzaj modelami", - selectProvider: "Wybierz dostawcę", - selectModel: "Wybierz model", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Pozostaw puste, aby użyć klucza globalnego", - createdFiles: "Utworzone pliki", - viewSteps: "Zobacz kroki ({n})", - waitingForLogs: "Oczekiwanie na logi…", - errorsDetected: "Wykryto błędy", - }, - settings: { - title: "Konfiguracja", - subtitle: "Zarządzaj silnikami inteligencji, modelami i promptami systemowymi.", - tabs: { - general: "Ogólne", - providers: "Dostawcy", - models: "Modele", - prompts: "Prompty", - secrets: "Sekrety", - }, - }, - general: { - title: "Ogólne preferencje", - sections: { - networkApi: "Sieć i API", - preferredIntelligence: "Preferowana inteligencja", - appearance: "Wygląd", - language: "Język", - }, - fields: { - backendUrl: "URL backendu", - backendUrlHint: "Adres usługi backend Unity Generator", - outputBasePath: "Ścieżka bazowa (wyjście)", - outputBasePathHint: "Gdzie zapisywane są wygenerowane pliki", - unityEditorPath: "Ścieżka edytora Unity (opcjonalne)", - unityEditorPathHint: "Pełna ścieżka do pliku wykonywalnego edytora Unity", - preferredLlm: "Domyślny dostawca tekstu/logiki", - preferredLlmModel: "Domyślny model tekstu/logiki", - preferredImage: "Domyślny dostawca generowania obrazów", - preferredImageModel: "Domyślny model generowania obrazów", - preferredAudio: "Domyślny dostawca głosu (TTS)", - preferredAudioModel: "Domyślny model głosu (TTS)", - preferredMusic: "Domyślny dostawca muzyki", - preferredMusicModel: "Domyślny model muzyki", - theme: "Motyw", - language: "Język interfejsu", - }, - actions: { saveAll: "Zapisz wszystkie zmiany" }, - status: { - saving: "Zapisywanie…", - saved: "Preferencje zapisane pomyślnie.", - modelNotRegistered: 'Model "{model}" nie jest zarejestrowany dla dostawcy "{provider}".', - networkError: "Błąd sieci: nie można połączyć się z backendem.", - }, - }, - code: { - title: "Kod C# Unity", - subtitle: "Generuj skrypty C# Unity za pomocą AI.", - fields: { prompt: "Prompt", maxTokens: "Maksymalna liczba tokenów" }, - actions: { generate: "Generuj kod" }, - activeProject: "Aktywny projekt: {name}", - autoSave: "Automatycznie zapisuj do projektu", - }, - text: { - title: "Generowanie tekstu", - subtitle: "Generuj treść tekstową za pomocą AI.", - fields: { prompt: "Prompt", length: "Długość" }, - actions: { generate: "Generuj tekst" }, - }, - image: { - title: "Generowanie obrazów", - subtitle: "Generuj obrazy za pomocą AI.", - fields: { - prompt: "Prompt", - aspectRatio: "Proporcje", - quality: "Jakość", - textureName: "Nazwa tekstury", - textureType: "Typ tekstury", - }, - actions: { generate: "Generuj obraz", saveToUnity: "Zapisz w Unity" }, - }, - sprites: { - title: "Sprites 2D", - badge: "Zoptymalizowane dla Pixel Art", - subtitle: "Generuj sprite'y 2D dla Unity.", - fields: { - prompt: "Prompt", - resolution: "Rozdzielczość", - paletteSize: "Rozmiar palety", - autoCrop: "Automatyczne przycinanie przezroczystych krawędzi", - colors: "{n} kolorów", - }, - preview: "Obszar podglądu", - actions: { generate: "Generuj sprites" }, - }, - audio: { - title: "Generowanie audio", - subtitle: "Generuj zasoby audio za pomocą AI.", - generationType: "Typ generowania", - speechTts: "Głos (TTS)", - atmosphericMusic: "Muzyka atmosferyczna", - fields: { - prompt: "Prompt", - musicDescription: "Opis muzyczny", - speechPrompt: "Prompt głosowy", - voiceOptional: "Głos (opcjonalne)", - musicModel: "Model muzyki", - stability: "Stabilność", - audioClipName: "Nazwa klipu audio", - audioFormat: "Format audio", - }, - actions: { generate: "Generuj audio", saveToUnity: "Zapisz w Unity" }, - }, - scenes: { - title: "Kreator scen", - subtitle: "Generuj sceny Unity za pomocą AI.", - fields: { prompt: "Opis sceny" }, - actions: { generate: "Utwórz scenę" }, - mediaReady: "Media gotowe do importu", - mediaReadyText: "{type} \"{name}\" jest gotowy do zaimportowania do Unity. Przejrzyj prompt i kliknij \"Utwórz scenę\", aby kontynuować.", - mediaTypeImage: "Obraz", - mediaTypeAudio: "Audio", - }, - unityUi: { - title: "Unity UI", - subtitle: "Generuj komponenty UI Unity za pomocą AI.", - fields: { - uiSystem: "System UI", - elementType: "Typ elementu", - prompt: "Opis elementu UI", - outputFormat: "Format wyjściowy", - anchorPreset: "Preset kotwicy", - colourTheme: "Motyw kolorystyczny (opcjonalne)", - includeAnimations: "Dołącz animacje", - }, - actions: { generate: "Generuj UI" }, - }, - unityPhysics: { - title: "Unity Fizyka", - subtitle: "Generuj konfigurację fizyki Unity — Rigidbodies, kolizory, grawitacja i więcej.", - fields: { - physicsBackend: "Silnik fizyki", - simulationMode: "Tryb symulacji", - gravityPreset: "Preset grawitacji", - prompt: "Opis fizyki", - includeRigidbody: "Dołącz konfigurację Rigidbody", - includeColliders: "Dołącz konfigurację kolizorów", - includeLayers: "Dołącz warstwy fizyki", - }, - actions: { generate: "Generuj konfigurację fizyki" }, - quickActions: "Szybkie akcje", - }, - unityProject: { - title: "Projekt Unity", - subtitle: "Generuj kompletną strukturę projektu Unity.", - engineSettings: "Ustawienia silnika Unity", - fields: { - projectName: "Nazwa projektu", - unityTemplate: "Szablon Unity", - unityVersion: "Wersja Unity", - targetPlatform: "Platforma docelowa", - generateDefaultScene: "Generuj domyślną scenę", - autoInstallPackages: "Automatycznie instaluj pakiety UPM", - setupUrp: "Skonfiguruj URP", - upmPackages: "Pakiety UPM (oddzielone przecinkami)", - sceneName: "Nazwa sceny", - unityEditorPath: "Ścieżka edytora Unity (opcjonalne)", - timeout: "Limit czasu (sekundy)", - resultJson: "Wynik (JSON)", - }, - actions: { - generate: "Generuj projekt", - finalize: "Finalizuj projekt", - openFolder: "Otwórz folder wyjściowy", - addVersion: "Dodaj wersję Unity", - downloadZip: "Pobierz sfinalizowany projekt (.zip)", - }, - dialogs: { - addVersion: { - title: "Dodaj wersję Unity", - versionId: "ID wersji", - label: "Etykieta (opcjonalne)", - }, - }, - }, - theme: { light: "Jasny", dark: "Ciemny", system: "Systemowy" }, - languages: { - en: "Angielski", - es: "Hiszpański", - ca: "Kataloński", - eu: "Baskijski", - oc: "Oksytański", - uk: "Ukraiński", - pt: "Portugalski", - gl: "Galicyjski", - fr: "Francuski", - it: "Włoski", - pl: "Polski", - zh: "Chiński", - ar: "Arabski", - de: "Niemiecki", - tr: "turecki", - }, -} as const; +/** + * Polish (pl) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "Online", offline: "Offline" }, + nav: { + settings: "Ustawienia", + scenes: "Sceny", + code: "Kod", + text: "Tekst", + image: "Obraz", + sprites: "Sprites", + audio: "Audio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Fizyka", + unityProject: "Projekt Unity", + logs: "Logs", + }, + actions: { repository: "Repozytorium" }, + }, + common: { + loading: "Ładowanie…", + saving: "Zapisywanie…", + save: "Zapisz", + cancel: "Anuluj", + close: "Zamknij", + ok: "OK", + yes: "Tak", + no: "Nie", + error: "Błąd", + warning: "Ostrzeżenie", + success: "Sukces", + generate: "Generuj", + result: "Wynik", + provider: "Dostawca", + model: "Model", + temperature: "Temperatura", + apiKey: "Klucz API (opcjonalne nadpisanie)", + systemPrompt: "Nadpisanie promptu systemowego", + advancedOptions: "Opcje zaawansowane", + examplePrompts: "Przykładowe prompty", + manageModels: "Zarządzaj modelami", + selectProvider: "Wybierz dostawcę", + selectModel: "Wybierz model", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Pozostaw puste, aby użyć klucza globalnego", + createdFiles: "Utworzone pliki", + viewSteps: "Zobacz kroki ({n})", + waitingForLogs: "Oczekiwanie na logi…", + errorsDetected: "Wykryto błędy", + }, + settings: { + title: "Konfiguracja", + subtitle: "Zarządzaj silnikami inteligencji, modelami i promptami systemowymi.", + tabs: { + general: "Ogólne", + providers: "Dostawcy", + models: "Modele", + prompts: "Prompty", + secrets: "Sekrety", + }, + }, + general: { + title: "Ogólne preferencje", + sections: { + networkApi: "Sieć i API", + preferredIntelligence: "Preferowana inteligencja", + appearance: "Wygląd", + language: "Język", + }, + fields: { + backendUrl: "URL backendu", + backendUrlHint: "Adres usługi backend Unity Generator", + outputBasePath: "Ścieżka bazowa (wyjście)", + outputBasePathHint: "Gdzie zapisywane są wygenerowane pliki", + unityEditorPath: "Ścieżka edytora Unity (opcjonalne)", + unityEditorPathHint: "Pełna ścieżka do pliku wykonywalnego edytora Unity", + preferredLlm: "Domyślny dostawca tekstu/logiki", + preferredLlmModel: "Domyślny model tekstu/logiki", + preferredImage: "Domyślny dostawca generowania obrazów", + preferredImageModel: "Domyślny model generowania obrazów", + preferredAudio: "Domyślny dostawca głosu (TTS)", + preferredAudioModel: "Domyślny model głosu (TTS)", + preferredMusic: "Domyślny dostawca muzyki", + preferredMusicModel: "Domyślny model muzyki", + theme: "Motyw", + language: "Język interfejsu", + }, + actions: { saveAll: "Zapisz wszystkie zmiany" }, + status: { + saving: "Zapisywanie…", + saved: "Preferencje zapisane pomyślnie.", + modelNotRegistered: 'Model "{model}" nie jest zarejestrowany dla dostawcy "{provider}".', + networkError: "Błąd sieci: nie można połączyć się z backendem.", + }, + }, + code: { + title: "Kod C# Unity", + subtitle: "Generuj skrypty C# Unity za pomocą AI.", + fields: { prompt: "Prompt", maxTokens: "Maksymalna liczba tokenów" }, + actions: { generate: "Generuj kod" }, + activeProject: "Aktywny projekt: {name}", + autoSave: "Automatycznie zapisuj do projektu", + }, + text: { + title: "Generowanie tekstu", + subtitle: "Generuj treść tekstową za pomocą AI.", + fields: { prompt: "Prompt", length: "Długość" }, + actions: { generate: "Generuj tekst" }, + }, + image: { + title: "Generowanie obrazów", + subtitle: "Generuj obrazy za pomocą AI.", + fields: { + prompt: "Prompt", + aspectRatio: "Proporcje", + quality: "Jakość", + textureName: "Nazwa tekstury", + textureType: "Typ tekstury", + }, + actions: { generate: "Generuj obraz", saveToUnity: "Zapisz w Unity" }, + }, + sprites: { + title: "Sprites 2D", + badge: "Zoptymalizowane dla Pixel Art", + subtitle: "Generuj sprite'y 2D dla Unity.", + fields: { + prompt: "Prompt", + resolution: "Rozdzielczość", + paletteSize: "Rozmiar palety", + autoCrop: "Automatyczne przycinanie przezroczystych krawędzi", + colors: "{n} kolorów", + }, + preview: "Obszar podglądu", + actions: { generate: "Generuj sprites" }, + }, + audio: { + title: "Generowanie audio", + subtitle: "Generuj zasoby audio za pomocą AI.", + generationType: "Typ generowania", + speechTts: "Głos (TTS)", + atmosphericMusic: "Muzyka atmosferyczna", + fields: { + prompt: "Prompt", + musicDescription: "Opis muzyczny", + speechPrompt: "Prompt głosowy", + voiceOptional: "Głos (opcjonalne)", + musicModel: "Model muzyki", + stability: "Stabilność", + audioClipName: "Nazwa klipu audio", + audioFormat: "Format audio", + }, + actions: { generate: "Generuj audio", saveToUnity: "Zapisz w Unity" }, + }, + scenes: { + title: "Kreator scen", + subtitle: "Generuj sceny Unity za pomocą AI.", + fields: { prompt: "Opis sceny" }, + actions: { generate: "Utwórz scenę" }, + mediaReady: "Media gotowe do importu", + mediaReadyText: "{type} \"{name}\" jest gotowy do zaimportowania do Unity. Przejrzyj prompt i kliknij \"Utwórz scenę\", aby kontynuować.", + mediaTypeImage: "Obraz", + mediaTypeAudio: "Audio", + }, + unityUi: { + title: "Unity UI", + subtitle: "Generuj komponenty UI Unity za pomocą AI.", + fields: { + uiSystem: "System UI", + elementType: "Typ elementu", + prompt: "Opis elementu UI", + outputFormat: "Format wyjściowy", + anchorPreset: "Preset kotwicy", + colourTheme: "Motyw kolorystyczny (opcjonalne)", + includeAnimations: "Dołącz animacje", + }, + actions: { generate: "Generuj UI" }, + }, + unityPhysics: { + title: "Unity Fizyka", + subtitle: "Generuj konfigurację fizyki Unity — Rigidbodies, kolizory, grawitacja i więcej.", + fields: { + physicsBackend: "Silnik fizyki", + simulationMode: "Tryb symulacji", + gravityPreset: "Preset grawitacji", + prompt: "Opis fizyki", + includeRigidbody: "Dołącz konfigurację Rigidbody", + includeColliders: "Dołącz konfigurację kolizorów", + includeLayers: "Dołącz warstwy fizyki", + }, + actions: { generate: "Generuj konfigurację fizyki" }, + quickActions: "Szybkie akcje", + }, + unityProject: { + title: "Projekt Unity", + subtitle: "Generuj kompletną strukturę projektu Unity.", + engineSettings: "Ustawienia silnika Unity", + fields: { + projectName: "Nazwa projektu", + unityTemplate: "Szablon Unity", + unityVersion: "Wersja Unity", + targetPlatform: "Platforma docelowa", + generateDefaultScene: "Generuj domyślną scenę", + autoInstallPackages: "Automatycznie instaluj pakiety UPM", + setupUrp: "Skonfiguruj URP", + upmPackages: "Pakiety UPM (oddzielone przecinkami)", + sceneName: "Nazwa sceny", + unityEditorPath: "Ścieżka edytora Unity (opcjonalne)", + timeout: "Limit czasu (sekundy)", + resultJson: "Wynik (JSON)", + }, + actions: { + generate: "Generuj projekt", + finalize: "Finalizuj projekt", + openFolder: "Otwórz folder wyjściowy", + addVersion: "Dodaj wersję Unity", + downloadZip: "Pobierz sfinalizowany projekt (.zip)", + }, + dialogs: { + addVersion: { + title: "Dodaj wersję Unity", + versionId: "ID wersji", + label: "Etykieta (opcjonalne)", + }, + }, + }, + theme: { light: "Jasny", dark: "Ciemny", system: "Systemowy" }, + languages: { + en: "Angielski", + es: "Hiszpański", + ca: "Kataloński", + eu: "Baskijski", + oc: "Oksytański", + uk: "Ukraiński", + pt: "Portugalski", + gl: "Galicyjski", + fr: "Francuski", + it: "Włoski", + pl: "Polski", + zh: "Chiński", + ar: "Arabski", + de: "Niemiecki", + tr: "turecki", + }, +} as const; diff --git a/frontend/src/i18n/locales/pt.ts b/frontend/src/i18n/locales/pt.ts index a84d1fe0..6c54d1b1 100644 --- a/frontend/src/i18n/locales/pt.ts +++ b/frontend/src/i18n/locales/pt.ts @@ -1,246 +1,254 @@ -/** - * Portuguese (pt) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "Online", offline: "Offline" }, - nav: { - settings: "Definições", - scenes: "Cenas", - code: "Código", - text: "Texto", - image: "Imagem", - sprites: "Sprites", - audio: "Áudio", - unityUi: "Unity UI", - unityPhysics: "Unity Física", - unityProject: "Projeto Unity", - }, - actions: { repository: "Repositório" }, - }, - common: { - loading: "A carregar…", - saving: "A guardar…", - save: "Guardar", - cancel: "Cancelar", - close: "Fechar", - ok: "OK", - yes: "Sim", - no: "Não", - error: "Erro", - warning: "Aviso", - success: "Sucesso", - generate: "Gerar", - result: "Resultado", - provider: "Fornecedor", - model: "Modelo", - temperature: "Temperatura", - apiKey: "Chave API (substituição opcional)", - systemPrompt: "Substituição do prompt de sistema", - advancedOptions: "Opções avançadas", - examplePrompts: "Exemplos de prompts", - manageModels: "Gerir modelos", - selectProvider: "Selecionar fornecedor", - selectModel: "Selecionar modelo", - prompt: "Prompt", - leaveEmptyForGlobalKey: "Deixar vazio para usar a chave global", - createdFiles: "Ficheiros criados", - viewSteps: "Ver passos ({n})", - waitingForLogs: "À espera de registos…", - errorsDetected: "Erros detetados", - }, - settings: { - title: "Configuração", - subtitle: "Gere os seus motores de inteligência, modelos e prompts de sistema.", - tabs: { - general: "Geral", - providers: "Fornecedores", - models: "Modelos", - prompts: "Prompts", - secrets: "Segredos", - }, - }, - general: { - title: "Preferências gerais", - sections: { - networkApi: "Rede e API", - preferredIntelligence: "Inteligência preferida", - appearance: "Aparência", - language: "Idioma", - }, - fields: { - backendUrl: "URL do backend", - backendUrlHint: "O endereço do seu serviço backend Unity Generator", - outputBasePath: "Caminho base (saída)", - outputBasePathHint: "Onde os ficheiros gerados são guardados", - unityEditorPath: "Caminho do editor Unity (opcional)", - unityEditorPathHint: "Caminho completo para o executável do editor Unity", - preferredLlm: "Fornecedor de texto/lógica predefinido", - preferredLlmModel: "Modelo de texto/lógica predefinido", - preferredImage: "Fornecedor de geração de imagens predefinido", - preferredImageModel: "Modelo de geração de imagens predefinido", - preferredAudio: "Fornecedor de voz (TTS) predefinido", - preferredAudioModel: "Modelo de voz (TTS) predefinido", - preferredMusic: "Fornecedor de música predefinido", - preferredMusicModel: "Modelo de música predefinido", - theme: "Tema", - language: "Idioma da interface", - }, - actions: { saveAll: "Guardar todas as alterações" }, - status: { - saving: "A guardar…", - saved: "Preferências guardadas com sucesso.", - modelNotRegistered: 'O modelo "{model}" não está registado para o fornecedor "{provider}".', - networkError: "Erro de rede: não foi possível contactar o backend.", - }, - }, - code: { - title: "Código C# Unity", - subtitle: "Gere scripts C# para Unity com IA.", - fields: { prompt: "Prompt", maxTokens: "Tokens máximos" }, - actions: { generate: "Gerar código" }, - activeProject: "Projeto ativo: {name}", - autoSave: "Guardar automaticamente no projeto", - }, - text: { - title: "Geração de texto", - subtitle: "Gere conteúdo de texto com IA.", - fields: { prompt: "Prompt", length: "Comprimento" }, - actions: { generate: "Gerar texto" }, - }, - image: { - title: "Geração de imagens", - subtitle: "Gere imagens com IA.", - fields: { - prompt: "Prompt", - aspectRatio: "Proporção", - quality: "Qualidade", - textureName: "Nome da textura", - textureType: "Tipo de textura", - }, - actions: { generate: "Gerar imagem", saveToUnity: "Guardar no Unity" }, - }, - sprites: { - title: "Sprites 2D", - badge: "Otimizado para Pixel Art", - subtitle: "Gere sprites 2D para Unity.", - fields: { - prompt: "Prompt", - resolution: "Resolução", - paletteSize: "Tamanho da paleta", - autoCrop: "Corte automático de bordas transparentes", - colors: "{n} cores", - }, - preview: "Área de pré-visualização", - actions: { generate: "Gerar sprites" }, - }, - audio: { - title: "Geração de áudio", - subtitle: "Gere recursos de áudio com IA.", - generationType: "Tipo de geração", - speechTts: "Voz (TTS)", - atmosphericMusic: "Música ambiente", - fields: { - prompt: "Prompt", - musicDescription: "Descrição musical", - speechPrompt: "Prompt de voz", - voiceOptional: "Voz (opcional)", - musicModel: "Modelo de música", - stability: "Estabilidade", - audioClipName: "Nome do clip de áudio", - audioFormat: "Formato de áudio", - }, - actions: { generate: "Gerar áudio", saveToUnity: "Guardar no Unity" }, - }, - scenes: { - title: "Criador de cenas", - subtitle: "Gere cenas Unity com IA.", - fields: { prompt: "Descrição da cena" }, - actions: { generate: "Criar cena" }, - mediaReady: "Média pronta para importar", - mediaReadyText: "{type} \"{name}\" está pronta para ser importada no Unity. Reveja o prompt e clique em \"Criar cena\" para continuar.", - mediaTypeImage: "Imagem", - mediaTypeAudio: "Áudio", - }, - unityUi: { - title: "Unity UI", - subtitle: "Gere componentes de UI para Unity com IA.", - fields: { - uiSystem: "Sistema de UI", - elementType: "Tipo de elemento", - prompt: "Descrição do elemento UI", - outputFormat: "Formato de saída", - anchorPreset: "Preset de âncora", - colourTheme: "Tema de cor (opcional)", - includeAnimations: "Incluir animações", - }, - actions: { generate: "Gerar UI" }, - }, - unityPhysics: { - title: "Unity Física", - subtitle: "Gere configuração de física Unity — Rigidbodies, colisores, gravidade e mais.", - fields: { - physicsBackend: "Motor de física", - simulationMode: "Modo de simulação", - gravityPreset: "Preset de gravidade", - prompt: "Descrição da física", - includeRigidbody: "Incluir configuração Rigidbody", - includeColliders: "Incluir configuração de colisores", - includeLayers: "Incluir camadas de física", - }, - actions: { generate: "Gerar configuração de física" }, - quickActions: "Ações rápidas", - }, - unityProject: { - title: "Projeto Unity", - subtitle: "Gere uma estrutura completa de projeto Unity.", - engineSettings: "Definições do motor Unity", - fields: { - projectName: "Nome do projeto", - unityTemplate: "Template Unity", - unityVersion: "Versão Unity", - targetPlatform: "Plataforma alvo", - generateDefaultScene: "Gerar cena predefinida", - autoInstallPackages: "Instalar pacotes UPM automaticamente", - setupUrp: "Configurar URP", - upmPackages: "Pacotes UPM (separados por vírgulas)", - sceneName: "Nome da cena", - unityEditorPath: "Caminho do editor Unity (opcional)", - timeout: "Tempo limite (segundos)", - resultJson: "Resultado (JSON)", - }, - actions: { - generate: "Gerar projeto", - finalize: "Finalizar projeto", - openFolder: "Abrir pasta de saída", - addVersion: "Adicionar versão Unity", - downloadZip: "Descarregar projeto finalizado (.zip)", - }, - dialogs: { - addVersion: { - title: "Adicionar versão Unity", - versionId: "ID de versão", - label: "Etiqueta (opcional)", - }, - }, - }, - theme: { light: "Claro", dark: "Escuro", system: "Sistema" }, - languages: { - en: "Inglês", - es: "Espanhol", - ca: "Catalão", - eu: "Basco", - oc: "Occitano", - uk: "Ucraniano", - pt: "Português", - gl: "Galego", - fr: "Francês", - it: "Italiano", - pl: "Polaco", - zh: "Chinês", - ar: "Árabe", - de: "Alemão", - tr: "Turco", - }, -} as const; +/** + * Portuguese (pt) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "Online", offline: "Offline" }, + nav: { + settings: "Definições", + scenes: "Cenas", + code: "Código", + text: "Texto", + image: "Imagem", + sprites: "Sprites", + audio: "Áudio", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Física", + unityProject: "Projeto Unity", + logs: "Logs", + }, + actions: { repository: "Repositório" }, + }, + common: { + loading: "A carregar…", + saving: "A guardar…", + save: "Guardar", + cancel: "Cancelar", + close: "Fechar", + ok: "OK", + yes: "Sim", + no: "Não", + error: "Erro", + warning: "Aviso", + success: "Sucesso", + generate: "Gerar", + result: "Resultado", + provider: "Fornecedor", + model: "Modelo", + temperature: "Temperatura", + apiKey: "Chave API (substituição opcional)", + systemPrompt: "Substituição do prompt de sistema", + advancedOptions: "Opções avançadas", + examplePrompts: "Exemplos de prompts", + manageModels: "Gerir modelos", + selectProvider: "Selecionar fornecedor", + selectModel: "Selecionar modelo", + prompt: "Prompt", + leaveEmptyForGlobalKey: "Deixar vazio para usar a chave global", + createdFiles: "Ficheiros criados", + viewSteps: "Ver passos ({n})", + waitingForLogs: "À espera de registos…", + errorsDetected: "Erros detetados", + }, + settings: { + title: "Configuração", + subtitle: "Gere os seus motores de inteligência, modelos e prompts de sistema.", + tabs: { + general: "Geral", + providers: "Fornecedores", + models: "Modelos", + prompts: "Prompts", + secrets: "Segredos", + }, + }, + general: { + title: "Preferências gerais", + sections: { + networkApi: "Rede e API", + preferredIntelligence: "Inteligência preferida", + appearance: "Aparência", + language: "Idioma", + }, + fields: { + backendUrl: "URL do backend", + backendUrlHint: "O endereço do seu serviço backend Unity Generator", + outputBasePath: "Caminho base (saída)", + outputBasePathHint: "Onde os ficheiros gerados são guardados", + unityEditorPath: "Caminho do editor Unity (opcional)", + unityEditorPathHint: "Caminho completo para o executável do editor Unity", + preferredLlm: "Fornecedor de texto/lógica predefinido", + preferredLlmModel: "Modelo de texto/lógica predefinido", + preferredImage: "Fornecedor de geração de imagens predefinido", + preferredImageModel: "Modelo de geração de imagens predefinido", + preferredAudio: "Fornecedor de voz (TTS) predefinido", + preferredAudioModel: "Modelo de voz (TTS) predefinido", + preferredMusic: "Fornecedor de música predefinido", + preferredMusicModel: "Modelo de música predefinido", + theme: "Tema", + language: "Idioma da interface", + }, + actions: { saveAll: "Guardar todas as alterações" }, + status: { + saving: "A guardar…", + saved: "Preferências guardadas com sucesso.", + modelNotRegistered: 'O modelo "{model}" não está registado para o fornecedor "{provider}".', + networkError: "Erro de rede: não foi possível contactar o backend.", + }, + }, + code: { + title: "Código C# Unity", + subtitle: "Gere scripts C# para Unity com IA.", + fields: { prompt: "Prompt", maxTokens: "Tokens máximos" }, + actions: { generate: "Gerar código" }, + activeProject: "Projeto ativo: {name}", + autoSave: "Guardar automaticamente no projeto", + }, + text: { + title: "Geração de texto", + subtitle: "Gere conteúdo de texto com IA.", + fields: { prompt: "Prompt", length: "Comprimento" }, + actions: { generate: "Gerar texto" }, + }, + image: { + title: "Geração de imagens", + subtitle: "Gere imagens com IA.", + fields: { + prompt: "Prompt", + aspectRatio: "Proporção", + quality: "Qualidade", + textureName: "Nome da textura", + textureType: "Tipo de textura", + }, + actions: { generate: "Gerar imagem", saveToUnity: "Guardar no Unity" }, + }, + sprites: { + title: "Sprites 2D", + badge: "Otimizado para Pixel Art", + subtitle: "Gere sprites 2D para Unity.", + fields: { + prompt: "Prompt", + resolution: "Resolução", + paletteSize: "Tamanho da paleta", + autoCrop: "Corte automático de bordas transparentes", + colors: "{n} cores", + }, + preview: "Área de pré-visualização", + actions: { generate: "Gerar sprites" }, + }, + audio: { + title: "Geração de áudio", + subtitle: "Gere recursos de áudio com IA.", + generationType: "Tipo de geração", + speechTts: "Voz (TTS)", + atmosphericMusic: "Música ambiente", + fields: { + prompt: "Prompt", + musicDescription: "Descrição musical", + speechPrompt: "Prompt de voz", + voiceOptional: "Voz (opcional)", + musicModel: "Modelo de música", + stability: "Estabilidade", + audioClipName: "Nome do clip de áudio", + audioFormat: "Formato de áudio", + }, + actions: { generate: "Gerar áudio", saveToUnity: "Guardar no Unity" }, + }, + scenes: { + title: "Criador de cenas", + subtitle: "Gere cenas Unity com IA.", + fields: { prompt: "Descrição da cena" }, + actions: { generate: "Criar cena" }, + mediaReady: "Média pronta para importar", + mediaReadyText: "{type} \"{name}\" está pronta para ser importada no Unity. Reveja o prompt e clique em \"Criar cena\" para continuar.", + mediaTypeImage: "Imagem", + mediaTypeAudio: "Áudio", + }, + unityUi: { + title: "Unity UI", + subtitle: "Gere componentes de UI para Unity com IA.", + fields: { + uiSystem: "Sistema de UI", + elementType: "Tipo de elemento", + prompt: "Descrição do elemento UI", + outputFormat: "Formato de saída", + anchorPreset: "Preset de âncora", + colourTheme: "Tema de cor (opcional)", + includeAnimations: "Incluir animações", + }, + actions: { generate: "Gerar UI" }, + }, + unityPhysics: { + title: "Unity Física", + subtitle: "Gere configuração de física Unity — Rigidbodies, colisores, gravidade e mais.", + fields: { + physicsBackend: "Motor de física", + simulationMode: "Modo de simulação", + gravityPreset: "Preset de gravidade", + prompt: "Descrição da física", + includeRigidbody: "Incluir configuração Rigidbody", + includeColliders: "Incluir configuração de colisores", + includeLayers: "Incluir camadas de física", + }, + actions: { generate: "Gerar configuração de física" }, + quickActions: "Ações rápidas", + }, + unityProject: { + title: "Projeto Unity", + subtitle: "Gere uma estrutura completa de projeto Unity.", + engineSettings: "Definições do motor Unity", + fields: { + projectName: "Nome do projeto", + unityTemplate: "Template Unity", + unityVersion: "Versão Unity", + targetPlatform: "Plataforma alvo", + generateDefaultScene: "Gerar cena predefinida", + autoInstallPackages: "Instalar pacotes UPM automaticamente", + setupUrp: "Configurar URP", + upmPackages: "Pacotes UPM (separados por vírgulas)", + sceneName: "Nome da cena", + unityEditorPath: "Caminho do editor Unity (opcional)", + timeout: "Tempo limite (segundos)", + resultJson: "Resultado (JSON)", + }, + actions: { + generate: "Gerar projeto", + finalize: "Finalizar projeto", + openFolder: "Abrir pasta de saída", + addVersion: "Adicionar versão Unity", + downloadZip: "Descarregar projeto finalizado (.zip)", + }, + dialogs: { + addVersion: { + title: "Adicionar versão Unity", + versionId: "ID de versão", + label: "Etiqueta (opcional)", + }, + }, + }, + theme: { light: "Claro", dark: "Escuro", system: "Sistema" }, + languages: { + en: "Inglês", + es: "Espanhol", + ca: "Catalão", + eu: "Basco", + oc: "Occitano", + uk: "Ucraniano", + pt: "Português", + gl: "Galego", + fr: "Francês", + it: "Italiano", + pl: "Polaco", + zh: "Chinês", + ar: "Árabe", + de: "Alemão", + tr: "Turco", + }, +} as const; diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts index afa68fee..fe4de89e 100644 --- a/frontend/src/i18n/locales/tr.ts +++ b/frontend/src/i18n/locales/tr.ts @@ -1,258 +1,266 @@ -/** - * Turkish (tr) locale. - * Structure mirrors `en.ts` exactly. - */ -export default { - app: { - title: "Unity Generator", - status: { - online: "Çevrimiçi", - offline: "Çevrimdışı", - }, - nav: { - settings: "Ayarlar", - scenes: "Sahneler", - code: "Kod", - text: "Metin", - image: "Görsel", - sprites: "Sprite'lar", - audio: "Ses", - unityUi: "Unity UI", - unityPhysics: "Unity Fizik", - unityProject: "Unity Projesi", - }, - actions: { repository: "Depo" }, - }, - common: { - loading: "Yükleniyor…", - saving: "Kaydediliyor…", - save: "Kaydet", - cancel: "İptal", - close: "Kapat", - ok: "Tamam", - yes: "Evet", - no: "Hayır", - error: "Hata", - warning: "Uyarı", - success: "Başarılı", - generate: "Oluştur", - result: "Sonuç", - provider: "Sağlayıcı", - model: "Model", - temperature: "Sıcaklık", - apiKey: "API Anahtarı (isteğe bağlı)", - systemPrompt: "Sistem istemi geçersiz kılma", - advancedOptions: "Gelişmiş seçenekler", - examplePrompts: "Örnek istemler", - manageModels: "Modelleri yönet", - selectProvider: "Sağlayıcı seç", - selectModel: "Model seç", - prompt: "İstem", - leaveEmptyForGlobalKey: "Genel anahtarı kullanmak için boş bırakın", - createdFiles: "Oluşturulan dosyalar", - viewSteps: "Adımları görüntüle ({n})", - waitingForLogs: "Günlükler bekleniyor…", - errorsDetected: "Hatalar algılandı", - }, - settings: { - title: "Yapılandırma", - subtitle: "Yapay zeka motorlarınızı, modellerinizi ve sistem istemlerinizi yönetin.", - tabs: { - general: "Genel", - providers: "Sağlayıcılar", - models: "Modeller", - prompts: "İstemler", - secrets: "Gizli bilgiler", - }, - }, - general: { - title: "Genel tercihler", - sections: { - networkApi: "Ağ ve API", - preferredIntelligence: "Tercih edilen yapay zeka", - appearance: "Görünüm", - language: "Dil", - }, - fields: { - backendUrl: "Backend URL", - backendUrlHint: "Unity Generator backend hizmetinizin adresi", - outputBasePath: "Temel yol (çıktı)", - outputBasePathHint: "Oluşturulan dosyaların kaydedildiği yer", - unityEditorPath: "Unity Editor yolu (isteğe bağlı)", - unityEditorPathHint: "Unity Editor yürütülebilir dosyasının tam yolu", - preferredLlm: "Varsayılan metin / mantık sağlayıcısı", - preferredLlmModel: "Varsayılan metin / mantık modeli", - preferredImage: "Varsayılan görsel oluşturma sağlayıcısı", - preferredImageModel: "Varsayılan görsel oluşturma modeli", - preferredAudio: "Varsayılan konuşma (TTS) sağlayıcısı", - preferredAudioModel: "Varsayılan konuşma (TTS) modeli", - preferredMusic: "Varsayılan müzik oluşturma sağlayıcısı", - preferredMusicModel: "Varsayılan müzik oluşturma modeli", - theme: "Tema", - language: "Arayüz dili", - }, - actions: { saveAll: "Tüm değişiklikleri kaydet" }, - status: { - saving: "Kaydediliyor…", - saved: "Tercihler başarıyla kaydedildi.", - modelNotRegistered: '"{provider}" sağlayıcısı için "{model}" modeli kayıtlı değil.', - networkError: "Ağ hatası: Backend'e ulaşılamadı.", - }, - }, - code: { - title: "Unity C# Kodu", - subtitle: "Yapay zeka ile Unity C# betikleri oluşturun.", - fields: { prompt: "İstem", maxTokens: "Maksimum token" }, - actions: { generate: "Kod oluştur" }, - activeProject: "Aktif proje: {name}", - autoSave: "Projeye otomatik kaydet", - }, - text: { - title: "Metin oluşturma", - subtitle: "Yapay zeka ile metin içeriği oluşturun.", - fields: { prompt: "İstem", length: "Uzunluk" }, - actions: { generate: "Metin oluştur" }, - }, - image: { - title: "Görsel oluşturma", - subtitle: "Yapay zeka ile görseller oluşturun.", - fields: { - prompt: "İstem", - aspectRatio: "En-boy oranı", - quality: "Kalite", - textureName: "Doku adı", - textureType: "Doku türü", - }, - actions: { generate: "Görsel oluştur", saveToUnity: "Unity'ye kaydet" }, - }, - sprites: { - title: "2D Sprite'lar", - badge: "Piksel sanatına uygun", - subtitle: "Unity için 2D sprite varlıkları oluşturun.", - fields: { - prompt: "İstem", - resolution: "Çözünürlük", - paletteSize: "Palet boyutu", - autoCrop: "Şeffaf kenarları otomatik kırp", - colors: "{n} renk", - }, - preview: "Önizleme alanı", - actions: { generate: "Sprite oluştur" }, - }, - audio: { - title: "Ses oluşturma", - subtitle: "Yapay zeka ile ses varlıkları oluşturun.", - generationType: "Oluşturma türü", - speechTts: "Konuşma (TTS)", - atmosphericMusic: "Atmosferik müzik", - fields: { - prompt: "İstem", - musicDescription: "Müzik açıklaması", - speechPrompt: "Konuşma istemi", - voiceOptional: "Ses (isteğe bağlı)", - musicModel: "Müzik modeli", - stability: "Kararlılık", - audioClipName: "Ses klibi adı", - audioFormat: "Ses formatı", - }, - actions: { generate: "Ses oluştur", saveToUnity: "Unity'ye kaydet" }, - }, - scenes: { - title: "Sahne oluşturucu", - subtitle: "Yapay zeka ile Unity sahneleri oluşturun.", - fields: { prompt: "Sahne açıklaması" }, - actions: { generate: "Sahne oluştur" }, - mediaReady: "İçe aktarmaya hazır medya", - mediaReadyText: - '{type} "{name}" Unity\'ye aktarılmaya hazır. Aşağıdaki istemi inceleyin ve devam etmek için "Sahne oluştur"a tıklayın.', - mediaTypeImage: "Görsel", - mediaTypeAudio: "Ses", - }, - unityUi: { - title: "Unity UI", - subtitle: "Yapay zeka ile Unity UI bileşenleri oluşturun.", - fields: { - uiSystem: "UI sistemi", - elementType: "Öğe türü", - prompt: "UI öğesi açıklaması", - outputFormat: "Çıktı formatı", - anchorPreset: "Sabitleme ön ayarı", - colourTheme: "Renk teması (isteğe bağlı)", - includeAnimations: "Animasyonları dahil et", - }, - actions: { generate: "UI oluştur" }, - }, - unityPhysics: { - title: "Unity Fizik", - subtitle: "Unity fizik yapılandırması oluşturun — Rigidbody'ler, çarpışma birimleri, yerçekimi ve daha fazlası.", - fields: { - physicsBackend: "Fizik motoru", - simulationMode: "Simülasyon modu", - gravityPreset: "Yerçekimi ön ayarı", - prompt: "Fizik açıklaması", - includeRigidbody: "Rigidbody kurulumunu dahil et", - includeColliders: "Çarpışma birimi kurulumunu dahil et", - includeLayers: "Fizik katmanlarını dahil et", - }, - actions: { generate: "Fizik yapılandırması oluştur" }, - quickActions: "Hızlı işlemler", - }, - unityProject: { - title: "Unity Projesi", - subtitle: "Tam bir Unity proje yapısı oluşturun.", - engineSettings: "Unity motor ayarları", - fields: { - projectName: "Proje adı", - unityTemplate: "Unity şablonu", - unityVersion: "Unity sürümü", - targetPlatform: "Hedef platform", - generateDefaultScene: "Varsayılan sahne oluştur", - autoInstallPackages: "UPM paketlerini otomatik kur", - setupUrp: "URP kurulumu", - upmPackages: "UPM paketleri (virgülle ayrılmış)", - sceneName: "Sahne adı", - unityEditorPath: "Unity Editor yolu (isteğe bağlı)", - timeout: "Zaman aşımı (saniye)", - resultJson: "Sonuç (JSON)", - }, - actions: { - generate: "Proje oluştur", - finalize: "Projeyi sonlandır", - openFolder: "Çıktı klasörünü aç", - addVersion: "Unity sürümü ekle", - downloadZip: "Sonlandırılmış projeyi indir (.zip)", - }, - dialogs: { - addVersion: { - title: "Unity sürümü ekle", - versionId: "Sürüm kimliği", - label: "Etiket (isteğe bağlı)", - }, - }, - }, - theme: { light: "Açık", dark: "Koyu", system: "Sistem" }, - languages: { - en: "İngilizce", - es: "İspanyolca", - ca: "Katalanca", - eu: "Baskça", - oc: "Oksitanca", - uk: "Ukraynaca", - pt: "Portekizce", - gl: "Galiçyaca", - fr: "Fransızca", - it: "İtalyanca", - pl: "Lehçe", - zh: "Çince", - ar: "Arapça", - de: "Almanca", - hi: "Hintçe", - bn: "Bengalce", - ur: "Urduca", - id: "Endonezce", - ja: "Japonca", - vi: "Vietnamca", - ko: "Korece", - tr: "Türkçe", - }, -} as const; +/** + * Turkish (tr) locale. + * Structure mirrors `en.ts` exactly. + */ +export default { + app: { + title: "Unity Generator", + status: { + online: "Çevrimiçi", + offline: "Çevrimdışı", + }, + nav: { + settings: "Ayarlar", + scenes: "Sahneler", + code: "Kod", + text: "Metin", + image: "Görsel", + sprites: "Sprite'lar", + audio: "Ses", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Fizik", + unityProject: "Unity Projesi", + logs: "Logs", + }, + actions: { repository: "Depo" }, + }, + common: { + loading: "Yükleniyor…", + saving: "Kaydediliyor…", + save: "Kaydet", + cancel: "İptal", + close: "Kapat", + ok: "Tamam", + yes: "Evet", + no: "Hayır", + error: "Hata", + warning: "Uyarı", + success: "Başarılı", + generate: "Oluştur", + result: "Sonuç", + provider: "Sağlayıcı", + model: "Model", + temperature: "Sıcaklık", + apiKey: "API Anahtarı (isteğe bağlı)", + systemPrompt: "Sistem istemi geçersiz kılma", + advancedOptions: "Gelişmiş seçenekler", + examplePrompts: "Örnek istemler", + manageModels: "Modelleri yönet", + selectProvider: "Sağlayıcı seç", + selectModel: "Model seç", + prompt: "İstem", + leaveEmptyForGlobalKey: "Genel anahtarı kullanmak için boş bırakın", + createdFiles: "Oluşturulan dosyalar", + viewSteps: "Adımları görüntüle ({n})", + waitingForLogs: "Günlükler bekleniyor…", + errorsDetected: "Hatalar algılandı", + }, + settings: { + title: "Yapılandırma", + subtitle: "Yapay zeka motorlarınızı, modellerinizi ve sistem istemlerinizi yönetin.", + tabs: { + general: "Genel", + providers: "Sağlayıcılar", + models: "Modeller", + prompts: "İstemler", + secrets: "Gizli bilgiler", + }, + }, + general: { + title: "Genel tercihler", + sections: { + networkApi: "Ağ ve API", + preferredIntelligence: "Tercih edilen yapay zeka", + appearance: "Görünüm", + language: "Dil", + }, + fields: { + backendUrl: "Backend URL", + backendUrlHint: "Unity Generator backend hizmetinizin adresi", + outputBasePath: "Temel yol (çıktı)", + outputBasePathHint: "Oluşturulan dosyaların kaydedildiği yer", + unityEditorPath: "Unity Editor yolu (isteğe bağlı)", + unityEditorPathHint: "Unity Editor yürütülebilir dosyasının tam yolu", + preferredLlm: "Varsayılan metin / mantık sağlayıcısı", + preferredLlmModel: "Varsayılan metin / mantık modeli", + preferredImage: "Varsayılan görsel oluşturma sağlayıcısı", + preferredImageModel: "Varsayılan görsel oluşturma modeli", + preferredAudio: "Varsayılan konuşma (TTS) sağlayıcısı", + preferredAudioModel: "Varsayılan konuşma (TTS) modeli", + preferredMusic: "Varsayılan müzik oluşturma sağlayıcısı", + preferredMusicModel: "Varsayılan müzik oluşturma modeli", + theme: "Tema", + language: "Arayüz dili", + }, + actions: { saveAll: "Tüm değişiklikleri kaydet" }, + status: { + saving: "Kaydediliyor…", + saved: "Tercihler başarıyla kaydedildi.", + modelNotRegistered: '"{provider}" sağlayıcısı için "{model}" modeli kayıtlı değil.', + networkError: "Ağ hatası: Backend'e ulaşılamadı.", + }, + }, + code: { + title: "Unity C# Kodu", + subtitle: "Yapay zeka ile Unity C# betikleri oluşturun.", + fields: { prompt: "İstem", maxTokens: "Maksimum token" }, + actions: { generate: "Kod oluştur" }, + activeProject: "Aktif proje: {name}", + autoSave: "Projeye otomatik kaydet", + }, + text: { + title: "Metin oluşturma", + subtitle: "Yapay zeka ile metin içeriği oluşturun.", + fields: { prompt: "İstem", length: "Uzunluk" }, + actions: { generate: "Metin oluştur" }, + }, + image: { + title: "Görsel oluşturma", + subtitle: "Yapay zeka ile görseller oluşturun.", + fields: { + prompt: "İstem", + aspectRatio: "En-boy oranı", + quality: "Kalite", + textureName: "Doku adı", + textureType: "Doku türü", + }, + actions: { generate: "Görsel oluştur", saveToUnity: "Unity'ye kaydet" }, + }, + sprites: { + title: "2D Sprite'lar", + badge: "Piksel sanatına uygun", + subtitle: "Unity için 2D sprite varlıkları oluşturun.", + fields: { + prompt: "İstem", + resolution: "Çözünürlük", + paletteSize: "Palet boyutu", + autoCrop: "Şeffaf kenarları otomatik kırp", + colors: "{n} renk", + }, + preview: "Önizleme alanı", + actions: { generate: "Sprite oluştur" }, + }, + audio: { + title: "Ses oluşturma", + subtitle: "Yapay zeka ile ses varlıkları oluşturun.", + generationType: "Oluşturma türü", + speechTts: "Konuşma (TTS)", + atmosphericMusic: "Atmosferik müzik", + fields: { + prompt: "İstem", + musicDescription: "Müzik açıklaması", + speechPrompt: "Konuşma istemi", + voiceOptional: "Ses (isteğe bağlı)", + musicModel: "Müzik modeli", + stability: "Kararlılık", + audioClipName: "Ses klibi adı", + audioFormat: "Ses formatı", + }, + actions: { generate: "Ses oluştur", saveToUnity: "Unity'ye kaydet" }, + }, + scenes: { + title: "Sahne oluşturucu", + subtitle: "Yapay zeka ile Unity sahneleri oluşturun.", + fields: { prompt: "Sahne açıklaması" }, + actions: { generate: "Sahne oluştur" }, + mediaReady: "İçe aktarmaya hazır medya", + mediaReadyText: + '{type} "{name}" Unity\'ye aktarılmaya hazır. Aşağıdaki istemi inceleyin ve devam etmek için "Sahne oluştur"a tıklayın.', + mediaTypeImage: "Görsel", + mediaTypeAudio: "Ses", + }, + unityUi: { + title: "Unity UI", + subtitle: "Yapay zeka ile Unity UI bileşenleri oluşturun.", + fields: { + uiSystem: "UI sistemi", + elementType: "Öğe türü", + prompt: "UI öğesi açıklaması", + outputFormat: "Çıktı formatı", + anchorPreset: "Sabitleme ön ayarı", + colourTheme: "Renk teması (isteğe bağlı)", + includeAnimations: "Animasyonları dahil et", + }, + actions: { generate: "UI oluştur" }, + }, + unityPhysics: { + title: "Unity Fizik", + subtitle: "Unity fizik yapılandırması oluşturun — Rigidbody'ler, çarpışma birimleri, yerçekimi ve daha fazlası.", + fields: { + physicsBackend: "Fizik motoru", + simulationMode: "Simülasyon modu", + gravityPreset: "Yerçekimi ön ayarı", + prompt: "Fizik açıklaması", + includeRigidbody: "Rigidbody kurulumunu dahil et", + includeColliders: "Çarpışma birimi kurulumunu dahil et", + includeLayers: "Fizik katmanlarını dahil et", + }, + actions: { generate: "Fizik yapılandırması oluştur" }, + quickActions: "Hızlı işlemler", + }, + unityProject: { + title: "Unity Projesi", + subtitle: "Tam bir Unity proje yapısı oluşturun.", + engineSettings: "Unity motor ayarları", + fields: { + projectName: "Proje adı", + unityTemplate: "Unity şablonu", + unityVersion: "Unity sürümü", + targetPlatform: "Hedef platform", + generateDefaultScene: "Varsayılan sahne oluştur", + autoInstallPackages: "UPM paketlerini otomatik kur", + setupUrp: "URP kurulumu", + upmPackages: "UPM paketleri (virgülle ayrılmış)", + sceneName: "Sahne adı", + unityEditorPath: "Unity Editor yolu (isteğe bağlı)", + timeout: "Zaman aşımı (saniye)", + resultJson: "Sonuç (JSON)", + }, + actions: { + generate: "Proje oluştur", + finalize: "Projeyi sonlandır", + openFolder: "Çıktı klasörünü aç", + addVersion: "Unity sürümü ekle", + downloadZip: "Sonlandırılmış projeyi indir (.zip)", + }, + dialogs: { + addVersion: { + title: "Unity sürümü ekle", + versionId: "Sürüm kimliği", + label: "Etiket (isteğe bağlı)", + }, + }, + }, + theme: { light: "Açık", dark: "Koyu", system: "Sistem" }, + languages: { + en: "İngilizce", + es: "İspanyolca", + ca: "Katalanca", + eu: "Baskça", + oc: "Oksitanca", + uk: "Ukraynaca", + pt: "Portekizce", + gl: "Galiçyaca", + fr: "Fransızca", + it: "İtalyanca", + pl: "Lehçe", + zh: "Çince", + ar: "Arapça", + de: "Almanca", + hi: "Hintçe", + bn: "Bengalce", + ur: "Urduca", + id: "Endonezce", + ja: "Japonca", + vi: "Vietnamca", + ko: "Korece", + tr: "Türkçe", + }, +} as const; diff --git a/frontend/src/i18n/locales/uk.ts b/frontend/src/i18n/locales/uk.ts index ff6d74e9..5c245969 100644 --- a/frontend/src/i18n/locales/uk.ts +++ b/frontend/src/i18n/locales/uk.ts @@ -1,253 +1,261 @@ -/** - * Ukrainian (uk) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "Онлайн", offline: "Офлайн" }, - nav: { - settings: "Налаштування", - scenes: "Сцени", - code: "Код", - text: "Текст", - image: "Зображення", - sprites: "Спрайти", - audio: "Аудіо", - unityUi: "Unity UI", - unityPhysics: "Unity Фізика", - unityProject: "Проєкт Unity", - }, - actions: { repository: "Репозиторій" }, - }, - common: { - loading: "Завантаження…", - saving: "Збереження…", - save: "Зберегти", - cancel: "Скасувати", - close: "Закрити", - ok: "ОК", - yes: "Так", - no: "Ні", - error: "Помилка", - warning: "Попередження", - success: "Успіх", - generate: "Згенерувати", - result: "Результат", - provider: "Провайдер", - model: "Модель", - temperature: "Температура", - apiKey: "API-ключ (необов'язкове перевизначення)", - systemPrompt: "Перевизначення системного промпту", - advancedOptions: "Розширені параметри", - examplePrompts: "Приклади промптів", - manageModels: "Керувати моделями", - selectProvider: "Оберіть провайдера", - selectModel: "Оберіть модель", - prompt: "Промпт", - leaveEmptyForGlobalKey: "Залиште порожнім для використання глобального ключа", - createdFiles: "Створені файли", - viewSteps: "Переглянути кроки ({n})", - waitingForLogs: "Очікування журналів…", - errorsDetected: "Виявлено помилки", - }, - settings: { - title: "Конфігурація", - subtitle: "Керуйте своїми рушіями інтелекту, моделями та системними промптами.", - tabs: { - general: "Загальне", - providers: "Провайдери", - models: "Моделі", - prompts: "Промпти", - secrets: "Секрети", - }, - }, - general: { - title: "Загальні налаштування", - sections: { - networkApi: "Мережа та API", - preferredIntelligence: "Бажаний інтелект", - appearance: "Зовнішній вигляд", - language: "Мова", - }, - fields: { - backendUrl: "URL бекенду", - backendUrlHint: "Адреса вашого бекенд-сервісу Unity Generator", - outputBasePath: "Базовий шлях (вихід)", - outputBasePathHint: "Де зберігаються згенеровані файли", - unityEditorPath: "Шлях до редактора Unity (необов'язково)", - unityEditorPathHint: "Повний шлях до виконуваного файлу редактора Unity", - preferredLlm: "Стандартний провайдер тексту/логіки", - preferredLlmModel: "Стандартна модель тексту/логіки", - preferredImage: "Стандартний провайдер генерації зображень", - preferredImageModel: "Стандартна модель генерації зображень", - preferredAudio: "Стандартний провайдер мовлення (TTS)", - preferredAudioModel: "Стандартна модель мовлення (TTS)", - preferredMusic: "Стандартний провайдер музики", - preferredMusicModel: "Стандартна модель музики", - theme: "Тема", - language: "Мова інтерфейсу", - }, - actions: { saveAll: "Зберегти всі зміни" }, - status: { - saving: "Збереження…", - saved: "Налаштування успішно збережено.", - modelNotRegistered: 'Модель "{model}" не зареєстрована для провайдера "{provider}".', - networkError: "Помилка мережі: не вдалося підключитися до бекенду.", - }, - }, - code: { - title: "Код C# для Unity", - subtitle: "Генеруйте скрипти C# для Unity за допомогою ШІ.", - fields: { prompt: "Промпт", maxTokens: "Максимум токенів" }, - actions: { generate: "Згенерувати код" }, - activeProject: "Активний проєкт: {name}", - autoSave: "Автоматично зберігати в проєкт", - }, - text: { - title: "Генерація тексту", - subtitle: "Генеруйте текстовий контент за допомогою ШІ.", - fields: { prompt: "Промпт", length: "Довжина" }, - actions: { generate: "Згенерувати текст" }, - }, - image: { - title: "Генерація зображень", - subtitle: "Генеруйте зображення за допомогою ШІ.", - fields: { - prompt: "Промпт", - aspectRatio: "Співвідношення сторін", - quality: "Якість", - textureName: "Назва текстури", - textureType: "Тип текстури", - }, - actions: { generate: "Згенерувати зображення", saveToUnity: "Зберегти в Unity" }, - }, - sprites: { - title: "2D Спрайти", - badge: "Оптимізовано для Pixel Art", - subtitle: "Генеруйте 2D спрайти для Unity.", - fields: { - prompt: "Промпт", - resolution: "Роздільна здатність", - paletteSize: "Розмір палітри", - autoCrop: "Автоматичне обрізання прозорих країв", - colors: "{n} кольорів", - }, - preview: "Область попереднього перегляду", - actions: { generate: "Згенерувати спрайти" }, - }, - audio: { - title: "Генерація аудіо", - subtitle: "Генеруйте аудіоресурси за допомогою ШІ.", - generationType: "Тип генерації", - speechTts: "Мовлення (TTS)", - atmosphericMusic: "Атмосферна музика", - fields: { - prompt: "Промпт", - musicDescription: "Опис музики", - speechPrompt: "Промпт мовлення", - voiceOptional: "Голос (необов'язково)", - musicModel: "Модель музики", - stability: "Стабільність", - audioClipName: "Назва аудіокліпу", - audioFormat: "Формат аудіо", - }, - actions: { generate: "Згенерувати аудіо", saveToUnity: "Зберегти в Unity" }, - }, - scenes: { - title: "Конструктор сцен", - subtitle: "Генеруйте сцени Unity за допомогою ШІ.", - fields: { prompt: "Опис сцени" }, - actions: { generate: "Створити сцену" }, - mediaReady: "Медіа готове до імпорту", - mediaReadyText: "{type} \"{name}\" готове до імпорту в Unity. Перегляньте промпт і натисніть «Створити сцену» для продовження.", - mediaTypeImage: "Зображення", - mediaTypeAudio: "Аудіо", - }, - unityUi: { - title: "Unity UI", - subtitle: "Генеруйте компоненти UI для Unity за допомогою ШІ.", - fields: { - uiSystem: "Система UI", - elementType: "Тип елемента", - prompt: "Опис елемента UI", - outputFormat: "Формат виводу", - anchorPreset: "Пресет прив'язки", - colourTheme: "Колірна тема (необов'язково)", - includeAnimations: "Включити анімації", - }, - actions: { generate: "Згенерувати UI" }, - }, - unityPhysics: { - title: "Unity Фізика", - subtitle: "Генеруйте конфігурацію фізики Unity — Rigidbodies, колайдери, гравітація тощо.", - fields: { - physicsBackend: "Фізичний рушій", - simulationMode: "Режим симуляції", - gravityPreset: "Пресет гравітації", - prompt: "Опис фізики", - includeRigidbody: "Включити налаштування Rigidbody", - includeColliders: "Включити налаштування колайдерів", - includeLayers: "Включити фізичні шари", - }, - actions: { generate: "Згенерувати конфігурацію фізики" }, - quickActions: "Швидкі дії", - }, - unityProject: { - title: "Проєкт Unity", - subtitle: "Згенеруйте повну структуру проєкту Unity.", - engineSettings: "Налаштування рушія Unity", - fields: { - projectName: "Назва проєкту", - unityTemplate: "Шаблон Unity", - unityVersion: "Версія Unity", - targetPlatform: "Цільова платформа", - generateDefaultScene: "Згенерувати стандартну сцену", - autoInstallPackages: "Автоматично встановити пакети UPM", - setupUrp: "Налаштувати URP", - upmPackages: "Пакети UPM (через кому)", - sceneName: "Назва сцени", - unityEditorPath: "Шлях до редактора Unity (необов'язково)", - timeout: "Тайм-аут (секунди)", - resultJson: "Результат (JSON)", - }, - actions: { - generate: "Згенерувати проєкт", - finalize: "Завершити проєкт", - openFolder: "Відкрити вихідну папку", - addVersion: "Додати версію Unity", - downloadZip: "Завантажити завершений проєкт (.zip)", - }, - dialogs: { - addVersion: { - title: "Додати версію Unity", - versionId: "ID версії", - label: "Мітка (необов'язково)", - }, - }, - }, - theme: { light: "Світла", dark: "Темна", system: "Системна" }, - languages: { - en: "Англійська", - es: "Іспанська", - ca: "Каталонська", - eu: "Баскська", - oc: "Окситанська", - uk: "Українська", - pt: "Португальська", - gl: "Галісійська", - fr: "Французька", - it: "Італійська", - pl: "Польська", - zh: "Китайська", - ar: "Арабська", - de: "Німецька", - hi: "Гінді", - bn: "Бенгальська", - ur: "Урду", - id: "Індонезійська", - ja: "Японська", - vi: "В'єтнамська", - ko: "Корейська", - tr: "Турецька", - }, -} as const; +/** + * Ukrainian (uk) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "Онлайн", offline: "Офлайн" }, + nav: { + settings: "Налаштування", + scenes: "Сцени", + code: "Код", + text: "Текст", + image: "Зображення", + sprites: "Спрайти", + audio: "Аудіо", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity Фізика", + unityProject: "Проєкт Unity", + logs: "Logs", + }, + actions: { repository: "Репозиторій" }, + }, + common: { + loading: "Завантаження…", + saving: "Збереження…", + save: "Зберегти", + cancel: "Скасувати", + close: "Закрити", + ok: "ОК", + yes: "Так", + no: "Ні", + error: "Помилка", + warning: "Попередження", + success: "Успіх", + generate: "Згенерувати", + result: "Результат", + provider: "Провайдер", + model: "Модель", + temperature: "Температура", + apiKey: "API-ключ (необов'язкове перевизначення)", + systemPrompt: "Перевизначення системного промпту", + advancedOptions: "Розширені параметри", + examplePrompts: "Приклади промптів", + manageModels: "Керувати моделями", + selectProvider: "Оберіть провайдера", + selectModel: "Оберіть модель", + prompt: "Промпт", + leaveEmptyForGlobalKey: "Залиште порожнім для використання глобального ключа", + createdFiles: "Створені файли", + viewSteps: "Переглянути кроки ({n})", + waitingForLogs: "Очікування журналів…", + errorsDetected: "Виявлено помилки", + }, + settings: { + title: "Конфігурація", + subtitle: "Керуйте своїми рушіями інтелекту, моделями та системними промптами.", + tabs: { + general: "Загальне", + providers: "Провайдери", + models: "Моделі", + prompts: "Промпти", + secrets: "Секрети", + }, + }, + general: { + title: "Загальні налаштування", + sections: { + networkApi: "Мережа та API", + preferredIntelligence: "Бажаний інтелект", + appearance: "Зовнішній вигляд", + language: "Мова", + }, + fields: { + backendUrl: "URL бекенду", + backendUrlHint: "Адреса вашого бекенд-сервісу Unity Generator", + outputBasePath: "Базовий шлях (вихід)", + outputBasePathHint: "Де зберігаються згенеровані файли", + unityEditorPath: "Шлях до редактора Unity (необов'язково)", + unityEditorPathHint: "Повний шлях до виконуваного файлу редактора Unity", + preferredLlm: "Стандартний провайдер тексту/логіки", + preferredLlmModel: "Стандартна модель тексту/логіки", + preferredImage: "Стандартний провайдер генерації зображень", + preferredImageModel: "Стандартна модель генерації зображень", + preferredAudio: "Стандартний провайдер мовлення (TTS)", + preferredAudioModel: "Стандартна модель мовлення (TTS)", + preferredMusic: "Стандартний провайдер музики", + preferredMusicModel: "Стандартна модель музики", + theme: "Тема", + language: "Мова інтерфейсу", + }, + actions: { saveAll: "Зберегти всі зміни" }, + status: { + saving: "Збереження…", + saved: "Налаштування успішно збережено.", + modelNotRegistered: 'Модель "{model}" не зареєстрована для провайдера "{provider}".', + networkError: "Помилка мережі: не вдалося підключитися до бекенду.", + }, + }, + code: { + title: "Код C# для Unity", + subtitle: "Генеруйте скрипти C# для Unity за допомогою ШІ.", + fields: { prompt: "Промпт", maxTokens: "Максимум токенів" }, + actions: { generate: "Згенерувати код" }, + activeProject: "Активний проєкт: {name}", + autoSave: "Автоматично зберігати в проєкт", + }, + text: { + title: "Генерація тексту", + subtitle: "Генеруйте текстовий контент за допомогою ШІ.", + fields: { prompt: "Промпт", length: "Довжина" }, + actions: { generate: "Згенерувати текст" }, + }, + image: { + title: "Генерація зображень", + subtitle: "Генеруйте зображення за допомогою ШІ.", + fields: { + prompt: "Промпт", + aspectRatio: "Співвідношення сторін", + quality: "Якість", + textureName: "Назва текстури", + textureType: "Тип текстури", + }, + actions: { generate: "Згенерувати зображення", saveToUnity: "Зберегти в Unity" }, + }, + sprites: { + title: "2D Спрайти", + badge: "Оптимізовано для Pixel Art", + subtitle: "Генеруйте 2D спрайти для Unity.", + fields: { + prompt: "Промпт", + resolution: "Роздільна здатність", + paletteSize: "Розмір палітри", + autoCrop: "Автоматичне обрізання прозорих країв", + colors: "{n} кольорів", + }, + preview: "Область попереднього перегляду", + actions: { generate: "Згенерувати спрайти" }, + }, + audio: { + title: "Генерація аудіо", + subtitle: "Генеруйте аудіоресурси за допомогою ШІ.", + generationType: "Тип генерації", + speechTts: "Мовлення (TTS)", + atmosphericMusic: "Атмосферна музика", + fields: { + prompt: "Промпт", + musicDescription: "Опис музики", + speechPrompt: "Промпт мовлення", + voiceOptional: "Голос (необов'язково)", + musicModel: "Модель музики", + stability: "Стабільність", + audioClipName: "Назва аудіокліпу", + audioFormat: "Формат аудіо", + }, + actions: { generate: "Згенерувати аудіо", saveToUnity: "Зберегти в Unity" }, + }, + scenes: { + title: "Конструктор сцен", + subtitle: "Генеруйте сцени Unity за допомогою ШІ.", + fields: { prompt: "Опис сцени" }, + actions: { generate: "Створити сцену" }, + mediaReady: "Медіа готове до імпорту", + mediaReadyText: "{type} \"{name}\" готове до імпорту в Unity. Перегляньте промпт і натисніть «Створити сцену» для продовження.", + mediaTypeImage: "Зображення", + mediaTypeAudio: "Аудіо", + }, + unityUi: { + title: "Unity UI", + subtitle: "Генеруйте компоненти UI для Unity за допомогою ШІ.", + fields: { + uiSystem: "Система UI", + elementType: "Тип елемента", + prompt: "Опис елемента UI", + outputFormat: "Формат виводу", + anchorPreset: "Пресет прив'язки", + colourTheme: "Колірна тема (необов'язково)", + includeAnimations: "Включити анімації", + }, + actions: { generate: "Згенерувати UI" }, + }, + unityPhysics: { + title: "Unity Фізика", + subtitle: "Генеруйте конфігурацію фізики Unity — Rigidbodies, колайдери, гравітація тощо.", + fields: { + physicsBackend: "Фізичний рушій", + simulationMode: "Режим симуляції", + gravityPreset: "Пресет гравітації", + prompt: "Опис фізики", + includeRigidbody: "Включити налаштування Rigidbody", + includeColliders: "Включити налаштування колайдерів", + includeLayers: "Включити фізичні шари", + }, + actions: { generate: "Згенерувати конфігурацію фізики" }, + quickActions: "Швидкі дії", + }, + unityProject: { + title: "Проєкт Unity", + subtitle: "Згенеруйте повну структуру проєкту Unity.", + engineSettings: "Налаштування рушія Unity", + fields: { + projectName: "Назва проєкту", + unityTemplate: "Шаблон Unity", + unityVersion: "Версія Unity", + targetPlatform: "Цільова платформа", + generateDefaultScene: "Згенерувати стандартну сцену", + autoInstallPackages: "Автоматично встановити пакети UPM", + setupUrp: "Налаштувати URP", + upmPackages: "Пакети UPM (через кому)", + sceneName: "Назва сцени", + unityEditorPath: "Шлях до редактора Unity (необов'язково)", + timeout: "Тайм-аут (секунди)", + resultJson: "Результат (JSON)", + }, + actions: { + generate: "Згенерувати проєкт", + finalize: "Завершити проєкт", + openFolder: "Відкрити вихідну папку", + addVersion: "Додати версію Unity", + downloadZip: "Завантажити завершений проєкт (.zip)", + }, + dialogs: { + addVersion: { + title: "Додати версію Unity", + versionId: "ID версії", + label: "Мітка (необов'язково)", + }, + }, + }, + theme: { light: "Світла", dark: "Темна", system: "Системна" }, + languages: { + en: "Англійська", + es: "Іспанська", + ca: "Каталонська", + eu: "Баскська", + oc: "Окситанська", + uk: "Українська", + pt: "Португальська", + gl: "Галісійська", + fr: "Французька", + it: "Італійська", + pl: "Польська", + zh: "Китайська", + ar: "Арабська", + de: "Німецька", + hi: "Гінді", + bn: "Бенгальська", + ur: "Урду", + id: "Індонезійська", + ja: "Японська", + vi: "В'єтнамська", + ko: "Корейська", + tr: "Турецька", + }, +} as const; diff --git a/frontend/src/i18n/locales/ur.ts b/frontend/src/i18n/locales/ur.ts index 1bb409da..0945a26e 100644 --- a/frontend/src/i18n/locales/ur.ts +++ b/frontend/src/i18n/locales/ur.ts @@ -1,112 +1,120 @@ -/** - * Urdu (ur) locale. RTL language. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "آن لائن", offline: "آف لائن" }, - nav: { - settings: "ترتیبات", scenes: "مناظر", code: "کوڈ", text: "متن", - image: "تصویر", sprites: "اسپرائٹس", audio: "آڈیو", - unityUi: "Unity UI", unityPhysics: "Unity طبیعیات", unityProject: "Unity منصوبہ", - }, - actions: { repository: "ریپوزیٹری" }, - }, - common: { - loading: "لوڈ ہو رہا ہے…", saving: "محفوظ ہو رہا ہے…", save: "محفوظ کریں", - cancel: "منسوخ کریں", close: "بند کریں", ok: "ٹھیک ہے", yes: "ہاں", no: "نہیں", - error: "خرابی", warning: "انتباہ", success: "کامیابی", generate: "بنائیں", - result: "نتیجہ", provider: "فراہم کنندہ", model: "ماڈل", temperature: "درجہ حرارت", - apiKey: "API کلید (اختیاری اوور رائڈ)", systemPrompt: "سسٹم پرامپٹ اوور رائڈ", - advancedOptions: "جدید اختیارات", examplePrompts: "مثال پرامپٹس", - manageModels: "ماڈلز منظم کریں", selectProvider: "فراہم کنندہ منتخب کریں", - selectModel: "ماڈل منتخب کریں", prompt: "پرامپٹ", - leaveEmptyForGlobalKey: "عالمی کلید استعمال کرنے کے لیے خالی چھوڑیں", - createdFiles: "بنائی گئی فائلیں", viewSteps: "مراحل دیکھیں ({n})", - waitingForLogs: "لاگز کا انتظار…", errorsDetected: "خرابیاں ملیں", - }, - settings: { - title: "ترتیب", - subtitle: "اپنے ذہانت انجن، ماڈلز اور سسٹم پرامپٹس منظم کریں۔", - tabs: { general: "عمومی", providers: "فراہم کنندگان", models: "ماڈلز", prompts: "پرامپٹس", secrets: "راز" }, - }, - general: { - title: "عمومی ترجیحات", - sections: { networkApi: "نیٹ ورک اور API", preferredIntelligence: "پسندیدہ ذہانت", appearance: "ظاہری شکل", language: "زبان" }, - fields: { - backendUrl: "بیک اینڈ URL", backendUrlHint: "آپ کی Unity Generator بیک اینڈ سروس کا پتہ", - outputBasePath: "بنیادی راستہ (آؤٹ پٹ)", outputBasePathHint: "بنائی گئی فائلیں کہاں محفوظ ہوتی ہیں", - unityEditorPath: "Unity ایڈیٹر راستہ (اختیاری)", unityEditorPathHint: "Unity ایڈیٹر ایگزیکیوٹیبل کا مکمل راستہ", - preferredLlm: "ڈیفالٹ متن/منطق فراہم کنندہ", preferredLlmModel: "ڈیفالٹ متن/منطق ماڈل", - preferredImage: "ڈیفالٹ تصویر سازی فراہم کنندہ", preferredImageModel: "ڈیفالٹ تصویر سازی ماڈل", - preferredAudio: "ڈیفالٹ آواز (TTS) فراہم کنندہ", preferredAudioModel: "ڈیفالٹ آواز (TTS) ماڈل", - preferredMusic: "ڈیفالٹ موسیقی فراہم کنندہ", preferredMusicModel: "ڈیفالٹ موسیقی ماڈل", - theme: "تھیم", language: "انٹرفیس زبان", - }, - actions: { saveAll: "تمام تبدیلیاں محفوظ کریں" }, - status: { - saving: "محفوظ ہو رہا ہے…", saved: "ترجیحات کامیابی سے محفوظ ہو گئیں۔", - modelNotRegistered: 'ماڈل "{model}" فراہم کنندہ "{provider}" کے لیے رجسٹرڈ نہیں ہے۔', - networkError: "نیٹ ورک خرابی: بیک اینڈ سے رابطہ نہیں ہو سکا۔", - }, - }, - code: { - title: "Unity C# کوڈ", subtitle: "AI استعمال کرتے ہوئے Unity C# اسکرپٹس بنائیں۔", - fields: { prompt: "پرامپٹ", maxTokens: "زیادہ سے زیادہ ٹوکنز" }, - actions: { generate: "کوڈ بنائیں" }, - activeProject: "فعال منصوبہ: {name}", autoSave: "منصوبے میں خودکار محفوظ کریں", - }, - text: { - title: "متن سازی", subtitle: "AI استعمال کرتے ہوئے متن مواد بنائیں۔", - fields: { prompt: "پرامپٹ", length: "لمبائی" }, actions: { generate: "متن بنائیں" }, - }, - image: { - title: "تصویر سازی", subtitle: "AI استعمال کرتے ہوئے تصاویر بنائیں۔", - fields: { prompt: "پرامپٹ", aspectRatio: "پہلو تناسب", quality: "معیار", textureName: "ٹیکسچر نام", textureType: "ٹیکسچر قسم" }, - actions: { generate: "تصویر بنائیں", saveToUnity: "Unity میں محفوظ کریں" }, - }, - sprites: { - title: "2D اسپرائٹس", badge: "پکسل آرٹ کے لیے بہتر", subtitle: "Unity کے لیے 2D اسپرائٹس بنائیں۔", - fields: { prompt: "پرامپٹ", resolution: "ریزولیوشن", paletteSize: "پیلیٹ سائز", autoCrop: "شفاف کناروں کو خودکار کاٹیں", colors: "{n} رنگ" }, - preview: "پیش نظارہ علاقہ", actions: { generate: "اسپرائٹس بنائیں" }, - }, - audio: { - title: "آڈیو سازی", subtitle: "AI استعمال کرتے ہوئے آڈیو وسائل بنائیں۔", - generationType: "سازی کی قسم", speechTts: "تقریر (TTS)", atmosphericMusic: "ماحولیاتی موسیقی", - fields: { prompt: "پرامپٹ", musicDescription: "موسیقی کی تفصیل", speechPrompt: "تقریر پرامپٹ", voiceOptional: "آواز (اختیاری)", musicModel: "موسیقی ماڈل", stability: "استحکام", audioClipName: "آڈیو کلپ نام", audioFormat: "آڈیو فارمیٹ" }, - actions: { generate: "آڈیو بنائیں", saveToUnity: "Unity میں محفوظ کریں" }, - }, - scenes: { - title: "منظر ساز", subtitle: "AI استعمال کرتے ہوئے Unity مناظر بنائیں۔", - fields: { prompt: "منظر کی تفصیل" }, actions: { generate: "منظر بنائیں" }, - mediaReady: "میڈیا درآمد کے لیے تیار", - mediaReadyText: "{type} \"{name}\" Unity میں درآمد کے لیے تیار ہے۔ پرامپٹ کا جائزہ لیں اور جاری رکھنے کے لیے \"منظر بنائیں\" پر کلک کریں۔", - mediaTypeImage: "تصویر", mediaTypeAudio: "آڈیو", - }, - unityUi: { - title: "Unity UI", subtitle: "AI استعمال کرتے ہوئے Unity UI اجزاء بنائیں۔", - fields: { uiSystem: "UI سسٹم", elementType: "عنصر کی قسم", prompt: "UI عنصر کی تفصیل", outputFormat: "آؤٹ پٹ فارمیٹ", anchorPreset: "اینکر پری سیٹ", colourTheme: "رنگ تھیم (اختیاری)", includeAnimations: "اینیمیشنز شامل کریں" }, - actions: { generate: "UI بنائیں" }, - }, - unityPhysics: { - title: "Unity طبیعیات", subtitle: "Unity طبیعیات ترتیب بنائیں — Rigidbodies، Colliders، کشش ثقل اور مزید۔", - fields: { physicsBackend: "طبیعیات انجن", simulationMode: "نقلی طریقہ", gravityPreset: "کشش ثقل پری سیٹ", prompt: "طبیعیات کی تفصیل", includeRigidbody: "Rigidbody ترتیب شامل کریں", includeColliders: "Collider ترتیب شامل کریں", includeLayers: "طبیعیات پرتیں شامل کریں" }, - actions: { generate: "طبیعیات ترتیب بنائیں" }, - quickActions: "فوری اقدامات", - }, - unityProject: { - title: "Unity منصوبہ", subtitle: "مکمل Unity منصوبہ ڈھانچہ بنائیں۔", - engineSettings: "Unity انجن ترتیبات", - fields: { projectName: "منصوبے کا نام", unityTemplate: "Unity ٹیمپلیٹ", unityVersion: "Unity ورژن", targetPlatform: "ہدف پلیٹ فارم", generateDefaultScene: "ڈیفالٹ منظر بنائیں", autoInstallPackages: "UPM پیکجز خودکار انسٹال کریں", setupUrp: "URP ترتیب دیں", upmPackages: "UPM پیکجز (کاما سے الگ)", sceneName: "منظر کا نام", unityEditorPath: "Unity ایڈیٹر راستہ (اختیاری)", timeout: "ٹائم آؤٹ (سیکنڈ)", resultJson: "نتیجہ (JSON)" }, - actions: { generate: "منصوبہ بنائیں", finalize: "منصوبہ مکمل کریں", openFolder: "آؤٹ پٹ فولڈر کھولیں", addVersion: "Unity ورژن شامل کریں", downloadZip: "مکمل منصوبہ ڈاؤن لوڈ کریں (.zip)" }, - dialogs: { addVersion: { title: "Unity ورژن شامل کریں", versionId: "ورژن ID", label: "لیبل (اختیاری)" } }, - }, - theme: { light: "روشن", dark: "تاریک", system: "سسٹم" }, - languages: { - en: "انگریزی", es: "ہسپانوی", ca: "کاتالان", eu: "باسک", oc: "آکسیٹان", uk: "یوکرینی", - pt: "پرتگالی", gl: "گالیشین", fr: "فرانسیسی", it: "اطالوی", pl: "پولش", zh: "چینی", - ar: "عربی", de: "جرمن", hi: "ہندی", bn: "بنگالی", ur: "اردو", - id: "انڈونیشیائی", ja: "جاپانی", vi: "ویتنامی", ko: "کوریائی", - tr: "ترکی", - }, -} as const; +/** + * Urdu (ur) locale. RTL language. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "آن لائن", offline: "آف لائن" }, + nav: { + settings: "ترتیبات", scenes: "مناظر", code: "کوڈ", text: "متن", + image: "تصویر", sprites: "اسپرائٹس", audio: "آڈیو", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", unityPhysics: "Unity طبیعیات", unityProject: "Unity منصوبہ", + logs: "Logs", + }, + actions: { repository: "ریپوزیٹری" }, + }, + common: { + loading: "لوڈ ہو رہا ہے…", saving: "محفوظ ہو رہا ہے…", save: "محفوظ کریں", + cancel: "منسوخ کریں", close: "بند کریں", ok: "ٹھیک ہے", yes: "ہاں", no: "نہیں", + error: "خرابی", warning: "انتباہ", success: "کامیابی", generate: "بنائیں", + result: "نتیجہ", provider: "فراہم کنندہ", model: "ماڈل", temperature: "درجہ حرارت", + apiKey: "API کلید (اختیاری اوور رائڈ)", systemPrompt: "سسٹم پرامپٹ اوور رائڈ", + advancedOptions: "جدید اختیارات", examplePrompts: "مثال پرامپٹس", + manageModels: "ماڈلز منظم کریں", selectProvider: "فراہم کنندہ منتخب کریں", + selectModel: "ماڈل منتخب کریں", prompt: "پرامپٹ", + leaveEmptyForGlobalKey: "عالمی کلید استعمال کرنے کے لیے خالی چھوڑیں", + createdFiles: "بنائی گئی فائلیں", viewSteps: "مراحل دیکھیں ({n})", + waitingForLogs: "لاگز کا انتظار…", errorsDetected: "خرابیاں ملیں", + }, + settings: { + title: "ترتیب", + subtitle: "اپنے ذہانت انجن، ماڈلز اور سسٹم پرامپٹس منظم کریں۔", + tabs: { general: "عمومی", providers: "فراہم کنندگان", models: "ماڈلز", prompts: "پرامپٹس", secrets: "راز" }, + }, + general: { + title: "عمومی ترجیحات", + sections: { networkApi: "نیٹ ورک اور API", preferredIntelligence: "پسندیدہ ذہانت", appearance: "ظاہری شکل", language: "زبان" }, + fields: { + backendUrl: "بیک اینڈ URL", backendUrlHint: "آپ کی Unity Generator بیک اینڈ سروس کا پتہ", + outputBasePath: "بنیادی راستہ (آؤٹ پٹ)", outputBasePathHint: "بنائی گئی فائلیں کہاں محفوظ ہوتی ہیں", + unityEditorPath: "Unity ایڈیٹر راستہ (اختیاری)", unityEditorPathHint: "Unity ایڈیٹر ایگزیکیوٹیبل کا مکمل راستہ", + preferredLlm: "ڈیفالٹ متن/منطق فراہم کنندہ", preferredLlmModel: "ڈیفالٹ متن/منطق ماڈل", + preferredImage: "ڈیفالٹ تصویر سازی فراہم کنندہ", preferredImageModel: "ڈیفالٹ تصویر سازی ماڈل", + preferredAudio: "ڈیفالٹ آواز (TTS) فراہم کنندہ", preferredAudioModel: "ڈیفالٹ آواز (TTS) ماڈل", + preferredMusic: "ڈیفالٹ موسیقی فراہم کنندہ", preferredMusicModel: "ڈیفالٹ موسیقی ماڈل", + theme: "تھیم", language: "انٹرفیس زبان", + }, + actions: { saveAll: "تمام تبدیلیاں محفوظ کریں" }, + status: { + saving: "محفوظ ہو رہا ہے…", saved: "ترجیحات کامیابی سے محفوظ ہو گئیں۔", + modelNotRegistered: 'ماڈل "{model}" فراہم کنندہ "{provider}" کے لیے رجسٹرڈ نہیں ہے۔', + networkError: "نیٹ ورک خرابی: بیک اینڈ سے رابطہ نہیں ہو سکا۔", + }, + }, + code: { + title: "Unity C# کوڈ", subtitle: "AI استعمال کرتے ہوئے Unity C# اسکرپٹس بنائیں۔", + fields: { prompt: "پرامپٹ", maxTokens: "زیادہ سے زیادہ ٹوکنز" }, + actions: { generate: "کوڈ بنائیں" }, + activeProject: "فعال منصوبہ: {name}", autoSave: "منصوبے میں خودکار محفوظ کریں", + }, + text: { + title: "متن سازی", subtitle: "AI استعمال کرتے ہوئے متن مواد بنائیں۔", + fields: { prompt: "پرامپٹ", length: "لمبائی" }, actions: { generate: "متن بنائیں" }, + }, + image: { + title: "تصویر سازی", subtitle: "AI استعمال کرتے ہوئے تصاویر بنائیں۔", + fields: { prompt: "پرامپٹ", aspectRatio: "پہلو تناسب", quality: "معیار", textureName: "ٹیکسچر نام", textureType: "ٹیکسچر قسم" }, + actions: { generate: "تصویر بنائیں", saveToUnity: "Unity میں محفوظ کریں" }, + }, + sprites: { + title: "2D اسپرائٹس", badge: "پکسل آرٹ کے لیے بہتر", subtitle: "Unity کے لیے 2D اسپرائٹس بنائیں۔", + fields: { prompt: "پرامپٹ", resolution: "ریزولیوشن", paletteSize: "پیلیٹ سائز", autoCrop: "شفاف کناروں کو خودکار کاٹیں", colors: "{n} رنگ" }, + preview: "پیش نظارہ علاقہ", actions: { generate: "اسپرائٹس بنائیں" }, + }, + audio: { + title: "آڈیو سازی", subtitle: "AI استعمال کرتے ہوئے آڈیو وسائل بنائیں۔", + generationType: "سازی کی قسم", speechTts: "تقریر (TTS)", atmosphericMusic: "ماحولیاتی موسیقی", + fields: { prompt: "پرامپٹ", musicDescription: "موسیقی کی تفصیل", speechPrompt: "تقریر پرامپٹ", voiceOptional: "آواز (اختیاری)", musicModel: "موسیقی ماڈل", stability: "استحکام", audioClipName: "آڈیو کلپ نام", audioFormat: "آڈیو فارمیٹ" }, + actions: { generate: "آڈیو بنائیں", saveToUnity: "Unity میں محفوظ کریں" }, + }, + scenes: { + title: "منظر ساز", subtitle: "AI استعمال کرتے ہوئے Unity مناظر بنائیں۔", + fields: { prompt: "منظر کی تفصیل" }, actions: { generate: "منظر بنائیں" }, + mediaReady: "میڈیا درآمد کے لیے تیار", + mediaReadyText: "{type} \"{name}\" Unity میں درآمد کے لیے تیار ہے۔ پرامپٹ کا جائزہ لیں اور جاری رکھنے کے لیے \"منظر بنائیں\" پر کلک کریں۔", + mediaTypeImage: "تصویر", mediaTypeAudio: "آڈیو", + }, + unityUi: { + title: "Unity UI", subtitle: "AI استعمال کرتے ہوئے Unity UI اجزاء بنائیں۔", + fields: { uiSystem: "UI سسٹم", elementType: "عنصر کی قسم", prompt: "UI عنصر کی تفصیل", outputFormat: "آؤٹ پٹ فارمیٹ", anchorPreset: "اینکر پری سیٹ", colourTheme: "رنگ تھیم (اختیاری)", includeAnimations: "اینیمیشنز شامل کریں" }, + actions: { generate: "UI بنائیں" }, + }, + unityPhysics: { + title: "Unity طبیعیات", subtitle: "Unity طبیعیات ترتیب بنائیں — Rigidbodies، Colliders، کشش ثقل اور مزید۔", + fields: { physicsBackend: "طبیعیات انجن", simulationMode: "نقلی طریقہ", gravityPreset: "کشش ثقل پری سیٹ", prompt: "طبیعیات کی تفصیل", includeRigidbody: "Rigidbody ترتیب شامل کریں", includeColliders: "Collider ترتیب شامل کریں", includeLayers: "طبیعیات پرتیں شامل کریں" }, + actions: { generate: "طبیعیات ترتیب بنائیں" }, + quickActions: "فوری اقدامات", + }, + unityProject: { + title: "Unity منصوبہ", subtitle: "مکمل Unity منصوبہ ڈھانچہ بنائیں۔", + engineSettings: "Unity انجن ترتیبات", + fields: { projectName: "منصوبے کا نام", unityTemplate: "Unity ٹیمپلیٹ", unityVersion: "Unity ورژن", targetPlatform: "ہدف پلیٹ فارم", generateDefaultScene: "ڈیفالٹ منظر بنائیں", autoInstallPackages: "UPM پیکجز خودکار انسٹال کریں", setupUrp: "URP ترتیب دیں", upmPackages: "UPM پیکجز (کاما سے الگ)", sceneName: "منظر کا نام", unityEditorPath: "Unity ایڈیٹر راستہ (اختیاری)", timeout: "ٹائم آؤٹ (سیکنڈ)", resultJson: "نتیجہ (JSON)" }, + actions: { generate: "منصوبہ بنائیں", finalize: "منصوبہ مکمل کریں", openFolder: "آؤٹ پٹ فولڈر کھولیں", addVersion: "Unity ورژن شامل کریں", downloadZip: "مکمل منصوبہ ڈاؤن لوڈ کریں (.zip)" }, + dialogs: { addVersion: { title: "Unity ورژن شامل کریں", versionId: "ورژن ID", label: "لیبل (اختیاری)" } }, + }, + theme: { light: "روشن", dark: "تاریک", system: "سسٹم" }, + languages: { + en: "انگریزی", es: "ہسپانوی", ca: "کاتالان", eu: "باسک", oc: "آکسیٹان", uk: "یوکرینی", + pt: "پرتگالی", gl: "گالیشین", fr: "فرانسیسی", it: "اطالوی", pl: "پولش", zh: "چینی", + ar: "عربی", de: "جرمن", hi: "ہندی", bn: "بنگالی", ur: "اردو", + id: "انڈونیشیائی", ja: "جاپانی", vi: "ویتنامی", ko: "کوریائی", + tr: "ترکی", + }, +} as const; diff --git a/frontend/src/i18n/locales/vi.ts b/frontend/src/i18n/locales/vi.ts index 0b535379..8cbcc717 100644 --- a/frontend/src/i18n/locales/vi.ts +++ b/frontend/src/i18n/locales/vi.ts @@ -1,113 +1,121 @@ -/** - * Vietnamese (vi) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "Trực tuyến", offline: "Ngoại tuyến" }, - nav: { - settings: "Cài đặt", scenes: "Cảnh", code: "Mã", text: "Văn bản", - image: "Hình ảnh", sprites: "Sprites", audio: "Âm thanh", - unityUi: "Unity UI", unityPhysics: "Unity Vật lý", unityProject: "Dự án Unity", - }, - actions: { repository: "Kho lưu trữ" }, - }, - common: { - loading: "Đang tải…", saving: "Đang lưu…", save: "Lưu", - cancel: "Hủy", close: "Đóng", ok: "OK", yes: "Có", no: "Không", - error: "Lỗi", warning: "Cảnh báo", success: "Thành công", generate: "Tạo", - result: "Kết quả", provider: "Nhà cung cấp", model: "Mô hình", temperature: "Nhiệt độ", - apiKey: "Khóa API (ghi đè tùy chọn)", systemPrompt: "Ghi đè prompt hệ thống", - advancedOptions: "Tùy chọn nâng cao", examplePrompts: "Ví dụ prompt", - manageModels: "Quản lý mô hình", selectProvider: "Chọn nhà cung cấp", - selectModel: "Chọn mô hình", prompt: "Prompt", - leaveEmptyForGlobalKey: "Để trống để dùng khóa toàn cục", - createdFiles: "Tệp đã tạo", viewSteps: "Xem các bước ({n})", - waitingForLogs: "Đang chờ nhật ký…", errorsDetected: "Phát hiện lỗi", - }, - settings: { - title: "Cấu hình", - subtitle: "Quản lý các công cụ trí tuệ, mô hình và prompt hệ thống của bạn.", - tabs: { general: "Chung", providers: "Nhà cung cấp", models: "Mô hình", prompts: "Prompt", secrets: "Bí mật" }, - }, - general: { - title: "Tùy chọn chung", - sections: { networkApi: "Mạng & API", preferredIntelligence: "Trí tuệ ưa thích", appearance: "Giao diện", language: "Ngôn ngữ" }, - fields: { - backendUrl: "URL backend", backendUrlHint: "Địa chỉ dịch vụ backend Unity Generator của bạn", - outputBasePath: "Đường dẫn cơ sở (đầu ra)", outputBasePathHint: "Nơi lưu các tệp được tạo", - unityEditorPath: "Đường dẫn Unity Editor (tùy chọn)", unityEditorPathHint: "Đường dẫn đầy đủ đến tệp thực thi Unity Editor", - preferredLlm: "Nhà cung cấp văn bản/logic mặc định", preferredLlmModel: "Mô hình văn bản/logic mặc định", - preferredImage: "Nhà cung cấp tạo ảnh mặc định", preferredImageModel: "Mô hình tạo ảnh mặc định", - preferredAudio: "Nhà cung cấp giọng nói (TTS) mặc định", preferredAudioModel: "Mô hình giọng nói (TTS) mặc định", - preferredMusic: "Nhà cung cấp âm nhạc mặc định", preferredMusicModel: "Mô hình âm nhạc mặc định", - theme: "Giao diện", language: "Ngôn ngữ giao diện", - }, - actions: { saveAll: "Lưu tất cả thay đổi" }, - status: { - saving: "Đang lưu…", saved: "Đã lưu tùy chọn thành công.", - modelNotRegistered: 'Mô hình "{model}" chưa được đăng ký cho nhà cung cấp "{provider}".', - networkError: "Lỗi mạng: không thể kết nối với backend.", - }, - }, - code: { - title: "Mã C# Unity", subtitle: "Tạo script C# Unity bằng AI.", - fields: { prompt: "Prompt", maxTokens: "Token tối đa" }, - actions: { generate: "Tạo mã" }, - activeProject: "Dự án đang hoạt động: {name}", autoSave: "Tự động lưu vào dự án", - }, - text: { - title: "Tạo văn bản", subtitle: "Tạo nội dung văn bản bằng AI.", - fields: { prompt: "Prompt", length: "Độ dài" }, actions: { generate: "Tạo văn bản" }, - }, - image: { - title: "Tạo hình ảnh", subtitle: "Tạo hình ảnh bằng AI.", - fields: { prompt: "Prompt", aspectRatio: "Tỷ lệ khung hình", quality: "Chất lượng", textureName: "Tên texture", textureType: "Loại texture" }, - actions: { generate: "Tạo hình ảnh", saveToUnity: "Lưu vào Unity" }, - }, - sprites: { - title: "Sprites 2D", badge: "Tối ưu cho Pixel Art", subtitle: "Tạo sprite 2D cho Unity.", - fields: { prompt: "Prompt", resolution: "Độ phân giải", paletteSize: "Kích thước bảng màu", autoCrop: "Tự động cắt viền trong suốt", colors: "{n} màu" }, - preview: "Khu vực xem trước", actions: { generate: "Tạo sprites" }, - }, - audio: { - title: "Tạo âm thanh", subtitle: "Tạo tài nguyên âm thanh bằng AI.", - generationType: "Loại tạo", speechTts: "Giọng nói (TTS)", atmosphericMusic: "Nhạc không khí", - fields: { prompt: "Prompt", musicDescription: "Mô tả âm nhạc", speechPrompt: "Prompt giọng nói", voiceOptional: "Giọng (tùy chọn)", musicModel: "Mô hình âm nhạc", stability: "Độ ổn định", audioClipName: "Tên clip âm thanh", audioFormat: "Định dạng âm thanh" }, - actions: { generate: "Tạo âm thanh", saveToUnity: "Lưu vào Unity" }, - }, - scenes: { - title: "Trình tạo cảnh", subtitle: "Tạo cảnh Unity bằng AI.", - fields: { prompt: "Mô tả cảnh" }, actions: { generate: "Tạo cảnh" }, - mediaReady: "Phương tiện sẵn sàng để nhập", - mediaReadyText: "{type} \"{name}\" sẵn sàng để nhập vào Unity. Xem lại prompt và nhấp \"Tạo cảnh\" để tiếp tục.", - mediaTypeImage: "Hình ảnh", mediaTypeAudio: "Âm thanh", - }, - unityUi: { - title: "Unity UI", subtitle: "Tạo thành phần UI Unity bằng AI.", - fields: { uiSystem: "Hệ thống UI", elementType: "Loại phần tử", prompt: "Mô tả phần tử UI", outputFormat: "Định dạng đầu ra", anchorPreset: "Preset neo", colourTheme: "Chủ đề màu (tùy chọn)", includeAnimations: "Bao gồm hoạt ảnh" }, - actions: { generate: "Tạo UI" }, - }, - unityPhysics: { - title: "Unity Vật lý", subtitle: "Tạo cấu hình vật lý Unity — Rigidbodies, collider, trọng lực và hơn thế.", - fields: { physicsBackend: "Động cơ vật lý", simulationMode: "Chế độ mô phỏng", gravityPreset: "Preset trọng lực", prompt: "Mô tả vật lý", includeRigidbody: "Bao gồm cấu hình Rigidbody", includeColliders: "Bao gồm cấu hình collider", includeLayers: "Bao gồm lớp vật lý" }, - actions: { generate: "Tạo cấu hình vật lý" }, - quickActions: "Hành động nhanh", - }, - unityProject: { - title: "Dự án Unity", subtitle: "Tạo cấu trúc dự án Unity hoàn chỉnh.", - engineSettings: "Cài đặt động cơ Unity", - fields: { projectName: "Tên dự án", unityTemplate: "Template Unity", unityVersion: "Phiên bản Unity", targetPlatform: "Nền tảng mục tiêu", generateDefaultScene: "Tạo cảnh mặc định", autoInstallPackages: "Tự động cài đặt gói UPM", setupUrp: "Cấu hình URP", upmPackages: "Gói UPM (phân cách bằng dấu phẩy)", sceneName: "Tên cảnh", unityEditorPath: "Đường dẫn Unity Editor (tùy chọn)", timeout: "Thời gian chờ (giây)", resultJson: "Kết quả (JSON)" }, - actions: { generate: "Tạo dự án", finalize: "Hoàn thiện dự án", openFolder: "Mở thư mục đầu ra", addVersion: "Thêm phiên bản Unity", downloadZip: "Tải dự án hoàn thiện (.zip)" }, - dialogs: { addVersion: { title: "Thêm phiên bản Unity", versionId: "ID phiên bản", label: "Nhãn (tùy chọn)" } }, - }, - theme: { light: "Sáng", dark: "Tối", system: "Hệ thống" }, - languages: { - en: "Tiếng Anh", es: "Tiếng Tây Ban Nha", ca: "Tiếng Catalan", eu: "Tiếng Basque", - oc: "Tiếng Occitan", uk: "Tiếng Ukraina", pt: "Tiếng Bồ Đào Nha", gl: "Tiếng Galicia", - fr: "Tiếng Pháp", it: "Tiếng Ý", pl: "Tiếng Ba Lan", zh: "Tiếng Trung", - ar: "Tiếng Ả Rập", de: "Tiếng Đức", hi: "Tiếng Hindi", bn: "Tiếng Bengali", - ur: "Tiếng Urdu", id: "Tiếng Indonesia", ja: "Tiếng Nhật", vi: "Tiếng Việt", ko: "Tiếng Hàn", - tr: "Tiếng Thổ Nhĩ Kỳ", - }, -} as const; +/** + * Vietnamese (vi) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "Trực tuyến", offline: "Ngoại tuyến" }, + nav: { + settings: "Cài đặt", scenes: "Cảnh", code: "Mã", text: "Văn bản", + image: "Hình ảnh", sprites: "Sprites", audio: "Âm thanh", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", unityPhysics: "Unity Vật lý", unityProject: "Dự án Unity", + logs: "Logs", + }, + actions: { repository: "Kho lưu trữ" }, + }, + common: { + loading: "Đang tải…", saving: "Đang lưu…", save: "Lưu", + cancel: "Hủy", close: "Đóng", ok: "OK", yes: "Có", no: "Không", + error: "Lỗi", warning: "Cảnh báo", success: "Thành công", generate: "Tạo", + result: "Kết quả", provider: "Nhà cung cấp", model: "Mô hình", temperature: "Nhiệt độ", + apiKey: "Khóa API (ghi đè tùy chọn)", systemPrompt: "Ghi đè prompt hệ thống", + advancedOptions: "Tùy chọn nâng cao", examplePrompts: "Ví dụ prompt", + manageModels: "Quản lý mô hình", selectProvider: "Chọn nhà cung cấp", + selectModel: "Chọn mô hình", prompt: "Prompt", + leaveEmptyForGlobalKey: "Để trống để dùng khóa toàn cục", + createdFiles: "Tệp đã tạo", viewSteps: "Xem các bước ({n})", + waitingForLogs: "Đang chờ nhật ký…", errorsDetected: "Phát hiện lỗi", + }, + settings: { + title: "Cấu hình", + subtitle: "Quản lý các công cụ trí tuệ, mô hình và prompt hệ thống của bạn.", + tabs: { general: "Chung", providers: "Nhà cung cấp", models: "Mô hình", prompts: "Prompt", secrets: "Bí mật" }, + }, + general: { + title: "Tùy chọn chung", + sections: { networkApi: "Mạng & API", preferredIntelligence: "Trí tuệ ưa thích", appearance: "Giao diện", language: "Ngôn ngữ" }, + fields: { + backendUrl: "URL backend", backendUrlHint: "Địa chỉ dịch vụ backend Unity Generator của bạn", + outputBasePath: "Đường dẫn cơ sở (đầu ra)", outputBasePathHint: "Nơi lưu các tệp được tạo", + unityEditorPath: "Đường dẫn Unity Editor (tùy chọn)", unityEditorPathHint: "Đường dẫn đầy đủ đến tệp thực thi Unity Editor", + preferredLlm: "Nhà cung cấp văn bản/logic mặc định", preferredLlmModel: "Mô hình văn bản/logic mặc định", + preferredImage: "Nhà cung cấp tạo ảnh mặc định", preferredImageModel: "Mô hình tạo ảnh mặc định", + preferredAudio: "Nhà cung cấp giọng nói (TTS) mặc định", preferredAudioModel: "Mô hình giọng nói (TTS) mặc định", + preferredMusic: "Nhà cung cấp âm nhạc mặc định", preferredMusicModel: "Mô hình âm nhạc mặc định", + theme: "Giao diện", language: "Ngôn ngữ giao diện", + }, + actions: { saveAll: "Lưu tất cả thay đổi" }, + status: { + saving: "Đang lưu…", saved: "Đã lưu tùy chọn thành công.", + modelNotRegistered: 'Mô hình "{model}" chưa được đăng ký cho nhà cung cấp "{provider}".', + networkError: "Lỗi mạng: không thể kết nối với backend.", + }, + }, + code: { + title: "Mã C# Unity", subtitle: "Tạo script C# Unity bằng AI.", + fields: { prompt: "Prompt", maxTokens: "Token tối đa" }, + actions: { generate: "Tạo mã" }, + activeProject: "Dự án đang hoạt động: {name}", autoSave: "Tự động lưu vào dự án", + }, + text: { + title: "Tạo văn bản", subtitle: "Tạo nội dung văn bản bằng AI.", + fields: { prompt: "Prompt", length: "Độ dài" }, actions: { generate: "Tạo văn bản" }, + }, + image: { + title: "Tạo hình ảnh", subtitle: "Tạo hình ảnh bằng AI.", + fields: { prompt: "Prompt", aspectRatio: "Tỷ lệ khung hình", quality: "Chất lượng", textureName: "Tên texture", textureType: "Loại texture" }, + actions: { generate: "Tạo hình ảnh", saveToUnity: "Lưu vào Unity" }, + }, + sprites: { + title: "Sprites 2D", badge: "Tối ưu cho Pixel Art", subtitle: "Tạo sprite 2D cho Unity.", + fields: { prompt: "Prompt", resolution: "Độ phân giải", paletteSize: "Kích thước bảng màu", autoCrop: "Tự động cắt viền trong suốt", colors: "{n} màu" }, + preview: "Khu vực xem trước", actions: { generate: "Tạo sprites" }, + }, + audio: { + title: "Tạo âm thanh", subtitle: "Tạo tài nguyên âm thanh bằng AI.", + generationType: "Loại tạo", speechTts: "Giọng nói (TTS)", atmosphericMusic: "Nhạc không khí", + fields: { prompt: "Prompt", musicDescription: "Mô tả âm nhạc", speechPrompt: "Prompt giọng nói", voiceOptional: "Giọng (tùy chọn)", musicModel: "Mô hình âm nhạc", stability: "Độ ổn định", audioClipName: "Tên clip âm thanh", audioFormat: "Định dạng âm thanh" }, + actions: { generate: "Tạo âm thanh", saveToUnity: "Lưu vào Unity" }, + }, + scenes: { + title: "Trình tạo cảnh", subtitle: "Tạo cảnh Unity bằng AI.", + fields: { prompt: "Mô tả cảnh" }, actions: { generate: "Tạo cảnh" }, + mediaReady: "Phương tiện sẵn sàng để nhập", + mediaReadyText: "{type} \"{name}\" sẵn sàng để nhập vào Unity. Xem lại prompt và nhấp \"Tạo cảnh\" để tiếp tục.", + mediaTypeImage: "Hình ảnh", mediaTypeAudio: "Âm thanh", + }, + unityUi: { + title: "Unity UI", subtitle: "Tạo thành phần UI Unity bằng AI.", + fields: { uiSystem: "Hệ thống UI", elementType: "Loại phần tử", prompt: "Mô tả phần tử UI", outputFormat: "Định dạng đầu ra", anchorPreset: "Preset neo", colourTheme: "Chủ đề màu (tùy chọn)", includeAnimations: "Bao gồm hoạt ảnh" }, + actions: { generate: "Tạo UI" }, + }, + unityPhysics: { + title: "Unity Vật lý", subtitle: "Tạo cấu hình vật lý Unity — Rigidbodies, collider, trọng lực và hơn thế.", + fields: { physicsBackend: "Động cơ vật lý", simulationMode: "Chế độ mô phỏng", gravityPreset: "Preset trọng lực", prompt: "Mô tả vật lý", includeRigidbody: "Bao gồm cấu hình Rigidbody", includeColliders: "Bao gồm cấu hình collider", includeLayers: "Bao gồm lớp vật lý" }, + actions: { generate: "Tạo cấu hình vật lý" }, + quickActions: "Hành động nhanh", + }, + unityProject: { + title: "Dự án Unity", subtitle: "Tạo cấu trúc dự án Unity hoàn chỉnh.", + engineSettings: "Cài đặt động cơ Unity", + fields: { projectName: "Tên dự án", unityTemplate: "Template Unity", unityVersion: "Phiên bản Unity", targetPlatform: "Nền tảng mục tiêu", generateDefaultScene: "Tạo cảnh mặc định", autoInstallPackages: "Tự động cài đặt gói UPM", setupUrp: "Cấu hình URP", upmPackages: "Gói UPM (phân cách bằng dấu phẩy)", sceneName: "Tên cảnh", unityEditorPath: "Đường dẫn Unity Editor (tùy chọn)", timeout: "Thời gian chờ (giây)", resultJson: "Kết quả (JSON)" }, + actions: { generate: "Tạo dự án", finalize: "Hoàn thiện dự án", openFolder: "Mở thư mục đầu ra", addVersion: "Thêm phiên bản Unity", downloadZip: "Tải dự án hoàn thiện (.zip)" }, + dialogs: { addVersion: { title: "Thêm phiên bản Unity", versionId: "ID phiên bản", label: "Nhãn (tùy chọn)" } }, + }, + theme: { light: "Sáng", dark: "Tối", system: "Hệ thống" }, + languages: { + en: "Tiếng Anh", es: "Tiếng Tây Ban Nha", ca: "Tiếng Catalan", eu: "Tiếng Basque", + oc: "Tiếng Occitan", uk: "Tiếng Ukraina", pt: "Tiếng Bồ Đào Nha", gl: "Tiếng Galicia", + fr: "Tiếng Pháp", it: "Tiếng Ý", pl: "Tiếng Ba Lan", zh: "Tiếng Trung", + ar: "Tiếng Ả Rập", de: "Tiếng Đức", hi: "Tiếng Hindi", bn: "Tiếng Bengali", + ur: "Tiếng Urdu", id: "Tiếng Indonesia", ja: "Tiếng Nhật", vi: "Tiếng Việt", ko: "Tiếng Hàn", + tr: "Tiếng Thổ Nhĩ Kỳ", + }, +} as const; diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 8f3b627b..6a228f53 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -1,246 +1,254 @@ -/** - * Chinese Simplified (zh) locale. - */ -export default { - app: { - title: "Unity Generator", - status: { online: "在线", offline: "离线" }, - nav: { - settings: "设置", - scenes: "场景", - code: "代码", - text: "文本", - image: "图像", - sprites: "精灵", - audio: "音频", - unityUi: "Unity UI", - unityPhysics: "Unity 物理", - unityProject: "Unity 项目", - }, - actions: { repository: "仓库" }, - }, - common: { - loading: "加载中…", - saving: "保存中…", - save: "保存", - cancel: "取消", - close: "关闭", - ok: "确定", - yes: "是", - no: "否", - error: "错误", - warning: "警告", - success: "成功", - generate: "生成", - result: "结果", - provider: "提供商", - model: "模型", - temperature: "温度", - apiKey: "API 密钥(可选覆盖)", - systemPrompt: "系统提示词覆盖", - advancedOptions: "高级选项", - examplePrompts: "示例提示词", - manageModels: "管理模型", - selectProvider: "选择提供商", - selectModel: "选择模型", - prompt: "提示词", - leaveEmptyForGlobalKey: "留空以使用全局密钥", - createdFiles: "已创建文件", - viewSteps: "查看步骤({n})", - waitingForLogs: "等待日志…", - errorsDetected: "检测到错误", - }, - settings: { - title: "配置", - subtitle: "管理您的智能引擎、模型和系统提示词。", - tabs: { - general: "常规", - providers: "提供商", - models: "模型", - prompts: "提示词", - secrets: "密钥", - }, - }, - general: { - title: "常规设置", - sections: { - networkApi: "网络与 API", - preferredIntelligence: "首选智能", - appearance: "外观", - language: "语言", - }, - fields: { - backendUrl: "后端 URL", - backendUrlHint: "您的 Unity Generator 后端服务地址", - outputBasePath: "基础路径(输出)", - outputBasePathHint: "生成文件的保存位置", - unityEditorPath: "Unity 编辑器路径(可选)", - unityEditorPathHint: "Unity 编辑器可执行文件的完整路径", - preferredLlm: "默认文本/逻辑提供商", - preferredLlmModel: "默认文本/逻辑模型", - preferredImage: "默认图像生成提供商", - preferredImageModel: "默认图像生成模型", - preferredAudio: "默认语音(TTS)提供商", - preferredAudioModel: "默认语音(TTS)模型", - preferredMusic: "默认音乐提供商", - preferredMusicModel: "默认音乐模型", - theme: "主题", - language: "界面语言", - }, - actions: { saveAll: "保存所有更改" }, - status: { - saving: "保存中…", - saved: "设置已成功保存。", - modelNotRegistered: '模型"{model}"未在提供商"{provider}"中注册。', - networkError: "网络错误:无法连接到后端。", - }, - }, - code: { - title: "Unity C# 代码", - subtitle: "使用 AI 生成 Unity C# 脚本。", - fields: { prompt: "提示词", maxTokens: "最大 Token 数" }, - actions: { generate: "生成代码" }, - activeProject: "活动项目:{name}", - autoSave: "自动保存到项目", - }, - text: { - title: "文本生成", - subtitle: "使用 AI 生成文本内容。", - fields: { prompt: "提示词", length: "长度" }, - actions: { generate: "生成文本" }, - }, - image: { - title: "图像生成", - subtitle: "使用 AI 生成图像。", - fields: { - prompt: "提示词", - aspectRatio: "宽高比", - quality: "质量", - textureName: "纹理名称", - textureType: "纹理类型", - }, - actions: { generate: "生成图像", saveToUnity: "保存到 Unity" }, - }, - sprites: { - title: "2D 精灵", - badge: "像素艺术优化", - subtitle: "为 Unity 生成 2D 精灵资源。", - fields: { - prompt: "提示词", - resolution: "分辨率", - paletteSize: "调色板大小", - autoCrop: "自动裁剪透明边缘", - colors: "{n} 种颜色", - }, - preview: "预览区域", - actions: { generate: "生成精灵" }, - }, - audio: { - title: "音频生成", - subtitle: "使用 AI 生成音频资源。", - generationType: "生成类型", - speechTts: "语音(TTS)", - atmosphericMusic: "氛围音乐", - fields: { - prompt: "提示词", - musicDescription: "音乐描述", - speechPrompt: "语音提示词", - voiceOptional: "声音(可选)", - musicModel: "音乐模型", - stability: "稳定性", - audioClipName: "音频片段名称", - audioFormat: "音频格式", - }, - actions: { generate: "生成音频", saveToUnity: "保存到 Unity" }, - }, - scenes: { - title: "场景创建器", - subtitle: "使用 AI 生成 Unity 场景。", - fields: { prompt: "场景描述" }, - actions: { generate: "创建场景" }, - mediaReady: "媒体已准备好导入", - mediaReadyText: '{type} "{name}" 已准备好导入 Unity。请检查提示词并点击"创建场景"继续。', - mediaTypeImage: "图像", - mediaTypeAudio: "音频", - }, - unityUi: { - title: "Unity UI", - subtitle: "使用 AI 生成 Unity UI 组件。", - fields: { - uiSystem: "UI 系统", - elementType: "元素类型", - prompt: "UI 元素描述", - outputFormat: "输出格式", - anchorPreset: "锚点预设", - colourTheme: "颜色主题(可选)", - includeAnimations: "包含动画", - }, - actions: { generate: "生成 UI" }, - }, - unityPhysics: { - title: "Unity 物理", - subtitle: "生成 Unity 物理配置——刚体、碰撞体、重力等。", - fields: { - physicsBackend: "物理引擎", - simulationMode: "模拟模式", - gravityPreset: "重力预设", - prompt: "物理描述", - includeRigidbody: "包含刚体配置", - includeColliders: "包含碰撞体配置", - includeLayers: "包含物理层", - }, - actions: { generate: "生成物理配置" }, - quickActions: "快速操作", - }, - unityProject: { - title: "Unity 项目", - subtitle: "生成完整的 Unity 项目结构。", - engineSettings: "Unity 引擎设置", - fields: { - projectName: "项目名称", - unityTemplate: "Unity 模板", - unityVersion: "Unity 版本", - targetPlatform: "目标平台", - generateDefaultScene: "生成默认场景", - autoInstallPackages: "自动安装 UPM 包", - setupUrp: "配置 URP", - upmPackages: "UPM 包(逗号分隔)", - sceneName: "场景名称", - unityEditorPath: "Unity 编辑器路径(可选)", - timeout: "超时(秒)", - resultJson: "结果(JSON)", - }, - actions: { - generate: "生成项目", - finalize: "完成项目", - openFolder: "打开输出文件夹", - addVersion: "添加 Unity 版本", - downloadZip: "下载已完成项目(.zip)", - }, - dialogs: { - addVersion: { - title: "添加 Unity 版本", - versionId: "版本 ID", - label: "标签(可选)", - }, - }, - }, - theme: { light: "浅色", dark: "深色", system: "系统" }, - languages: { - en: "英语", - es: "西班牙语", - ca: "加泰罗尼亚语", - eu: "巴斯克语", - oc: "奥克语", - uk: "乌克兰语", - pt: "葡萄牙语", - gl: "加利西亚语", - fr: "法语", - it: "意大利语", - pl: "波兰语", - zh: "中文", - ar: "阿拉伯语", - de: "德语", - tr: "土耳其语", - }, -} as const; +/** + * Chinese Simplified (zh) locale. + */ +export default { + app: { + title: "Unity Generator", + status: { online: "在线", offline: "离线" }, + nav: { + settings: "设置", + scenes: "场景", + code: "代码", + text: "文本", + image: "图像", + sprites: "精灵", + audio: "音频", + wizard: "Wizard", + video: "Video", + unityMcp: "Unity MCP", + unityLighting: "Unity Lighting", + unityCamera: "Unity Camera", + unityMaterials: "Unity Materials", + unityAnimations: "Unity Animations", + unityUi: "Unity UI", + unityPhysics: "Unity 物理", + unityProject: "Unity 项目", + logs: "Logs", + }, + actions: { repository: "仓库" }, + }, + common: { + loading: "加载中…", + saving: "保存中…", + save: "保存", + cancel: "取消", + close: "关闭", + ok: "确定", + yes: "是", + no: "否", + error: "错误", + warning: "警告", + success: "成功", + generate: "生成", + result: "结果", + provider: "提供商", + model: "模型", + temperature: "温度", + apiKey: "API 密钥(可选覆盖)", + systemPrompt: "系统提示词覆盖", + advancedOptions: "高级选项", + examplePrompts: "示例提示词", + manageModels: "管理模型", + selectProvider: "选择提供商", + selectModel: "选择模型", + prompt: "提示词", + leaveEmptyForGlobalKey: "留空以使用全局密钥", + createdFiles: "已创建文件", + viewSteps: "查看步骤({n})", + waitingForLogs: "等待日志…", + errorsDetected: "检测到错误", + }, + settings: { + title: "配置", + subtitle: "管理您的智能引擎、模型和系统提示词。", + tabs: { + general: "常规", + providers: "提供商", + models: "模型", + prompts: "提示词", + secrets: "密钥", + }, + }, + general: { + title: "常规设置", + sections: { + networkApi: "网络与 API", + preferredIntelligence: "首选智能", + appearance: "外观", + language: "语言", + }, + fields: { + backendUrl: "后端 URL", + backendUrlHint: "您的 Unity Generator 后端服务地址", + outputBasePath: "基础路径(输出)", + outputBasePathHint: "生成文件的保存位置", + unityEditorPath: "Unity 编辑器路径(可选)", + unityEditorPathHint: "Unity 编辑器可执行文件的完整路径", + preferredLlm: "默认文本/逻辑提供商", + preferredLlmModel: "默认文本/逻辑模型", + preferredImage: "默认图像生成提供商", + preferredImageModel: "默认图像生成模型", + preferredAudio: "默认语音(TTS)提供商", + preferredAudioModel: "默认语音(TTS)模型", + preferredMusic: "默认音乐提供商", + preferredMusicModel: "默认音乐模型", + theme: "主题", + language: "界面语言", + }, + actions: { saveAll: "保存所有更改" }, + status: { + saving: "保存中…", + saved: "设置已成功保存。", + modelNotRegistered: '模型"{model}"未在提供商"{provider}"中注册。', + networkError: "网络错误:无法连接到后端。", + }, + }, + code: { + title: "Unity C# 代码", + subtitle: "使用 AI 生成 Unity C# 脚本。", + fields: { prompt: "提示词", maxTokens: "最大 Token 数" }, + actions: { generate: "生成代码" }, + activeProject: "活动项目:{name}", + autoSave: "自动保存到项目", + }, + text: { + title: "文本生成", + subtitle: "使用 AI 生成文本内容。", + fields: { prompt: "提示词", length: "长度" }, + actions: { generate: "生成文本" }, + }, + image: { + title: "图像生成", + subtitle: "使用 AI 生成图像。", + fields: { + prompt: "提示词", + aspectRatio: "宽高比", + quality: "质量", + textureName: "纹理名称", + textureType: "纹理类型", + }, + actions: { generate: "生成图像", saveToUnity: "保存到 Unity" }, + }, + sprites: { + title: "2D 精灵", + badge: "像素艺术优化", + subtitle: "为 Unity 生成 2D 精灵资源。", + fields: { + prompt: "提示词", + resolution: "分辨率", + paletteSize: "调色板大小", + autoCrop: "自动裁剪透明边缘", + colors: "{n} 种颜色", + }, + preview: "预览区域", + actions: { generate: "生成精灵" }, + }, + audio: { + title: "音频生成", + subtitle: "使用 AI 生成音频资源。", + generationType: "生成类型", + speechTts: "语音(TTS)", + atmosphericMusic: "氛围音乐", + fields: { + prompt: "提示词", + musicDescription: "音乐描述", + speechPrompt: "语音提示词", + voiceOptional: "声音(可选)", + musicModel: "音乐模型", + stability: "稳定性", + audioClipName: "音频片段名称", + audioFormat: "音频格式", + }, + actions: { generate: "生成音频", saveToUnity: "保存到 Unity" }, + }, + scenes: { + title: "场景创建器", + subtitle: "使用 AI 生成 Unity 场景。", + fields: { prompt: "场景描述" }, + actions: { generate: "创建场景" }, + mediaReady: "媒体已准备好导入", + mediaReadyText: '{type} "{name}" 已准备好导入 Unity。请检查提示词并点击"创建场景"继续。', + mediaTypeImage: "图像", + mediaTypeAudio: "音频", + }, + unityUi: { + title: "Unity UI", + subtitle: "使用 AI 生成 Unity UI 组件。", + fields: { + uiSystem: "UI 系统", + elementType: "元素类型", + prompt: "UI 元素描述", + outputFormat: "输出格式", + anchorPreset: "锚点预设", + colourTheme: "颜色主题(可选)", + includeAnimations: "包含动画", + }, + actions: { generate: "生成 UI" }, + }, + unityPhysics: { + title: "Unity 物理", + subtitle: "生成 Unity 物理配置——刚体、碰撞体、重力等。", + fields: { + physicsBackend: "物理引擎", + simulationMode: "模拟模式", + gravityPreset: "重力预设", + prompt: "物理描述", + includeRigidbody: "包含刚体配置", + includeColliders: "包含碰撞体配置", + includeLayers: "包含物理层", + }, + actions: { generate: "生成物理配置" }, + quickActions: "快速操作", + }, + unityProject: { + title: "Unity 项目", + subtitle: "生成完整的 Unity 项目结构。", + engineSettings: "Unity 引擎设置", + fields: { + projectName: "项目名称", + unityTemplate: "Unity 模板", + unityVersion: "Unity 版本", + targetPlatform: "目标平台", + generateDefaultScene: "生成默认场景", + autoInstallPackages: "自动安装 UPM 包", + setupUrp: "配置 URP", + upmPackages: "UPM 包(逗号分隔)", + sceneName: "场景名称", + unityEditorPath: "Unity 编辑器路径(可选)", + timeout: "超时(秒)", + resultJson: "结果(JSON)", + }, + actions: { + generate: "生成项目", + finalize: "完成项目", + openFolder: "打开输出文件夹", + addVersion: "添加 Unity 版本", + downloadZip: "下载已完成项目(.zip)", + }, + dialogs: { + addVersion: { + title: "添加 Unity 版本", + versionId: "版本 ID", + label: "标签(可选)", + }, + }, + }, + theme: { light: "浅色", dark: "深色", system: "系统" }, + languages: { + en: "英语", + es: "西班牙语", + ca: "加泰罗尼亚语", + eu: "巴斯克语", + oc: "奥克语", + uk: "乌克兰语", + pt: "葡萄牙语", + gl: "加利西亚语", + fr: "法语", + it: "意大利语", + pl: "波兰语", + zh: "中文", + ar: "阿拉伯语", + de: "德语", + tr: "土耳其语", + }, +} as const; diff --git a/frontend/src/store/intelligenceStore.ts b/frontend/src/store/intelligenceStore.ts index 7d7e5888..60d0f863 100644 --- a/frontend/src/store/intelligenceStore.ts +++ b/frontend/src/store/intelligenceStore.ts @@ -1,78 +1,80 @@ -import { defineStore } from "pinia"; -import { type ProviderCapabilities, type ModelEntry, getAllConfig } from "@/api/client"; - -export const useIntelligenceStore = defineStore("intelligence", { - state: () => ({ - providers: [] as ProviderCapabilities[], - models: {} as Record, - prompts: {} as Record, - keys: {} as Record, - preferences: {} as Record, - isLoaded: false, - loading: false, - error: null as string | null, - }), - - getters: { - getProvidersByModality: (state) => (modality: string) => { - return state.providers.filter((p) => p.modalities.includes(modality)); - }, - getModelsByProvider: (state) => (provider: string, modality?: string) => { - const providerModels = state.models[provider] || []; - if (!modality) return providerModels; - return providerModels.filter((m) => m.modality === modality); - }, - getPreference: (state) => (key: string) => { - return state.preferences[key] || ""; - }, - /** - * Preferred engine (provider + model) for a modality, with correct-on-read: - * if the stored model is not in the provider's list, returns the same provider - * with the first available model or empty string. - */ - getPreferredEngine: (state) => (modality: string): { provider: string; model: string } => { - const provider = (state.preferences[`preferred_${modality}_provider`] || "").trim(); - const model = (state.preferences[`preferred_${modality}_model`] || "").trim(); - if (!provider) return { provider: "", model: "" }; - const providerModels = state.models[provider] || []; - const allowed = providerModels.filter((m) => m.modality === modality); - const valid = allowed.some((m) => m.value === model); - if (valid) return { provider, model }; - return { provider, model: allowed.length ? allowed[0].value : "" }; - }, - isKeyConfigured: (state) => (serviceName: string) => { - return !!state.keys[serviceName]; - } - }, - - actions: { - async load(force = false) { - if (this.isLoaded && !force) return; - - this.loading = true; - this.error = null; - try { - const data = await getAllConfig(); - this.providers = data.providers || []; - this.models = (data.models as Record) || {}; - this.prompts = (data.prompts as Record) || {}; - this.keys = (data.keys as Record) || {}; - this.preferences = (data.preferences as Record) || {}; - this.isLoaded = true; - } catch (e: unknown) { - this.error = (e as Error).message || "Failed to load intelligence configuration"; - console.error("IntelligenceStore load failed:", e); - } finally { - this.loading = false; - } - }, - - async refresh() { - return this.load(true); - }, - - setPreference(key: string, value: string) { - this.preferences[key] = value; - } - }, -}); +import { defineStore } from "pinia"; +import { type ProviderCapabilities, type ModelEntry, getAllConfig } from "@/api/client"; + +export const useIntelligenceStore = defineStore("intelligence", { + state: () => ({ + providers: [] as ProviderCapabilities[], + models: {} as Record, + prompts: {} as Record, + keys: {} as Record, + preferences: {} as Record, + unityToolNames: [] as string[], + isLoaded: false, + loading: false, + error: null as string | null, + }), + + getters: { + getProvidersByModality: (state) => (modality: string) => { + return state.providers.filter((p) => p.modalities.includes(modality)); + }, + getModelsByProvider: (state) => (provider: string, modality?: string) => { + const providerModels = state.models[provider] || []; + if (!modality) return providerModels; + return providerModels.filter((m) => m.modality === modality); + }, + getPreference: (state) => (key: string) => { + return state.preferences[key] || ""; + }, + /** + * Preferred engine (provider + model) for a modality, with correct-on-read: + * if the stored model is not in the provider's list, returns the same provider + * with the first available model or empty string. + */ + getPreferredEngine: (state) => (modality: string): { provider: string; model: string } => { + const provider = (state.preferences[`preferred_${modality}_provider`] || "").trim(); + const model = (state.preferences[`preferred_${modality}_model`] || "").trim(); + if (!provider) return { provider: "", model: "" }; + const providerModels = state.models[provider] || []; + const allowed = providerModels.filter((m) => m.modality === modality); + const valid = allowed.some((m) => m.value === model); + if (valid) return { provider, model }; + return { provider, model: allowed.length ? allowed[0].value : "" }; + }, + isKeyConfigured: (state) => (serviceName: string) => { + return !!state.keys[serviceName]; + } + }, + + actions: { + async load(force = false) { + if (this.isLoaded && !force) return; + + this.loading = true; + this.error = null; + try { + const data = await getAllConfig(); + this.providers = data.providers || []; + this.models = (data.models as Record) || {}; + this.prompts = (data.prompts as Record) || {}; + this.keys = (data.keys as Record) || {}; + this.preferences = (data.preferences as Record) || {}; + this.unityToolNames = (data.unity_tool_names as string[]) || []; + this.isLoaded = true; + } catch (e: unknown) { + this.error = (e as Error).message || "Failed to load intelligence configuration"; + console.error("IntelligenceStore load failed:", e); + } finally { + this.loading = false; + } + }, + + async refresh() { + return this.load(true); + }, + + setPreference(key: string, value: string) { + this.preferences[key] = value; + } + }, +}); diff --git a/frontend/src/store/unityProjectUiStore.ts b/frontend/src/store/unityProjectUiStore.ts index 9fde6d4a..cd82c5e0 100644 --- a/frontend/src/store/unityProjectUiStore.ts +++ b/frontend/src/store/unityProjectUiStore.ts @@ -1,235 +1,238 @@ -import { defineStore } from "pinia"; - -export type UnityProjectUiSettings = { - // Unity Engine Settings - installPackages: boolean; - generateScene: boolean; - setupUrp: boolean; - packages: string; - sceneName: string; - editorPath: string; - timeout: number; - template: string; - version: string; - platform: string; -}; - -export type UnityProjectUiState = { - /** - * Last known status line shown to the user (banner). - */ - status: string | null; - - /** - * Whether the status should be presented as OK or ERROR. - */ - tone: "ok" | "error"; - - /** - * Raw JSON result shown in the UI. - */ - result: string; - - /** - * Last project path produced by "Generate Base Project" or finalize. - * Used as the default for "Open Folder" and finalize requests. - */ - lastProjectPath: string; - - /** - * Unity project generation/finalize settings. - */ - settings: UnityProjectUiSettings; -}; - -const STORAGE_KEY = "unity_generator_unity_project_ui_v1"; -let autoPersistEnabled = false; - -function getDefaultState(): UnityProjectUiState { - return { - status: null, - tone: "ok", - result: "", - lastProjectPath: "", - settings: { - installPackages: false, - generateScene: false, - setupUrp: false, - packages: "com.unity.textmeshpro", - sceneName: "MainScene", - editorPath: "", - timeout: 300, - template: "", - version: "", - platform: "", - }, - }; -} - -/** - * Load the persisted Unity Project UI state from localStorage. - * - * @returns Persisted state if available and valid; otherwise default state. - * - * @example - * ```ts - * const store = useUnityProjectUiStore(); - * store.hydrateFromStorage(); - * ``` - */ -function loadFromStorage(): UnityProjectUiState { - if (typeof localStorage === "undefined" || !localStorage.getItem) { - return getDefaultState(); - } - - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return getDefaultState(); - - try { - const parsed = JSON.parse(raw) as Partial | null; - if (!parsed || typeof parsed !== "object") return getDefaultState(); - - const defaults = getDefaultState(); - const parsedSettings = parsed.settings; - - return { - ...defaults, - ...parsed, - settings: { - ...defaults.settings, - ...(parsedSettings && typeof parsedSettings === "object" ? parsedSettings : {}), - }, - }; - } catch { - return getDefaultState(); - } -} - -/** - * Quick runtime sanity check that the storage layer is writable. - * If this returns false in Electron, the app cannot persist UI state. - */ -function canWriteStorage(): boolean { - if (typeof localStorage === "undefined" || !localStorage.setItem) return false; - try { - const k = `${STORAGE_KEY}__probe`; - localStorage.setItem(k, "1"); - localStorage.removeItem?.(k); - return true; - } catch { - return false; - } -} - -/** - * Persist the Unity Project UI state to localStorage. - * - * @param state - Current UI state to persist. - * @returns True if state was written, false otherwise. - * @throws {Error} If state is not an object. - * - * @example - * ```ts - * const store = useUnityProjectUiStore(); - * store.persistToStorage(); - * ``` - */ -function saveToStorage(state: UnityProjectUiState): boolean { - if (!state || typeof state !== "object") { - throw new Error("state must be a valid UnityProjectUiState object"); - } - if (typeof localStorage === "undefined" || !localStorage.setItem) return false; - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); - return true; - } catch { - return false; - } -} - -export const useUnityProjectUiStore = defineStore("unityProjectUi", { - state: (): UnityProjectUiState => loadFromStorage(), - actions: { - /** - * Reset the Unity Project panel UI state to defaults and persist it. - * - * @example - * ```ts - * const store = useUnityProjectUiStore(); - * store.reset(); - * ``` - */ - reset(): void { - const next = getDefaultState(); - // IMPORTANT: do not replace `settings` object to keep Vue bindings stable - this.status = next.status; - this.tone = next.tone; - this.result = next.result; - this.lastProjectPath = next.lastProjectPath; - Object.assign(this.settings, next.settings); - - saveToStorage(this.$state); - }, - - /** - * Persist the current in-memory state to localStorage. - * - * @example - * ```ts - * const store = useUnityProjectUiStore(); - * store.persistToStorage(); - * ``` - */ - persistToStorage(): void { - saveToStorage(this.$state); - }, - - /** - * Enable automatic persistence for this store instance. - * - * This is meant to be called once (e.g. from App.vue) so persistence does not - * depend on whether a specific panel component is currently mounted. - * - * @example - * ```ts - * const store = useUnityProjectUiStore(); - * store.enableAutoPersist(); - * ``` - */ - enableAutoPersist(): void { - if (autoPersistEnabled) return; - autoPersistEnabled = true; - if (!canWriteStorage()) { - // Avoid spamming errors; persistence won't work in this environment. - return; - } - this.$subscribe(() => { - try { - saveToStorage(this.$state); - } catch { - // Best-effort persistence - } - }); - }, - - /** - * Re-hydrate the store from localStorage (overwrites current state). - * - * @example - * ```ts - * const store = useUnityProjectUiStore(); - * store.hydrateFromStorage(); - * ``` - */ - hydrateFromStorage(): void { - const next = loadFromStorage(); - // IMPORTANT: do not replace `settings` object to keep Vue bindings stable - this.status = next.status; - this.tone = next.tone; - this.result = next.result; - this.lastProjectPath = next.lastProjectPath; - Object.assign(this.settings, next.settings); - }, - }, -}); - +import { defineStore } from "pinia"; + +export type UnityProjectUiSettings = { + // Unity Engine Settings + installPackages: boolean; + generateScene: boolean; + setupUrp: boolean; + packages: string; + sceneName: string; + editorPath: string; + timeout: number; + template: string; + version: string; + platform: string; + /** Finalize backend: auto prefers MCP when unity-mcp is configured. */ + automationMode: "auto" | "mcp" | "batch"; +}; + +export type UnityProjectUiState = { + /** + * Last known status line shown to the user (banner). + */ + status: string | null; + + /** + * Whether the status should be presented as OK or ERROR. + */ + tone: "ok" | "error"; + + /** + * Raw JSON result shown in the UI. + */ + result: string; + + /** + * Last project path produced by "Generate Base Project" or finalize. + * Used as the default for "Open Folder" and finalize requests. + */ + lastProjectPath: string; + + /** + * Unity project generation/finalize settings. + */ + settings: UnityProjectUiSettings; +}; + +const STORAGE_KEY = "unity_generator_unity_project_ui_v1"; +let autoPersistEnabled = false; + +function getDefaultState(): UnityProjectUiState { + return { + status: null, + tone: "ok", + result: "", + lastProjectPath: "", + settings: { + installPackages: false, + generateScene: false, + setupUrp: false, + packages: "com.unity.textmeshpro", + sceneName: "MainScene", + editorPath: "", + timeout: 300, + template: "", + version: "", + platform: "", + automationMode: "mcp", + }, + }; +} + +/** + * Load the persisted Unity Project UI state from localStorage. + * + * @returns Persisted state if available and valid; otherwise default state. + * + * @example + * ```ts + * const store = useUnityProjectUiStore(); + * store.hydrateFromStorage(); + * ``` + */ +function loadFromStorage(): UnityProjectUiState { + if (typeof localStorage === "undefined" || !localStorage.getItem) { + return getDefaultState(); + } + + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return getDefaultState(); + + try { + const parsed = JSON.parse(raw) as Partial | null; + if (!parsed || typeof parsed !== "object") return getDefaultState(); + + const defaults = getDefaultState(); + const parsedSettings = parsed.settings; + + return { + ...defaults, + ...parsed, + settings: { + ...defaults.settings, + ...(parsedSettings && typeof parsedSettings === "object" ? parsedSettings : {}), + }, + }; + } catch { + return getDefaultState(); + } +} + +/** + * Quick runtime sanity check that the storage layer is writable. + * If this returns false in Electron, the app cannot persist UI state. + */ +function canWriteStorage(): boolean { + if (typeof localStorage === "undefined" || !localStorage.setItem) return false; + try { + const k = `${STORAGE_KEY}__probe`; + localStorage.setItem(k, "1"); + localStorage.removeItem?.(k); + return true; + } catch { + return false; + } +} + +/** + * Persist the Unity Project UI state to localStorage. + * + * @param state - Current UI state to persist. + * @returns True if state was written, false otherwise. + * @throws {Error} If state is not an object. + * + * @example + * ```ts + * const store = useUnityProjectUiStore(); + * store.persistToStorage(); + * ``` + */ +function saveToStorage(state: UnityProjectUiState): boolean { + if (!state || typeof state !== "object") { + throw new Error("state must be a valid UnityProjectUiState object"); + } + if (typeof localStorage === "undefined" || !localStorage.setItem) return false; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + return true; + } catch { + return false; + } +} + +export const useUnityProjectUiStore = defineStore("unityProjectUi", { + state: (): UnityProjectUiState => loadFromStorage(), + actions: { + /** + * Reset the Unity Project panel UI state to defaults and persist it. + * + * @example + * ```ts + * const store = useUnityProjectUiStore(); + * store.reset(); + * ``` + */ + reset(): void { + const next = getDefaultState(); + // IMPORTANT: do not replace `settings` object to keep Vue bindings stable + this.status = next.status; + this.tone = next.tone; + this.result = next.result; + this.lastProjectPath = next.lastProjectPath; + Object.assign(this.settings, next.settings); + + saveToStorage(this.$state); + }, + + /** + * Persist the current in-memory state to localStorage. + * + * @example + * ```ts + * const store = useUnityProjectUiStore(); + * store.persistToStorage(); + * ``` + */ + persistToStorage(): void { + saveToStorage(this.$state); + }, + + /** + * Enable automatic persistence for this store instance. + * + * This is meant to be called once (e.g. from App.vue) so persistence does not + * depend on whether a specific panel component is currently mounted. + * + * @example + * ```ts + * const store = useUnityProjectUiStore(); + * store.enableAutoPersist(); + * ``` + */ + enableAutoPersist(): void { + if (autoPersistEnabled) return; + autoPersistEnabled = true; + if (!canWriteStorage()) { + // Avoid spamming errors; persistence won't work in this environment. + return; + } + this.$subscribe(() => { + try { + saveToStorage(this.$state); + } catch { + // Best-effort persistence + } + }); + }, + + /** + * Re-hydrate the store from localStorage (overwrites current state). + * + * @example + * ```ts + * const store = useUnityProjectUiStore(); + * store.hydrateFromStorage(); + * ``` + */ + hydrateFromStorage(): void { + const next = loadFromStorage(); + // IMPORTANT: do not replace `settings` object to keep Vue bindings stable + this.status = next.status; + this.tone = next.tone; + this.result = next.result; + this.lastProjectPath = next.lastProjectPath; + Object.assign(this.settings, next.settings); + }, + }, +}); + diff --git a/frontend/src/tests/integration/imageToUnity.spec.ts b/frontend/src/tests/integration/imageToUnity.spec.ts index dfd0aff5..8afce178 100644 --- a/frontend/src/tests/integration/imageToUnity.spec.ts +++ b/frontend/src/tests/integration/imageToUnity.spec.ts @@ -1,669 +1,668 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { mount, flushPromises } from "@vue/test-utils"; -import { createPinia, setActivePinia } from "pinia"; -import { createVuetify } from "vuetify"; -import ImagePanel from "@/components/ImagePanel/ImagePanel.vue"; -import ScenesPanel from "@/components/ScenesPanel.vue"; -import { useMediaImport } from "@/composables/useMediaImport"; -import * as client from "@/api/client"; -import { useIntelligenceStore } from "@/store/intelligenceStore"; -import i18n from "@/i18n"; - -/** - * Integration test suite for Image-to-Unity workflow. - * - * Tests the complete end-to-end workflow: - * 1. User generates an image in ImagePanel - * 2. User clicks "Save to Unity Project" button - * 3. Navigation to ScenesPanel with media data - * 4. ScenesPanel receives and displays media import info - * 5. User submits prompt with media data included - * 6. Media data is cleared after successful import - * - * **Validates Requirements: 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11** - * - * **Property 6: Media Navigation Preserves Data** - * **Property 7: Media Navigation Pre-fills Prompt** - * **Property 8: Query Parameters Populate Component State** - * **Property 9: Pending Media Import Included in Request** - * **Property 10: Successful Import Clears Pending State** - */ - -// Mock API client -vi.mock("@/api/client"); - -// Mock useApp for tab navigation -const mockSetActive = vi.fn(); -vi.mock("@/App", () => ({ - useApp: () => ({ - setActive: mockSetActive - }) -})); - -// Mock session project -vi.mock("@/composables/useSessionProject", () => ({ - useSessionProject: () => ({ - projectName: { value: "TestProject" } - }) -})); - -// Mock project store -vi.mock("@/store/projectStore", () => ({ - projectStore: { - activeProjectName: "TestProject" - } -})); - -describe("Image-to-Unity Integration Workflow", () => { - let vuetify: ReturnType; - - beforeEach(() => { - vi.clearAllMocks(); - setActivePinia(createPinia()); - vuetify = createVuetify(); - - // Clear media import state - const { clearPendingMediaImport } = useMediaImport(); - clearPendingMediaImport(); - - // Mock API preferences - vi.spyOn(client, "getPref").mockResolvedValue({ - success: true, - date: new Date().toISOString(), - error: null, - data: { key: "test", value: null }, - }); - - // Setup intelligence store with mock data - const store = useIntelligenceStore(); - store.$patch(state => { - state.providers = [ - { - name: "openai", - api_key_name: null, - base_url: null, - openai_compatible: false, - requires_api_key: false, - supports_vision: false, - supports_streaming: false, - supports_function_calling: false, - supports_tool_use: false, - modalities: ["text", "image"], - default_models: {}, - extra: {} - } - ]; - state.models = { - openai: [ - { value: "dall-e-3", label: "DALL-E 3", modality: "image" }, - { value: "gpt-4", label: "GPT-4", modality: "text" } - ] - }; - state.preferences = { - preferred_image_provider: "openai", - preferred_image_model: "dall-e-3", - preferred_text_provider: "openai", - preferred_text_model: "gpt-4" - }; - }); - store.load = vi.fn().mockResolvedValue(undefined); - }); - - /** - * Test 1: Complete workflow from image generation to Unity import - * - * Validates the entire user journey: - * - Generate image in ImagePanel - * - Configure texture settings - * - Click "Save to Unity Project" - * - Navigate to ScenesPanel - * - Verify media data and prompt are preserved - * - Submit scene generation with media - * - Verify media is cleared after success - */ - it("should complete full workflow: generate image → save to Unity → import in Unity", async () => { - // ===== STEP 1: Generate image in ImagePanel ===== - const testImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; - - vi.spyOn(client, "generateImage").mockResolvedValue({ - success: true, - date: new Date().toISOString(), - error: null, - data: { url: testImageData }, - }); - - const imagePanel = mount(ImagePanel, { - global: { - plugins: [vuetify, i18n] - } - }); - - await flushPromises(); - - // Set prompt and generate image - const imagePanelFields = imagePanel.findAllComponents({ name: "SmartField" }); - const promptField = imagePanelFields.find(f => f.props("label") === "Prompt"); - expect(promptField).toBeDefined(); - - promptField!.vm.$emit("update:modelValue", "A red cube texture"); - await flushPromises(); - - const generateBtn = imagePanel.findAll("button").find(b => - b.text().toLowerCase().includes("generate") - ); - expect(generateBtn).toBeDefined(); - - await generateBtn!.trigger("click"); - await flushPromises(); - - // Verify image was generated - const imagePanelInstance = imagePanel.vm as any; - expect(imagePanelInstance.generatedImage).toBe(testImageData); - - // ===== STEP 2: Configure texture settings ===== - // Update texture settings directly on the component instance - imagePanelInstance.textureNameInput = "CubeTexture"; - imagePanelInstance.textureTypeSelect = "Default"; - await flushPromises(); - - // ===== STEP 3: Click "Save to Unity Project" ===== - const saveToUnityBtn = imagePanel.findAll("button").find(b => - b.text().toLowerCase().includes("save to unity") - ); - expect(saveToUnityBtn).toBeDefined(); - - await saveToUnityBtn!.trigger("click"); - await flushPromises(); - - // ===== STEP 4: Verify navigation was triggered ===== - expect(mockSetActive).toHaveBeenCalledWith("Scenes"); - - // ===== STEP 5: Verify media import state was set ===== - const { pendingMediaImport, pendingPrompt, hasPendingMediaImport } = useMediaImport(); - - expect(hasPendingMediaImport()).toBe(true); - expect(pendingMediaImport.value).not.toBeNull(); - expect(pendingMediaImport.value?.data).toBe(testImageData); - expect(pendingMediaImport.value?.name).toBe("CubeTexture"); - expect(pendingMediaImport.value?.type).toBe("image"); - expect(pendingMediaImport.value?.textureType).toBe("Default"); - expect(pendingPrompt.value).toContain("CubeTexture"); - expect(pendingPrompt.value).toContain("Default"); - expect(pendingPrompt.value).toContain("Unity texture"); - - // ===== STEP 6: Mount ScenesPanel and verify it receives media data ===== - vi.spyOn(client, "createScene").mockResolvedValue({ - success: true, - date: new Date().toISOString(), - error: null, - data: { - content: "Scene generated successfully with texture imported", - files: ["Assets/Textures/CubeTexture.png", "Assets/Scenes/MainScene.unity"] - } - }); - - const scenesPanel = mount(ScenesPanel, { - global: { - plugins: [vuetify, i18n] - } - }); - - await flushPromises(); - - // Verify ScenesPanel received the media import data - const scenesPanelInstance = scenesPanel.vm as any; - expect(scenesPanelInstance.pendingMediaImport).not.toBeNull(); - expect(scenesPanelInstance.prompt).toBe(pendingPrompt.value); - - // ===== STEP 7: Verify info banner is displayed ===== - // The banner uses v-alert component, not a simple type attribute - const infoBanner = scenesPanel.findComponent({ name: 'VAlert' }); - expect(infoBanner.exists()).toBe(true); - expect(infoBanner.text()).toContain("Ready to import"); - expect(infoBanner.text()).toContain("image"); - - // ===== STEP 8: Submit scene generation with media ===== - const sceneGenerateBtn = scenesPanel.findAll("button").find(b => - b.text().toLowerCase().includes("create scene") - ); - expect(sceneGenerateBtn).toBeDefined(); - - await sceneGenerateBtn!.trigger("click"); - await flushPromises(); - - // ===== STEP 9: Verify API was called with media data ===== - expect(client.createScene).toHaveBeenCalledWith( - expect.objectContaining({ - prompt: expect.stringContaining("CubeTexture"), - media_import: expect.objectContaining({ - data: testImageData, - name: "CubeTexture", - type: "image", - texture_type: "Default" // Note: API uses snake_case - }) - }) - ); - - // ===== STEP 10: Verify media import was cleared after success ===== - await flushPromises(); - expect(hasPendingMediaImport()).toBe(false); - expect(pendingMediaImport.value).toBeNull(); - }); - - /** - * Test 2: Verify navigation with query parameters - * - * Tests that media data is correctly passed through the navigation system - * and that all required parameters are preserved. - * - * **Validates Requirements: 4.5, 4.6, 4.7** - * **Property 6: Media Navigation Preserves Data** - */ - it("should preserve all media data during navigation", async () => { - const testImageData = "data:image/png;base64,testdata123"; - const textureName = "PlayerSprite"; - const textureType = "Sprite"; - - // Simulate ImagePanel setting media import - const { setPendingMediaImport, pendingMediaImport } = useMediaImport(); - - const unityPrompt = `Import this generated image as a Unity texture named "${textureName}" with ${textureType} texture type, and create a GameObject with SpriteRenderer component displaying it at position (0, 0, 0)`; - - setPendingMediaImport( - { - data: testImageData, - name: textureName, - type: "image", - textureType: textureType - }, - unityPrompt - ); - - // Verify all data is preserved - expect(pendingMediaImport.value).not.toBeNull(); - expect(pendingMediaImport.value?.data).toBe(testImageData); - expect(pendingMediaImport.value?.name).toBe(textureName); - expect(pendingMediaImport.value?.type).toBe("image"); - expect(pendingMediaImport.value?.textureType).toBe(textureType); - }); - - /** - * Test 3: Verify prompt pre-fill with Unity import instructions - * - * Tests that the prompt is correctly pre-filled with instructions - * that include texture name, type, and Unity-specific commands. - * - * **Validates Requirements: 4.8, 4.9** - * **Property 7: Media Navigation Pre-fills Prompt** - */ - it("should pre-fill prompt with Unity import instructions", async () => { - const testImageData = "data:image/png;base64,testdata"; - const textureName = "GroundTexture"; - const textureType = "Normal"; - - const { setPendingMediaImport, pendingPrompt } = useMediaImport(); - - const unityPrompt = `Import this generated image as a Unity texture named "${textureName}" with ${textureType} texture type, and create a GameObject with SpriteRenderer component displaying it at position (0, 0, 0)`; - - setPendingMediaImport( - { - data: testImageData, - name: textureName, - type: "image", - textureType: textureType - }, - unityPrompt - ); - - // Verify prompt contains all required elements - expect(pendingPrompt.value).toContain(textureName); - expect(pendingPrompt.value).toContain(textureType); - expect(pendingPrompt.value).toContain("Unity texture"); - expect(pendingPrompt.value).toContain("GameObject"); - expect(pendingPrompt.value).toContain("SpriteRenderer"); - }); - - /** - * Test 4: Verify ScenesPanel populates state from media import - * - * Tests that ScenesPanel correctly reads and displays media import data - * when mounted with pending media. - * - * **Validates Requirements: 4.10, 6.2, 6.3** - * **Property 8: Query Parameters Populate Component State** - */ - it("should populate ScenesPanel state from media import data", async () => { - const testImageData = "data:image/png;base64,testdata"; - const textureName = "UITexture"; - const textureType = "UI"; - - // Set up media import before mounting ScenesPanel - const { setPendingMediaImport } = useMediaImport(); - - const unityPrompt = `Import this generated image as a Unity texture named "${textureName}" with ${textureType} texture type, and create a GameObject with SpriteRenderer component displaying it at position (0, 0, 0)`; - - setPendingMediaImport( - { - data: testImageData, - name: textureName, - type: "image", - textureType: textureType - }, - unityPrompt - ); - - // Mount ScenesPanel - const scenesPanel = mount(ScenesPanel, { - global: { - plugins: [vuetify, i18n] - } - }); - - await flushPromises(); - - // Verify component state - const scenesPanelInstance = scenesPanel.vm as any; - expect(scenesPanelInstance.prompt).toBe(unityPrompt); - expect(scenesPanelInstance.pendingMediaImport).not.toBeNull(); - expect(scenesPanelInstance.pendingMediaImport.data).toBe(testImageData); - expect(scenesPanelInstance.pendingMediaImport.name).toBe(textureName); - expect(scenesPanelInstance.pendingMediaImport.type).toBe("image"); - expect(scenesPanelInstance.pendingMediaImport.textureType).toBe(textureType); - - // Verify info banner is displayed - const infoBanner = scenesPanel.findComponent({ name: 'VAlert' }); - expect(infoBanner.exists()).toBe(true); - expect(infoBanner.text()).toContain("Ready to import"); - }); - - /** - * Test 5: Verify media data is included in scene generation request - * - * Tests that when ScenesPanel submits a scene generation request with - * pending media, the media data is correctly included in the API payload. - * - * **Validates Requirements: 4.11, 6.5** - * **Property 9: Pending Media Import Included in Request** - */ - it("should include media data in scene generation request", async () => { - const testImageData = "data:image/png;base64,testdata"; - const textureName = "CharacterTexture"; - const textureType = "Sprite"; - - // Set up media import - const { setPendingMediaImport } = useMediaImport(); - - const unityPrompt = `Import this generated image as a Unity texture named "${textureName}" with ${textureType} texture type, and create a GameObject with SpriteRenderer component displaying it at position (0, 0, 0)`; - - setPendingMediaImport( - { - data: testImageData, - name: textureName, - type: "image", - textureType: textureType - }, - unityPrompt - ); - - // Mock successful API response - vi.spyOn(client, "createScene").mockResolvedValue({ - success: true, - date: new Date().toISOString(), - error: null, - data: { - content: "Scene generated with texture", - files: ["Assets/Textures/CharacterTexture.png"] - } - }); - - // Mount ScenesPanel and trigger generation - const scenesPanel = mount(ScenesPanel, { - global: { - plugins: [vuetify, i18n] - } - }); - - await flushPromises(); - - const generateBtn = scenesPanel.findAll("button").find(b => - b.text().toLowerCase().includes("create scene") - ); - - await generateBtn!.trigger("click"); - await flushPromises(); - - // Verify API was called with media_import - expect(client.createScene).toHaveBeenCalledWith( - expect.objectContaining({ - media_import: expect.objectContaining({ - data: testImageData, - name: textureName, - type: "image", - texture_type: textureType // Note: API uses snake_case - }) - }) - ); - }); - - /** - * Test 6: Verify media import is cleared after successful generation - * - * Tests that pending media import state is cleared after a successful - * scene generation, preventing duplicate imports. - * - * **Validates Requirements: 6.6** - * **Property 10: Successful Import Clears Pending State** - */ - it("should clear pending media import after successful generation", async () => { - const testImageData = "data:image/png;base64,testdata"; - - // Set up media import - const { setPendingMediaImport, hasPendingMediaImport } = useMediaImport(); - - setPendingMediaImport( - { - data: testImageData, - name: "TestTexture", - type: "image", - textureType: "Default" - }, - "Import this texture" - ); - - expect(hasPendingMediaImport()).toBe(true); - - // Mock successful API response - vi.spyOn(client, "createScene").mockResolvedValue({ - success: true, - date: new Date().toISOString(), - error: null, - data: { - content: "Scene generated", - files: [] - } - }); - - // Mount ScenesPanel and trigger generation - const scenesPanel = mount(ScenesPanel, { - global: { - plugins: [vuetify, i18n] - } - }); - - await flushPromises(); - - const generateBtn = scenesPanel.findAll("button").find(b => - b.text().toLowerCase().includes("create scene") - ); - - await generateBtn!.trigger("click"); - await flushPromises(); - - // Verify media import was cleared - expect(hasPendingMediaImport()).toBe(false); - }); - - /** - * Test 7: Verify media import is NOT cleared after failed generation - * - * Tests that pending media import state is preserved when scene generation - * fails, allowing the user to retry without re-importing the media. - */ - it("should NOT clear pending media import after failed generation", async () => { - const testImageData = "data:image/png;base64,testdata"; - - // Set up media import - const { setPendingMediaImport, hasPendingMediaImport } = useMediaImport(); - - setPendingMediaImport( - { - data: testImageData, - name: "TestTexture", - type: "image", - textureType: "Default" - }, - "Import this texture" - ); - - expect(hasPendingMediaImport()).toBe(true); - - // Mock failed API response - vi.spyOn(client, "createScene").mockResolvedValue({ - success: false, - date: new Date().toISOString(), - error: "Scene generation failed", - data: null - }); - - // Mount ScenesPanel and trigger generation - const scenesPanel = mount(ScenesPanel, { - global: { - plugins: [vuetify, i18n] - } - }); - - await flushPromises(); - - const generateBtn = scenesPanel.findAll("button").find(b => - b.text().toLowerCase().includes("create scene") - ); - - await generateBtn!.trigger("click"); - await flushPromises(); - - // Verify media import was NOT cleared (user can retry) - expect(hasPendingMediaImport()).toBe(true); - }); - - /** - * Test 8: Verify different texture types are preserved - * - * Tests that all texture type options (Default, Sprite, Normal, UI) - * are correctly preserved through the workflow. - */ - it("should preserve different texture types through workflow", async () => { - const textureTypes = ["Default", "Sprite", "Normal", "UI"]; - - for (const textureType of textureTypes) { - const { setPendingMediaImport, pendingMediaImport, clearPendingMediaImport } = useMediaImport(); - - clearPendingMediaImport(); - - setPendingMediaImport( - { - data: "data:image/png;base64,test", - name: `${textureType}Texture`, - type: "image", - textureType: textureType - }, - `Import as ${textureType}` - ); - - expect(pendingMediaImport.value?.textureType).toBe(textureType); - } - }); - - /** - * Test 9: Verify error handling when no image is generated - * - * Tests that attempting to save to Unity without generating an image - * throws an appropriate error. - */ - it("should throw error when saving to Unity without generated image", async () => { - const imagePanel = mount(ImagePanel, { - global: { - plugins: [vuetify, i18n] - } - }); - - await flushPromises(); - - const imagePanelInstance = imagePanel.vm as any; - - // Verify no image is generated - expect(imagePanelInstance.generatedImage).toBeNull(); - - // Attempt to save to Unity should throw error - expect(() => { - imagePanelInstance.saveToUnity(); - }).toThrow("No image available to save to Unity"); - }); - - /** - * Test 10: Verify ScenesPanel works normally without pending media - * - * Tests backward compatibility - ScenesPanel should function normally - * when there is no pending media import. - */ - it("should work normally in ScenesPanel without pending media import", async () => { - // Ensure no pending media - const { clearPendingMediaImport, hasPendingMediaImport } = useMediaImport(); - clearPendingMediaImport(); - expect(hasPendingMediaImport()).toBe(false); - - // Mock successful API response without media - vi.spyOn(client, "createScene").mockResolvedValue({ - success: true, - date: new Date().toISOString(), - error: null, - data: { - content: "Scene generated normally", - files: [] - } - }); - - // Mount ScenesPanel - const scenesPanel = mount(ScenesPanel, { - global: { - plugins: [vuetify, i18n] - } - }); - - await flushPromises(); - - // Verify no info banner is shown - const infoBanner = scenesPanel.findComponent({ name: 'VAlert' }); - expect(infoBanner.exists()).toBe(false); - - // Set a normal prompt - const scenesPanelInstance = scenesPanel.vm as any; - scenesPanelInstance.prompt = "Create a simple scene with a cube"; - await flushPromises(); - - // Trigger generation - const generateBtn = scenesPanel.findAll("button").find(b => - b.text().toLowerCase().includes("create scene") - ); - - await generateBtn!.trigger("click"); - await flushPromises(); - - // Verify API was called WITHOUT media_import - expect(client.createScene).toHaveBeenCalledWith( - expect.not.objectContaining({ - media_import: expect.anything() - }) - ); - }); -}); - +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mount, flushPromises } from "@vue/test-utils"; +import { createPinia, setActivePinia } from "pinia"; +import { createVuetify } from "vuetify"; +import ImagePanel from "@/components/ImagePanel/ImagePanel.vue"; +import ScenesPanel from "@/components/ScenesPanel.vue"; +import { useMediaImport } from "@/composables/useMediaImport"; +import * as client from "@/api/client"; +import { useIntelligenceStore } from "@/store/intelligenceStore"; +import i18n from "@/i18n"; + +/** + * Integration test suite for Image-to-Unity workflow. + * + * Tests the complete end-to-end workflow: + * 1. User generates an image in ImagePanel + * 2. User clicks "Save to Unity Project" button + * 3. Navigation to ScenesPanel with media data + * 4. ScenesPanel receives and displays media import info + * 5. User submits prompt with media data included + * 6. Media data is cleared after successful import + * + * **Validates Requirements: 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11** + * + * **Property 6: Media Navigation Preserves Data** + * **Property 7: Media Navigation Pre-fills Prompt** + * **Property 8: Query Parameters Populate Component State** + * **Property 9: Pending Media Import Included in Request** + * **Property 10: Successful Import Clears Pending State** + */ + +// Mock API client +vi.mock("@/api/client"); + +// Mock useApp for tab navigation +const mockSetActive = vi.fn(); +vi.mock("@/App", () => ({ + useApp: () => ({ + setActive: mockSetActive + }) +})); + +// Mock session project +vi.mock("@/composables/useSessionProject", () => ({ + useSessionProject: () => ({ + projectName: { value: "TestProject" } + }) +})); + +// Mock project store +vi.mock("@/store/projectStore", () => ({ + projectStore: { + activeProjectName: "TestProject" + } +})); + +describe("Image-to-Unity Integration Workflow", () => { + let vuetify: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + setActivePinia(createPinia()); + vuetify = createVuetify(); + + // Clear media import state + const { clearPendingMediaImport } = useMediaImport(); + clearPendingMediaImport(); + + // Mock API preferences + vi.spyOn(client, "getPref").mockResolvedValue({ + success: true, + date: new Date().toISOString(), + error: null, + data: { key: "test", value: null }, + }); + + // Setup intelligence store with mock data + const store = useIntelligenceStore(); + store.$patch(state => { + state.providers = [ + { + name: "openai", + api_key_name: null, + base_url: null, + openai_compatible: false, + requires_api_key: false, + supports_vision: false, + supports_streaming: false, + supports_function_calling: false, + supports_tool_use: false, + modalities: ["text", "image"], + default_models: {}, + extra: {} + } + ]; + state.models = { + openai: [ + { value: "dall-e-3", label: "DALL-E 3", modality: "image" }, + { value: "gpt-4", label: "GPT-4", modality: "text" } + ] + }; + state.preferences = { + preferred_image_provider: "openai", + preferred_image_model: "dall-e-3", + preferred_text_provider: "openai", + preferred_text_model: "gpt-4" + }; + }); + store.load = vi.fn().mockResolvedValue(undefined); + }); + + /** + * Test 1: Complete workflow from image generation to Unity import + * + * Validates the entire user journey: + * - Generate image in ImagePanel + * - Configure texture settings + * - Click "Save to Unity Project" + * - Navigate to ScenesPanel + * - Verify media data and prompt are preserved + * - Submit scene generation with media + * - Verify media is cleared after success + */ + it("should complete full workflow: generate image → save to Unity → import in Unity", async () => { + // ===== STEP 1: Generate image in ImagePanel ===== + const testImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; + + vi.spyOn(client, "generateImage").mockResolvedValue({ + success: true, + date: new Date().toISOString(), + error: null, + data: { url: testImageData }, + }); + + const imagePanel = mount(ImagePanel, { + global: { + plugins: [vuetify, i18n] + } + }); + + await flushPromises(); + + // Set prompt and generate image + const imagePanelFields = imagePanel.findAllComponents({ name: "SmartField" }); + const promptField = imagePanelFields.find(f => f.props("label") === "Prompt"); + expect(promptField).toBeDefined(); + + promptField!.vm.$emit("update:modelValue", "A red cube texture"); + await flushPromises(); + + const generateBtn = imagePanel.findAll("button").find(b => + b.text().toLowerCase().includes("generate") + ); + expect(generateBtn).toBeDefined(); + + await generateBtn!.trigger("click"); + await flushPromises(); + + // Verify image was generated + const imagePanelInstance = imagePanel.vm as any; + expect(imagePanelInstance.generatedImage).toBe(testImageData); + + // ===== STEP 2: Configure texture settings ===== + // Update texture settings directly on the component instance + imagePanelInstance.textureNameInput = "CubeTexture"; + imagePanelInstance.textureTypeSelect = "Default"; + await flushPromises(); + + // ===== STEP 3: Click "Save to Unity Project" ===== + const saveToUnityBtn = imagePanel.findAll("button").find(b => + b.text().toLowerCase().includes("save to unity") + ); + expect(saveToUnityBtn).toBeDefined(); + + await saveToUnityBtn!.trigger("click"); + await flushPromises(); + + // ===== STEP 4: Verify navigation was triggered ===== + expect(mockSetActive).toHaveBeenCalledWith("Scenes"); + + // ===== STEP 5: Verify media import state was set ===== + const { pendingMediaImport, pendingPrompt, hasPendingMediaImport } = useMediaImport(); + + expect(hasPendingMediaImport()).toBe(true); + expect(pendingMediaImport.value).not.toBeNull(); + expect(pendingMediaImport.value?.data).toBe(testImageData); + expect(pendingMediaImport.value?.name).toBe("CubeTexture"); + expect(pendingMediaImport.value?.type).toBe("image"); + expect(pendingMediaImport.value?.textureType).toBe("Default"); + expect(pendingPrompt.value).toContain("CubeTexture"); + expect(pendingPrompt.value).toContain("Default"); + expect(pendingPrompt.value).toContain("Unity texture"); + + // ===== STEP 6: Mount ScenesPanel and verify it receives media data ===== + vi.spyOn(client, "createScene").mockResolvedValue({ + success: true, + date: new Date().toISOString(), + error: null, + data: { + content: "Scene generated successfully with texture imported", + files: ["Assets/Textures/CubeTexture.png", "Assets/Scenes/MainScene.unity"] + } + }); + + const scenesPanel = mount(ScenesPanel, { + global: { + plugins: [vuetify, i18n] + } + }); + + await flushPromises(); + + // Verify ScenesPanel received the media import data + const scenesPanelInstance = scenesPanel.vm as any; + expect(scenesPanelInstance.pendingMediaImport).not.toBeNull(); + expect(scenesPanelInstance.prompt).toBe(pendingPrompt.value); + + // ===== STEP 7: Verify info banner is displayed ===== + // The banner uses v-alert component, not a simple type attribute + const infoBanner = scenesPanel.findComponent({ name: 'VAlert' }); + expect(infoBanner.exists()).toBe(true); + expect(infoBanner.text()).toContain("Ready to import"); + expect(infoBanner.text()).toContain("image"); + + // ===== STEP 8: Submit scene generation with media ===== + const sceneGenerateBtn = scenesPanel.findAll("button").find(b => + b.text().toLowerCase().includes("create scene") + ); + expect(sceneGenerateBtn).toBeDefined(); + + await sceneGenerateBtn!.trigger("click"); + await flushPromises(); + + // ===== STEP 9: Verify API was called with media data ===== + expect(client.createScene).toHaveBeenCalledWith( + expect.objectContaining({ + prompt: expect.stringContaining("CubeTexture"), + media_import: expect.objectContaining({ + data: testImageData, + name: "CubeTexture", + type: "image", + texture_type: "Default" // Note: API uses snake_case + }) + }) + ); + + // ===== STEP 10: Verify media import was cleared after success ===== + await flushPromises(); + expect(hasPendingMediaImport()).toBe(false); + expect(pendingMediaImport.value).toBeNull(); + }); + + /** + * Test 2: Verify navigation with query parameters + * + * Tests that media data is correctly passed through the navigation system + * and that all required parameters are preserved. + * + * **Validates Requirements: 4.5, 4.6, 4.7** + * **Property 6: Media Navigation Preserves Data** + */ + it("should preserve all media data during navigation", async () => { + const testImageData = "data:image/png;base64,testdata123"; + const textureName = "PlayerSprite"; + const textureType = "Sprite"; + + // Simulate ImagePanel setting media import + const { setPendingMediaImport, pendingMediaImport } = useMediaImport(); + + const unityPrompt = `Import this generated image as a Unity texture named "${textureName}" with ${textureType} texture type, and create a GameObject with SpriteRenderer component displaying it at position (0, 0, 0)`; + + setPendingMediaImport( + { + data: testImageData, + name: textureName, + type: "image", + textureType: textureType + }, + unityPrompt + ); + + // Verify all data is preserved + expect(pendingMediaImport.value).not.toBeNull(); + expect(pendingMediaImport.value?.data).toBe(testImageData); + expect(pendingMediaImport.value?.name).toBe(textureName); + expect(pendingMediaImport.value?.type).toBe("image"); + expect(pendingMediaImport.value?.textureType).toBe(textureType); + }); + + /** + * Test 3: Verify prompt pre-fill with Unity import instructions + * + * Tests that the prompt is correctly pre-filled with instructions + * that include texture name, type, and Unity-specific commands. + * + * **Validates Requirements: 4.8, 4.9** + * **Property 7: Media Navigation Pre-fills Prompt** + */ + it("should pre-fill prompt with Unity import instructions", async () => { + const testImageData = "data:image/png;base64,testdata"; + const textureName = "GroundTexture"; + const textureType = "Normal"; + + const { setPendingMediaImport, pendingPrompt } = useMediaImport(); + + const unityPrompt = `Import this generated image as a Unity texture named "${textureName}" with ${textureType} texture type, and create a GameObject with SpriteRenderer component displaying it at position (0, 0, 0)`; + + setPendingMediaImport( + { + data: testImageData, + name: textureName, + type: "image", + textureType: textureType + }, + unityPrompt + ); + + // Verify prompt contains all required elements + expect(pendingPrompt.value).toContain(textureName); + expect(pendingPrompt.value).toContain(textureType); + expect(pendingPrompt.value).toContain("Unity texture"); + expect(pendingPrompt.value).toContain("GameObject"); + expect(pendingPrompt.value).toContain("SpriteRenderer"); + }); + + /** + * Test 4: Verify ScenesPanel populates state from media import + * + * Tests that ScenesPanel correctly reads and displays media import data + * when mounted with pending media. + * + * **Validates Requirements: 4.10, 6.2, 6.3** + * **Property 8: Query Parameters Populate Component State** + */ + it("should populate ScenesPanel state from media import data", async () => { + const testImageData = "data:image/png;base64,testdata"; + const textureName = "UITexture"; + const textureType = "UI"; + + // Set up media import before mounting ScenesPanel + const { setPendingMediaImport } = useMediaImport(); + + const unityPrompt = `Import this generated image as a Unity texture named "${textureName}" with ${textureType} texture type, and create a GameObject with SpriteRenderer component displaying it at position (0, 0, 0)`; + + setPendingMediaImport( + { + data: testImageData, + name: textureName, + type: "image", + textureType: textureType + }, + unityPrompt + ); + + // Mount ScenesPanel + const scenesPanel = mount(ScenesPanel, { + global: { + plugins: [vuetify, i18n] + } + }); + + await flushPromises(); + + // Verify component state + const scenesPanelInstance = scenesPanel.vm as any; + expect(scenesPanelInstance.prompt).toBe(unityPrompt); + expect(scenesPanelInstance.pendingMediaImport).not.toBeNull(); + expect(scenesPanelInstance.pendingMediaImport.data).toBe(testImageData); + expect(scenesPanelInstance.pendingMediaImport.name).toBe(textureName); + expect(scenesPanelInstance.pendingMediaImport.type).toBe("image"); + expect(scenesPanelInstance.pendingMediaImport.textureType).toBe(textureType); + + // Verify info banner is displayed + const infoBanner = scenesPanel.findComponent({ name: 'VAlert' }); + expect(infoBanner.exists()).toBe(true); + expect(infoBanner.text()).toContain("Ready to import"); + }); + + /** + * Test 5: Verify media data is included in scene generation request + * + * Tests that when ScenesPanel submits a scene generation request with + * pending media, the media data is correctly included in the API payload. + * + * **Validates Requirements: 4.11, 6.5** + * **Property 9: Pending Media Import Included in Request** + */ + it("should include media data in scene generation request", async () => { + const testImageData = "data:image/png;base64,testdata"; + const textureName = "CharacterTexture"; + const textureType = "Sprite"; + + // Set up media import + const { setPendingMediaImport } = useMediaImport(); + + const unityPrompt = `Import this generated image as a Unity texture named "${textureName}" with ${textureType} texture type, and create a GameObject with SpriteRenderer component displaying it at position (0, 0, 0)`; + + setPendingMediaImport( + { + data: testImageData, + name: textureName, + type: "image", + textureType: textureType + }, + unityPrompt + ); + + // Mock successful API response + vi.spyOn(client, "createScene").mockResolvedValue({ + success: true, + date: new Date().toISOString(), + error: null, + data: { + content: "Scene generated with texture", + files: ["Assets/Textures/CharacterTexture.png"] + } + }); + + // Mount ScenesPanel and trigger generation + const scenesPanel = mount(ScenesPanel, { + global: { + plugins: [vuetify, i18n] + } + }); + + await flushPromises(); + + const generateBtn = scenesPanel.findAll("button").find(b => + b.text().toLowerCase().includes("create scene") + ); + + await generateBtn!.trigger("click"); + await flushPromises(); + + // Verify API was called with media_import + expect(client.createScene).toHaveBeenCalledWith( + expect.objectContaining({ + media_import: expect.objectContaining({ + data: testImageData, + name: textureName, + type: "image", + texture_type: textureType // Note: API uses snake_case + }) + }) + ); + }); + + /** + * Test 6: Verify media import is cleared after successful generation + * + * Tests that pending media import state is cleared after a successful + * scene generation, preventing duplicate imports. + * + * **Validates Requirements: 6.6** + * **Property 10: Successful Import Clears Pending State** + */ + it("should clear pending media import after successful generation", async () => { + const testImageData = "data:image/png;base64,testdata"; + + // Set up media import + const { setPendingMediaImport, hasPendingMediaImport } = useMediaImport(); + + setPendingMediaImport( + { + data: testImageData, + name: "TestTexture", + type: "image", + textureType: "Default" + }, + "Import this texture" + ); + + expect(hasPendingMediaImport()).toBe(true); + + // Mock successful API response + vi.spyOn(client, "createScene").mockResolvedValue({ + success: true, + date: new Date().toISOString(), + error: null, + data: { + content: "Scene generated", + files: [] + } + }); + + // Mount ScenesPanel and trigger generation + const scenesPanel = mount(ScenesPanel, { + global: { + plugins: [vuetify, i18n] + } + }); + + await flushPromises(); + + const generateBtn = scenesPanel.findAll("button").find(b => + b.text().toLowerCase().includes("create scene") + ); + + await generateBtn!.trigger("click"); + await flushPromises(); + + // Verify media import was cleared + expect(hasPendingMediaImport()).toBe(false); + }); + + /** + * Test 7: Verify media import is NOT cleared after failed generation + * + * Tests that pending media import state is preserved when scene generation + * fails, allowing the user to retry without re-importing the media. + */ + it("should NOT clear pending media import after failed generation", async () => { + const testImageData = "data:image/png;base64,testdata"; + + // Set up media import + const { setPendingMediaImport, hasPendingMediaImport } = useMediaImport(); + + setPendingMediaImport( + { + data: testImageData, + name: "TestTexture", + type: "image", + textureType: "Default" + }, + "Import this texture" + ); + + expect(hasPendingMediaImport()).toBe(true); + + // Mock failed API response + vi.spyOn(client, "createScene").mockResolvedValue({ + success: false, + date: new Date().toISOString(), + error: "Scene generation failed", + data: null + }); + + // Mount ScenesPanel and trigger generation + const scenesPanel = mount(ScenesPanel, { + global: { + plugins: [vuetify, i18n] + } + }); + + await flushPromises(); + + const generateBtn = scenesPanel.findAll("button").find(b => + b.text().toLowerCase().includes("create scene") + ); + + await generateBtn!.trigger("click"); + await flushPromises(); + + // Verify media import was NOT cleared (user can retry) + expect(hasPendingMediaImport()).toBe(true); + }); + + /** + * Test 8: Verify different texture types are preserved + * + * Tests that all texture type options (Default, Sprite, Normal, UI) + * are correctly preserved through the workflow. + */ + it("should preserve different texture types through workflow", async () => { + const textureTypes = ["Default", "Sprite", "Normal", "UI"]; + + for (const textureType of textureTypes) { + const { setPendingMediaImport, pendingMediaImport, clearPendingMediaImport } = useMediaImport(); + + clearPendingMediaImport(); + + setPendingMediaImport( + { + data: "data:image/png;base64,test", + name: `${textureType}Texture`, + type: "image", + textureType: textureType + }, + `Import as ${textureType}` + ); + + expect(pendingMediaImport.value?.textureType).toBe(textureType); + } + }); + + /** + * Test 9: Verify error handling when no image is generated + * + * Tests that attempting to save to Unity without generating an image + * throws an appropriate error. + */ + it("should throw error when saving to Unity without generated image", async () => { + const imagePanel = mount(ImagePanel, { + global: { + plugins: [vuetify, i18n] + } + }); + + await flushPromises(); + + const imagePanelInstance = imagePanel.vm as any; + + // Verify no image is generated + expect(imagePanelInstance.generatedImage).toBeNull(); + + // Attempt to save to Unity should throw error + expect(() => { + imagePanelInstance.saveToUnity(); + }).toThrow("No image available to save to Unity"); + }); + + /** + * Test 10: Verify ScenesPanel works normally without pending media + * + * Tests backward compatibility - ScenesPanel should function normally + * when there is no pending media import. + */ + it("should work normally in ScenesPanel without pending media import", async () => { + // Ensure no pending media + const { clearPendingMediaImport, hasPendingMediaImport } = useMediaImport(); + clearPendingMediaImport(); + expect(hasPendingMediaImport()).toBe(false); + + // Mock successful API response without media + vi.spyOn(client, "createScene").mockResolvedValue({ + success: true, + date: new Date().toISOString(), + error: null, + data: { + content: "Scene generated normally", + files: [] + } + }); + + // Mount ScenesPanel + const scenesPanel = mount(ScenesPanel, { + global: { + plugins: [vuetify, i18n] + } + }); + + await flushPromises(); + + // Verify no media import banner is shown (MCP status alerts may still appear) + expect(scenesPanel.text()).not.toContain('Media Ready to Import'); + + // Set a normal prompt + const scenesPanelInstance = scenesPanel.vm as any; + scenesPanelInstance.prompt = "Create a simple scene with a cube"; + await flushPromises(); + + // Trigger generation + const generateBtn = scenesPanel.findAll("button").find(b => + b.text().toLowerCase().includes("create scene") + ); + + await generateBtn!.trigger("click"); + await flushPromises(); + + // Verify API was called WITHOUT media_import + expect(client.createScene).toHaveBeenCalledWith( + expect.not.objectContaining({ + media_import: expect.anything() + }) + ); + }); +}); + From 860476e69527e92a90667fb879f951be9db9215f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreu=20Mart=C3=ADnez=20Vidal?= Date: Mon, 17 Aug 2026 15:52:55 +0200 Subject: [PATCH 7/7] "version": "2.0.0" Enhance Unity agent scene management and tool invocation Added functionality to ensure scene files are created when necessary by introducing the `ensure_scene_file_if_missing` method. Updated the Unity agent to provide guidance on scene creation order and modified the handling of modality keys to include specific behavior for scene-related requests. Refactored the `InjectingUnityMcpPlugin` to track invoked tools and updated argument injection logic to conditionally include project paths. Enhanced tests to cover new scene management features and tool invocation behavior. --- backend/app/agents/unity_agent.py | 35 ++- backend/app/agents/unity_auto_invoke.py | 136 +++++++++ backend/app/agents/unity_mcp_plugin.py | 6 +- .../agents/unity_tool_argument_injection.py | 25 +- backend/app/services/unity_mcp_sdk_client.py | 258 ++++++++++++++++++ backend/package.json | 2 +- backend/tests/test_unity_mcp_pluginwrapper.py | 13 +- backend/tests/test_unity_mcp_sdk_client.py | 55 ++++ backend/tests/test_unity_orchestration.py | 65 ++++- frontend/package.json | 2 +- 10 files changed, 581 insertions(+), 16 deletions(-) create mode 100644 backend/app/services/unity_mcp_sdk_client.py create mode 100644 backend/tests/test_unity_mcp_sdk_client.py diff --git a/backend/app/agents/unity_agent.py b/backend/app/agents/unity_agent.py index 21bf3d40..18b74b63 100644 --- a/backend/app/agents/unity_agent.py +++ b/backend/app/agents/unity_agent.py @@ -20,7 +20,7 @@ from ..services.providers import provider_registry from ..services.providers.capabilities import Modality -from .unity_auto_invoke import auto_invoke_csharp_blocks +from .unity_auto_invoke import auto_invoke_csharp_blocks, ensure_scene_file_if_missing from .unity_mcp_plugin import ( InjectingUnityMcpPlugin, create_unity_mcp_plugin, @@ -212,6 +212,15 @@ def _build_enhanced_capabilities_guidance(self) -> str: When user requests animations, cutscenes, or prototypes, use these tools proactively. """ + _SCENES_MODALITY_GUIDANCE = ( + "\nSCENE CREATION ORDER (mandatory for scene requests):\n" + "1. FIRST call unity_create_scene or unity_create_detailed_scene to create the .unity file.\n" + "2. THEN add GameObjects with unity_add_gameobject or unity_create_detailed_scene sceneJson.\n" + "3. THEN create scripts with unity_create_script and attach via unity_attach_script when needed.\n" + "Never create scripts without first creating the target scene file. " + "Prefer unity_create_detailed_scene when the prompt describes multiple objects or a playable layout." + ) + async def run( self, prompt: str, @@ -351,12 +360,15 @@ async def run( "do not invent base64 and do not skip import." ) - full_prompt = f"{system_message}\n\nUser: {prompt}" - modality_key = options.get("modality_key") if isinstance(options, dict) else None max_tokens = options.get("max_tokens", 2000) if isinstance(options, dict) else 2000 - if str(modality_key or "").lower() == "wizard": + modality_lower = str(modality_key or "").lower() + if modality_lower in ("wizard", "scenes"): max_tokens = max(int(max_tokens), 8000) + if modality_lower == "scenes": + system_message += self._SCENES_MODALITY_GUIDANCE + + full_prompt = f"{system_message}\n\nUser: {prompt}" try: # Common settings setup @@ -407,12 +419,25 @@ async def run( project_path=project_path, tools_already_called=injecting_plugin.tools_called, ) + auto_scene: str | None = None + if modality_lower == "scenes": + auto_scene = await ensure_scene_file_if_missing( + injecting_plugin, + project_path=project_path, + tools_invoked=injecting_plugin.tools_invoked, + prompt=prompt, + response_text=content, + ) metadata: dict[str, Any] = {"steps": [], "modality_key": modality_key} + files = list(auto_saved) + if auto_scene: + files.append(auto_scene) + metadata["auto_created_scene"] = auto_scene if auto_saved: metadata["auto_saved_files"] = auto_saved return { "content": content, - "files": auto_saved, + "files": files, "metadata": metadata, } else: diff --git a/backend/app/agents/unity_auto_invoke.py b/backend/app/agents/unity_auto_invoke.py index f7104c48..df990f5a 100644 --- a/backend/app/agents/unity_auto_invoke.py +++ b/backend/app/agents/unity_auto_invoke.py @@ -5,6 +5,7 @@ import logging import re import time +from pathlib import Path from typing import Any LOGGER = logging.getLogger(__name__) @@ -14,6 +15,17 @@ re.IGNORECASE, ) _CLASS_NAME_RE = re.compile(r"\bclass\s+([A-Za-z_][A-Za-z0-9_]*)") +_SCENE_NAME_PATTERNS = ( + re.compile(r"(?:scene|level)\s+(?:named|called)\s+['\"]?([A-Za-z_][\w ]*)['\"]?", re.I), + re.compile(r"([A-Za-z_][\w]*)\.unity\b", re.I), +) +_SCENE_CREATION_MARKERS = ( + "unity_create_scene", + "unity_create_detailed_scene", + "unity_create_default_scene", + "unity_create_prototype_recipe", + "unity_create_core_recipe", +) def extract_csharp_blocks(text: str) -> list[tuple[str, str | None]]: @@ -42,6 +54,130 @@ def extract_csharp_blocks(text: str) -> list[tuple[str, str | None]]: return blocks +def _normalize_tool_name(tool_name: str) -> str: + """Strip Semantic Kernel plugin prefixes from a tool name.""" + if not isinstance(tool_name, str): + return "" + blob = tool_name.strip().lower().replace("-", "_") + if blob.startswith("unitymcpdiscovered_"): + return blob.removeprefix("unitymcpdiscovered_") + for prefix in ("project_", "scene_", "asset_", "validation_", "server_"): + if blob.startswith(prefix): + return blob[len(prefix) :] + return blob + + +def scene_creation_tool_was_called(tools_invoked: set[str]) -> bool: + """ + Return True when any invoked tool creates a Unity scene file. + + Args: + tools_invoked: Raw tool names recorded during an agent turn. + + Returns: + True if a scene-creation MCP tool was invoked. + + Example: + >>> scene_creation_tool_was_called({"project-unity_create_scene"}) + True + """ + if not tools_invoked: + return False + for tool_name in tools_invoked: + normalized = _normalize_tool_name(tool_name) + if any(marker in normalized for marker in _SCENE_CREATION_MARKERS): + return True + return False + + +def infer_scene_file_name(prompt: str, response_text: str = "") -> str: + """ + Infer a project-relative scene path from the user prompt or response. + + Args: + prompt: Original user instruction. + response_text: Optional assistant response to scan for explicit names. + + Returns: + Path such as ``Assets/Scenes/MyScene.unity``. + + Example: + >>> infer_scene_file_name("Create a scene called Arena") + 'Assets/Scenes/Arena.unity' + """ + combined = f"{prompt}\n{response_text}" + for pattern in _SCENE_NAME_PATTERNS: + match = pattern.search(combined) + if match: + raw_name = re.sub(r"\s+", "", match.group(1).strip()) + if raw_name: + base = Path(raw_name).stem + return f"Assets/Scenes/{base}.unity" + + words = re.findall(r"[A-Za-z]{3,}", prompt)[:4] + if words: + base = "".join(word[:1].upper() + word[1:] for word in words) + return f"Assets/Scenes/{base}.unity" + + ts = int(time.time()) + return f"Assets/Scenes/GeneratedScene_{ts}.unity" + + +async def ensure_scene_file_if_missing( + plugin: Any, + *, + project_path: str | None, + tools_invoked: set[str], + prompt: str, + response_text: str = "", +) -> str | None: + """ + Create a baseline scene file when the model saved scripts but skipped scene tools. + + Args: + plugin: Initialized Unity MCP plugin with ``invoke_tool``. + project_path: Unity project root. Required for scene creation. + tools_invoked: Tool names invoked during the agent turn. + prompt: Original user instruction (used to infer scene file name). + response_text: Assistant response (optional name hints). + + Returns: + Created project-relative scene path, or None when skipped or failed. + + Example: + >>> # doctest: +SKIP + >>> await ensure_scene_file_if_missing( + ... plugin, + ... project_path="/proj", + ... tools_invoked={"project-unity_create_script"}, + ... prompt="Create a platformer scene", + ... ) + 'Assets/Scenes/CreateAPlatformerScene.unity' + """ + if scene_creation_tool_was_called(tools_invoked): + return None + if not project_path or not str(project_path).strip(): + return None + if plugin is None or not hasattr(plugin, "invoke_tool"): + return None + + file_name = infer_scene_file_name(prompt, response_text) + try: + from .unity_tool_argument_injection import inject_unity_tool_arguments + + args = inject_unity_tool_arguments( + "unity_create_scene", + {"fileName": file_name}, + project_path=project_path, + ) + await plugin.invoke_tool("unity_create_scene", args) + LOGGER.info("Auto-invoked unity_create_scene for %s (model skipped scene tools)", file_name) + return file_name.replace("\\", "/") + except Exception as exc: + LOGGER.warning("Auto-invoke unity_create_scene failed for %s: %s", file_name, exc) + return None + + async def auto_invoke_csharp_blocks( plugin: Any, response_text: str, diff --git a/backend/app/agents/unity_mcp_plugin.py b/backend/app/agents/unity_mcp_plugin.py index 437119ce..99d1dbb9 100644 --- a/backend/app/agents/unity_mcp_plugin.py +++ b/backend/app/agents/unity_mcp_plugin.py @@ -14,6 +14,7 @@ from unity_mcp import UnityMCPPlugin, UnityMcpOptions from ..core.mcp_config import get_mcp_stdio_config, parse_mcp_args +from ..services.unity_mcp_sdk_client import OfficialMcpSdkClient from .modality_mcp_tool_policy import ( allowed_namespace_plugins, filter_tool_names, @@ -86,6 +87,7 @@ def __init__(self, inner: Any, *, project_path: str | None = None) -> None: self._inner = inner self._project_path = project_path self.tools_called = False + self.tools_invoked: set[str] = set() def __getattr__(self, name: str) -> Any: return getattr(self._inner, name) @@ -109,6 +111,7 @@ async def invoke_tool(self, tool_name: str, arguments: dict[str, Any] | None = N project_path=self._project_path, ) self.tools_called = True + self.tools_invoked.add(tool_name.strip()) return await self._inner.invoke_tool(tool_name, args) @@ -504,7 +507,8 @@ async def create_unity_mcp_plugin(): plugin = None try: options = _build_unity_mcp_options(command, mcp_args) - plugin = UnityMCPPlugin.create(options) + sdk_client = OfficialMcpSdkClient(options) + plugin = UnityMCPPlugin(sdk_client, options) await plugin.initialize() LOGGER.info("UnityMCP plugin created successfully.") try: diff --git a/backend/app/agents/unity_tool_argument_injection.py b/backend/app/agents/unity_tool_argument_injection.py index faa023ec..26f44eba 100644 --- a/backend/app/agents/unity_tool_argument_injection.py +++ b/backend/app/agents/unity_tool_argument_injection.py @@ -9,6 +9,17 @@ _CSHARP_CLASS_RE = re.compile(r"\bclass\s+([A-Za-z_][A-Za-z0-9_]*)") +# Tools that must never receive injected projectPath (they use other parameters only). +_NO_PROJECT_PATH_TOOLS = frozenset( + { + "unity_get_capabilities", + "unity_get_server_info", + "unity_validate_csharp", + "unity_query_documentation", + "unity_query_engine_documentation", + } +) + # Tools that require projectPath (camelCase MCP contract). _PROJECT_PATH_TOOLS = frozenset( name @@ -100,6 +111,15 @@ def _default_file_name(tool_name: str, arguments: dict[str, Any]) -> str | None: return None +def _should_inject_project_path(tool_name: str) -> bool: + """Return True when ``projectPath`` may be injected for this tool.""" + if not tool_name or tool_name == "ping": + return False + if tool_name in _NO_PROJECT_PATH_TOOLS: + return False + return tool_name in _PROJECT_PATH_TOOLS or tool_name.startswith("unity_") + + def inject_unity_tool_arguments( tool_name: str, arguments: dict[str, Any] | None, @@ -134,9 +154,8 @@ def inject_unity_tool_arguments( if project_path and str(project_path).strip(): normalized = Path(project_path.strip()).as_posix() - if tool_name in _PROJECT_PATH_TOOLS or tool_name.startswith("unity_"): - if not args.get("projectPath"): - args["projectPath"] = normalized + if _should_inject_project_path(tool_name) and not args.get("projectPath"): + args["projectPath"] = normalized if tool_name in _FILE_NAME_TOOLS or tool_name.startswith("unity_save_"): if not args.get("fileName"): diff --git a/backend/app/services/unity_mcp_sdk_client.py b/backend/app/services/unity_mcp_sdk_client.py new file mode 100644 index 00000000..6acdcdba --- /dev/null +++ b/backend/app/services/unity_mcp_sdk_client.py @@ -0,0 +1,258 @@ +""" +Official MCP SDK client adapter for unity-mcp-plugin. + +Unity MCP Server 3.2.x speaks the Model Context Protocol ``tools/call`` surface. +The bundled ``StdioMcpClient`` in unity-mcp-plugin still sends legacy JSON-RPC +requests (``method=tool_name``), which the server rejects with +``Method '' is not available`` for most tools. This adapter implements +``IMcpClient`` using the official Python MCP SDK so Semantic Kernel agents can +invoke tools reliably. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +from datetime import datetime, timedelta, timezone +from typing import Any + +from unity_mcp.models import ( + ConnectionState, + McpError, + McpParameterDefinition, + McpResponse, + McpToolDefinition, + UnityMcpOptions, +) + +LOGGER = logging.getLogger(__name__) + +try: + from mcp import ClientSession + from mcp.client.stdio import StdioServerParameters, stdio_client + from mcp.types import TextContent +except ImportError: + ClientSession = None + StdioServerParameters = None + stdio_client = None + TextContent = None + + +def _parse_tool_content(result: Any) -> Any: + """ + Parse MCP tool call content blocks into a Python value. + + Args: + result: MCP ``CallToolResult`` from the SDK. + + Returns: + Parsed JSON value, concatenated text, or None when empty. + """ + if result is None or TextContent is None: + return None + + chunks: list[str] = [] + for block in getattr(result, "content", []) or []: + if isinstance(block, TextContent) and block.text: + chunks.append(block.text) + + if not chunks: + return None + + combined = "".join(chunks).strip() + if not combined: + return None + + try: + return json.loads(combined) + except json.JSONDecodeError: + return combined + + +def _map_sdk_tool(tool: Any) -> McpToolDefinition: + """ + Convert an MCP SDK tool descriptor to ``McpToolDefinition``. + + Args: + tool: Tool object from ``session.list_tools()``. + + Returns: + Plugin-compatible tool definition. + """ + schema = getattr(tool, "inputSchema", None) or {} + if not isinstance(schema, dict): + schema = {} + props = schema.get("properties", {}) + if not isinstance(props, dict): + props = {} + required = set(schema.get("required", []) or []) + + parameters: dict[str, McpParameterDefinition] = {} + for name, definition in props.items(): + if not isinstance(definition, dict): + continue + parameters[name] = McpParameterDefinition( + name=name, + type=str(definition.get("type", "string")), + description=definition.get("description"), + required=name in required, + default_value=definition.get("default"), + ) + + return McpToolDefinition( + name=str(getattr(tool, "name", "") or ""), + description=str(getattr(tool, "description", "") or ""), + parameters=parameters, + ) + + +class OfficialMcpSdkClient: + """ + MCP client that uses the official Python MCP SDK over stdio. + + Implements the ``IMcpClient`` protocol expected by ``UnityMCPPlugin``. + """ + + def __init__(self, options: UnityMcpOptions) -> None: + if options is None: + raise ValueError("options is required") + options.validate() + self._options = options + self._state = ConnectionState.DISCONNECTED + self._lock = asyncio.Lock() + self._last_success = datetime.min.replace(tzinfo=timezone.utc) + self._stdio_context: Any = None + self._session_context: Any = None + self._session: Any = None + + @property + def state(self) -> ConnectionState: + """Current connection lifecycle state.""" + return self._state + + async def connect(self, cancellation_token: Any = None) -> None: + """Start unity-mcp via stdio and initialize an MCP session.""" + if ClientSession is None or stdio_client is None or StdioServerParameters is None: + raise RuntimeError("MCP Python SDK is not installed") + + async with self._lock: + if self._session is not None: + return + + self._state = ConnectionState.CONNECTING + params = StdioServerParameters( + command=self._options.executable_path, + args=list(self._options.args or []), + ) + try: + self._stdio_context = stdio_client(params) + read_stream, write_stream = await self._stdio_context.__aenter__() + self._session_context = ClientSession(read_stream, write_stream) + self._session = await self._session_context.__aenter__() + await asyncio.wait_for( + self._session.initialize(), + timeout=self._options.connection_timeout_seconds, + ) + self._state = ConnectionState.CONNECTED + self._last_success = datetime.now(timezone.utc) + LOGGER.info("Connected to Unity MCP server via official MCP SDK") + except Exception as exc: + await self._cleanup_unlocked() + self._state = ConnectionState.FAULTED + raise RuntimeError(f"Failed to connect to Unity MCP server: {exc}") from exc + + async def list_tools(self, cancellation_token: Any = None) -> list[McpToolDefinition]: + """Discover tools exposed by the connected MCP server.""" + await self._ensure_connected() + assert self._session is not None + response = await asyncio.wait_for( + self._session.list_tools(), + timeout=self._options.request_timeout_seconds, + ) + tools = [_map_sdk_tool(tool) for tool in response.tools if getattr(tool, "name", "")] + LOGGER.info("Discovered %d Unity MCP tools via official SDK", len(tools)) + return tools + + async def invoke_tool( + self, + tool_name: str, + parameters: dict[str, Any], + cancellation_token: Any = None, + ) -> McpResponse: + """ + Invoke a Unity MCP tool using ``tools/call``. + + Args: + tool_name: Registered MCP tool name. + parameters: Tool arguments. + + Returns: + Plugin-compatible ``McpResponse``. + """ + if not isinstance(tool_name, str) or not tool_name.strip(): + raise ValueError("tool_name must be a non-empty string") + if not isinstance(parameters, dict): + raise TypeError("parameters must be a dictionary") + + await self._ensure_connected() + assert self._session is not None + + filtered = {key: value for key, value in parameters.items() if value is not None} + result = await asyncio.wait_for( + self._session.call_tool(tool_name.strip(), filtered), + timeout=self._options.request_timeout_seconds, + ) + parsed = _parse_tool_content(result) + if getattr(result, "isError", False): + message = parsed if isinstance(parsed, str) else json.dumps(parsed, ensure_ascii=False) + return McpResponse( + id=tool_name, + success=False, + result=parsed, + error=McpError(code=-1, message=message or f"Tool '{tool_name}' returned an error"), + ) + + self._last_success = datetime.now(timezone.utc) + return McpResponse(id=tool_name, success=True, result=parsed) + + async def ping(self, cancellation_token: Any = None) -> bool: + """Return True when the server responds to ``ping``.""" + try: + response = await self.invoke_tool("ping", {}) + return response.success + except Exception: + return False + + def is_healthy(self) -> bool: + """Return True when connected and recently successful.""" + if self._state != ConnectionState.CONNECTED: + return False + idle = datetime.now(timezone.utc) - self._last_success + return idle < timedelta(seconds=self._options.max_idle_time_seconds) + + async def close(self) -> None: + """Shut down the MCP session and subprocess.""" + async with self._lock: + await self._cleanup_unlocked() + + async def _ensure_connected(self) -> None: + if self._session is None: + await self.connect() + + async def _cleanup_unlocked(self) -> None: + if self._session_context is not None: + try: + await self._session_context.__aexit__(None, None, None) + except Exception as exc: + LOGGER.debug("MCP session cleanup failed: %s", exc) + if self._stdio_context is not None: + try: + await self._stdio_context.__aexit__(None, None, None) + except Exception as exc: + LOGGER.debug("MCP stdio cleanup failed: %s", exc) + + self._session = None + self._session_context = None + self._stdio_context = None + self._state = ConnectionState.DISCONNECTED diff --git a/backend/package.json b/backend/package.json index 5c7d397c..2fb72934 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "unity-generator-backend", - "version": "1.9.0", + "version": "2.0.0", "description": "Backend for Unity Generator", "private": true, "scripts": { diff --git a/backend/tests/test_unity_mcp_pluginwrapper.py b/backend/tests/test_unity_mcp_pluginwrapper.py index 6a58e21d..c549fc3a 100644 --- a/backend/tests/test_unity_mcp_pluginwrapper.py +++ b/backend/tests/test_unity_mcp_pluginwrapper.py @@ -18,15 +18,18 @@ @pytest.mark.asyncio async def test_unity_mcp_pluginwrapper_create_initialize_and_cleanup() -> None: - """Ensure wrapper creates plugin via local package and performs lifecycle calls.""" + """Ensure wrapper creates plugin via official MCP SDK client and performs lifecycle calls.""" mock_plugin = MagicMock() mock_plugin.initialize = AsyncMock() mock_plugin.cleanup = AsyncMock() + mock_client = MagicMock() - with patch("app.agents.unity_mcp_plugin.UnityMCPPlugin.create", return_value=mock_plugin) as mock_create: + with patch("app.agents.unity_mcp_plugin.OfficialMcpSdkClient", return_value=mock_client), patch( + "app.agents.unity_mcp_plugin.UnityMCPPlugin", return_value=mock_plugin + ) as mock_plugin_cls: async with create_unity_mcp_plugin() as plugin: assert plugin is mock_plugin - mock_create.assert_called_once() + mock_plugin_cls.assert_called_once() mock_plugin.initialize.assert_awaited_once() mock_plugin.cleanup.assert_awaited_once() @@ -38,7 +41,9 @@ async def test_unity_mcp_pluginwrapper_error_propagates() -> None: mock_plugin.initialize = AsyncMock(side_effect=RuntimeError("boom")) mock_plugin.cleanup = AsyncMock() - with patch("app.agents.unity_mcp_plugin.UnityMCPPlugin.create", return_value=mock_plugin): + with patch("app.agents.unity_mcp_plugin.OfficialMcpSdkClient"), patch( + "app.agents.unity_mcp_plugin.UnityMCPPlugin", return_value=mock_plugin + ): with pytest.raises(RuntimeError, match="boom"): async with create_unity_mcp_plugin(): pass diff --git a/backend/tests/test_unity_mcp_sdk_client.py b/backend/tests/test_unity_mcp_sdk_client.py new file mode 100644 index 00000000..41b72d3b --- /dev/null +++ b/backend/tests/test_unity_mcp_sdk_client.py @@ -0,0 +1,55 @@ +"""Tests for the official MCP SDK adapter used by Semantic Kernel agents.""" + +from __future__ import annotations + +import pytest + +from app.services.unity_mcp_sdk_client import OfficialMcpSdkClient, _parse_tool_content + + +class _FakeText: + def __init__(self, text: str) -> None: + self.text = text + + +class _FakeResult: + def __init__(self, *, text: str, is_error: bool = False) -> None: + self.content = [_FakeText(text)] + self.isError = is_error + + +def test_parse_tool_content_parses_json() -> None: + """Tool text payloads should deserialize to Python values when JSON.""" + import app.services.unity_mcp_sdk_client as module + + original = module.TextContent + module.TextContent = _FakeText + try: + parsed = _parse_tool_content(_FakeResult(text='{"success": true, "items": [1]}')) + finally: + module.TextContent = original + + assert parsed == {"success": True, "items": [1]} + + +@pytest.mark.asyncio +@pytest.mark.live_mcp +async def test_sdk_client_invokes_list_assets_live() -> None: + """Live smoke: official SDK client can call a tool the legacy client rejects.""" + from unity_mcp.models import UnityMcpOptions + + client = OfficialMcpSdkClient(UnityMcpOptions(executable_path="unity-mcp")) + await client.connect() + try: + tools = await client.list_tools() + names = {tool.name for tool in tools} + assert "unity_list_assets" in names + + response = await client.invoke_tool( + "unity_list_assets", + {"projectPath": "C:/output/UnityProject1", "folderName": "Assets"}, + ) + assert response.success is True + assert response.result is not None + finally: + await client.close() diff --git a/backend/tests/test_unity_orchestration.py b/backend/tests/test_unity_orchestration.py index 0407e839..ec363f6c 100644 --- a/backend/tests/test_unity_orchestration.py +++ b/backend/tests/test_unity_orchestration.py @@ -12,7 +12,13 @@ normalize_modality_key, should_apply_modality_filter, ) -from app.agents.unity_auto_invoke import auto_invoke_csharp_blocks, extract_csharp_blocks +from app.agents.unity_auto_invoke import ( + auto_invoke_csharp_blocks, + ensure_scene_file_if_missing, + extract_csharp_blocks, + infer_scene_file_name, + scene_creation_tool_was_called, +) from app.agents.unity_tool_argument_injection import inject_unity_tool_arguments @@ -44,6 +50,63 @@ def test_inject_project_path_and_file_name(): assert args["fileName"] == "Assets/Scripts/Player.cs" +def test_inject_skips_project_path_for_validate_csharp(): + args = inject_unity_tool_arguments( + "unity_validate_csharp", + {"code": "class Player {}"}, + project_path="C:/Projects/MyGame", + ) + assert "projectPath" not in args + assert args["code"] == "class Player {}" + + +def test_inject_adds_project_path_for_list_assets(): + args = inject_unity_tool_arguments( + "unity_list_assets", + {"folderName": "Assets"}, + project_path="C:/Projects/MyGame", + ) + assert args["projectPath"] == "C:/Projects/MyGame" + assert args["folderName"] == "Assets" + + +def test_scene_creation_tool_detection(): + assert scene_creation_tool_was_called({"project-unity_create_scene"}) is True + assert scene_creation_tool_was_called({"project-unity_create_script"}) is False + + +def test_infer_scene_file_name(): + assert infer_scene_file_name("Create a scene called Arena") == "Assets/Scenes/Arena.unity" + + +@pytest.mark.asyncio +async def test_ensure_scene_file_if_missing_creates_scene(): + plugin = AsyncMock() + created = await ensure_scene_file_if_missing( + plugin, + project_path="/p", + tools_invoked={"project-unity_create_script"}, + prompt="Create a side scrolling platformer scene", + ) + assert created is not None + assert created.endswith(".unity") + plugin.invoke_tool.assert_awaited_once() + assert plugin.invoke_tool.await_args[0][0] == "unity_create_scene" + + +@pytest.mark.asyncio +async def test_ensure_scene_skips_when_scene_tool_used(): + plugin = AsyncMock() + created = await ensure_scene_file_if_missing( + plugin, + project_path="/p", + tools_invoked={"scene-unity_create_detailed_scene"}, + prompt="Create a scene", + ) + assert created is None + plugin.invoke_tool.assert_not_called() + + def test_extract_csharp_blocks(): text = "```csharp\nclass Hero : MonoBehaviour {}\n```" blocks = extract_csharp_blocks(text) diff --git a/frontend/package.json b/frontend/package.json index e5dc1a1b..06c15e81 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "unity-generator-frontend", "description": "Frontend for Unity Generator", "private": true, - "version": "1.9.0", + "version": "2.0.0", "type": "module", "scripts": { "dev": "vite",