diff --git a/apps/admin_console/replay_manager.py b/apps/admin_console/replay_manager.py
index 7174d64..14f5b75 100644
--- a/apps/admin_console/replay_manager.py
+++ b/apps/admin_console/replay_manager.py
@@ -86,14 +86,14 @@ class ReplayManager:
It reconstructs historical execution states (SQLite database records, trace
histories, and screenshots) and replays a single agent step in an isolated,
deterministic sandbox environment while interacting live with a physical
- Android device and Gemini.
+ Android device and OpenAI Responses.
Key Responsibilities:
- Reconstructs and preloads historical SQLite states up to a specific
step.
- Establishes sandboxed `ArtemisContext` and `DataEngine` instances.
- Integrates with physical devices via ADB and UIAutomator.
- - Hooks and spies on the Gemini API to capture thoughts and raw outputs.
+ - Records Responses model calls to capture thoughts and raw outputs.
- Reconciles live and preloaded traces into unified chronological trees.
Attributes:
@@ -1552,8 +1552,8 @@ class ReplayManager:
# Update values
conn_temp.execute(
"""
- UPDATE video_recordings
- SET local_video_path = ?, start_time = ?, end_time = ?
+ UPDATE video_recordings
+ SET local_video_path = ?, start_time = ?, end_time = ?
WHERE session_id = ?
""",
(
@@ -1583,8 +1583,8 @@ class ReplayManager:
conn_temp.execute(
"""
- UPDATE video_recordings
- SET local_video_path = ?, start_time = ?, end_time = ?
+ UPDATE video_recordings
+ SET local_video_path = ?, start_time = ?, end_time = ?
WHERE session_id = ?
""",
(
@@ -1885,121 +1885,34 @@ class ReplayManager:
data_engine = ctx.data_engine
- print("Executing using Live Gemini Client...")
+ print("Executing using OpenAI Responses...")
- from google.genai.models import AsyncModels
+ from langchain_openai import ChatOpenAI
from unittest.mock import patch
- original_generate_content = AsyncModels.generate_content
+ original_generate = ChatOpenAI._agenerate
- async def wrapped_generate_content(self, *, model, contents, config=None, **kwargs):
- system_instruction = None
- if config and config.system_instruction:
- if isinstance(config.system_instruction, str):
- system_instruction = config.system_instruction
- elif hasattr(config.system_instruction, "parts"):
- parts = []
- for p in config.system_instruction.parts:
- if hasattr(p, "text") and p.text:
- parts.append(p.text)
- system_instruction = " ".join(parts)
- else:
- system_instruction = str(config.system_instruction)
-
- prompt_data = {
- "model": model,
- "system_instruction": system_instruction,
- "contents": [],
+ async def wrapped_generate(model, messages, *args, **kwargs):
+ record = {
+ "prompt": {
+ "model": model.model_name,
+ "contents": [
+ {"role": message.type, "parts": message.content} for message in messages
+ ],
+ },
+ "response": None,
+ "error": None,
}
-
- contents_list = contents if isinstance(contents, list) else [contents]
- for content in contents_list:
- parts_desc = []
- if hasattr(content, "parts") and content.parts:
- for part in content.parts:
- if hasattr(part, "text") and part.text:
- parts_desc.append({"type": "text", "text": part.text})
- elif hasattr(part, "inline_data") and part.inline_data:
- parts_desc.append(
- {
- "type": "image",
- "mime_type": part.inline_data.mime_type,
- "length": len(part.inline_data.data),
- }
- )
- elif hasattr(part, "function_call") and part.function_call:
- parts_desc.append(
- {
- "type": "function_call",
- "name": part.function_call.name,
- "args": (
- dict(part.function_call.args)
- if part.function_call.args
- else {}
- ),
- }
- )
- elif hasattr(part, "function_call") and part.function_response:
- parts_desc.append(
- {
- "type": "function_response",
- "name": part.function_response.name,
- "response": (
- dict(part.function_response.response)
- if part.function_response.response
- else {}
- ),
- }
- )
- else:
- parts_desc.append({"type": "raw", "value": str(content)})
-
- prompt_data["contents"].append(
- {
- "role": getattr(content, "role", "unknown"),
- "parts": parts_desc,
- }
- )
-
- record = {"prompt": prompt_data, "response": None, "error": None}
llm_calls_log.append(record)
-
try:
- res = await original_generate_content(
- self,
- model=model,
- contents=contents,
- config=config,
- **kwargs,
- )
-
- thinking_parts = []
- if hasattr(res, "candidates") and res.candidates and res.candidates[0].content:
- for part in res.candidates[0].content.parts:
- if getattr(part, "thought", False):
- thinking_parts.append(part.text)
-
- resp_data = {
- "text": res.text if hasattr(res, "text") else None,
- "function_calls": [],
- "thought": ("\n".join(thinking_parts) if thinking_parts else None),
- }
- if hasattr(res, "function_calls") and res.function_calls:
- for fc in res.function_calls:
- resp_data["function_calls"].append(
- {
- "name": fc.name,
- "args": dict(fc.args) if fc.args else {},
- }
- )
- record["response"] = resp_data
- return res
- except Exception as err:
- err_str = str(err)
- record["error"] = (
- f"{err.__class__.__name__}: {err_str}" if err_str else err.__class__.__name__
- )
- raise err
+ result = await original_generate(model, messages, *args, **kwargs)
+ record["response"] = [
+ generation.message.model_dump(mode="json") for generation in result.generations
+ ]
+ return result
+ except Exception as error:
+ record["error"] = error.__class__.__name__
+ raise
try:
try:
@@ -2042,7 +1955,7 @@ class ReplayManager:
val = resolved_val
tool_args[name] = val
- with patch.object(AsyncModels, "generate_content", wrapped_generate_content):
+ with patch.object(ChatOpenAI, "_agenerate", wrapped_generate):
outcome = await tool_fn(**tool_args)
replay_result["success"] = True
diff --git a/apps/admin_console/routers/system.py b/apps/admin_console/routers/system.py
index 5127eca..141e272 100644
--- a/apps/admin_console/routers/system.py
+++ b/apps/admin_console/routers/system.py
@@ -31,6 +31,63 @@ from artemis.core.diagnostics.schema import SystemReadinessReport
router = APIRouter(prefix="/api/system", tags=["system"])
+from apps.admin_console.services.model_settings_service import (
+ ModelSettingsUpdate,
+ get_model_settings,
+ save_model_settings,
+ test_model_settings,
+ CapabilitySettings,
+ get_capability_settings,
+ save_capability_settings,
+)
+
+
+@router.get("/capability-settings")
+async def read_capability_settings():
+ return get_capability_settings()
+
+
+@router.post("/capability-settings")
+async def write_capability_settings(payload: CapabilitySettings):
+ try:
+ return save_capability_settings(payload)
+ except OSError as exc:
+ raise HTTPException(
+ status_code=500, detail="无法保存功能开关,请检查 .env 文件权限"
+ ) from exc
+
+
+@router.get("/model-settings")
+async def read_model_settings():
+ return get_model_settings()
+
+
+@router.post("/model-settings")
+async def write_model_settings(payload: ModelSettingsUpdate):
+ try:
+ result = save_model_settings(payload)
+ except OSError as exc:
+ raise HTTPException(status_code=500, detail="无法写入 .env,请检查文件权限") from exc
+ except ValueError as exc:
+ raise HTTPException(status_code=400, detail="API Key 格式不正确,请检查后重试") from exc
+ await readiness_engine.run_all(force_refresh=True)
+ return {**result, "message": "配置已保存,新任务立即生效,重启后自动加载。"}
+
+
+@router.post("/model-settings/test")
+async def check_model_settings(payload: ModelSettingsUpdate):
+ try:
+ return await test_model_settings(payload)
+ except Exception as exc:
+ # SDK exceptions may contain URLs or request data; never echo credentials.
+ status = getattr(exc, "status_code", None)
+ detail = (
+ f"Responses 连接失败(HTTP {status})"
+ if status
+ else "Responses 连接失败,请检查接口地址、模型、密钥和超时设置。"
+ )
+ raise HTTPException(status_code=400, detail=detail) from exc
+
def _require_local_admin_request(request: Request) -> None:
"""Keep endpoint probing and mutation on the local administration boundary."""
@@ -264,8 +321,8 @@ class UpdateCredentialsRequest(BaseModel):
"""Payload to update and configure LLM or Vision OCR API credentials."""
provider: str = Field(
- default="google",
- description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)",
+ default="openai",
+ description="Provider identifier (openai or ocr)",
)
api_key: str = Field(description="The secret API key string to configure")
persist_to_env: bool = Field(
@@ -277,8 +334,8 @@ class ValidateCredentialsRequest(BaseModel):
"""Payload to test and verify LLM or Vision OCR API credentials without saving."""
provider: str = Field(
- default="google",
- description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)",
+ default="openai",
+ description="Provider identifier (openai or ocr)",
)
api_key: str = Field(description="The secret API key string to test")
base_url: str | None = Field(default=None, description="Optional custom base URL")
@@ -294,9 +351,8 @@ async def get_credentials():
"""
from artemis.config import settings
- providers = ("google", "openai", "anthropic", "openrouter", "ocr")
+ providers = ("openai", "ocr")
status = {name: bool(settings.get_api_key(name)) for name in providers}
- status["gemini"] = status["google"]
return {
"providers": [
{"name": name, "configured": configured} for name, configured in status.items()
@@ -415,13 +471,7 @@ async def get_model_config_and_env():
return "****"
return f"****{k[-4:]}"
- gemini_real = get_real_env_value("GEMINI_API_KEY", "google") or get_real_env_value(
- "GOOGLE_API_KEY", "google"
- )
openai_real = get_real_env_value("OPENAI_API_KEY", "openai")
- anthropic_real = get_real_env_value("ANTHROPIC_API_KEY", "anthropic")
- openrouter_real = get_real_env_value("OPEN_ROUTER_API_KEY", "openrouter")
- xai_real = get_real_env_value("XAI_API_KEY", "xai")
base_url_val = settings.OPENAI_BASE_URL or os.environ.get("OPENAI_BASE_URL")
if base_url_val and is_placeholder_key(base_url_val):
base_url_val = None
@@ -430,47 +480,19 @@ async def get_model_config_and_env():
)
env_vars = [
- {
- "name": "GEMINI_API_KEY",
- "provider": "google",
- "is_set": bool(gemini_real),
- "preview": mask_key(gemini_real),
- "description": "Google Gemini multimodal vision API key (free tier available)",
- },
{
"name": "OPENAI_API_KEY",
"provider": "openai",
"is_set": bool(openai_real),
"preview": mask_key(openai_real),
- "description": "OpenAI API key (GPT-4o, GPT-4o-mini)",
- },
- {
- "name": "ANTHROPIC_API_KEY",
- "provider": "anthropic",
- "is_set": bool(anthropic_real),
- "preview": mask_key(anthropic_real),
- "description": "Anthropic Claude API key (Claude 3.5 Sonnet, Claude 3.7)",
- },
- {
- "name": "OPEN_ROUTER_API_KEY",
- "provider": "openrouter",
- "is_set": bool(openrouter_real),
- "preview": mask_key(openrouter_real),
- "description": "OpenRouter unified API gateway key",
- },
- {
- "name": "XAI_API_KEY",
- "provider": "xai",
- "is_set": bool(xai_real),
- "preview": mask_key(xai_real),
- "description": "xAI Grok vision API key",
+ "description": "OpenAI Responses API 密钥",
},
{
"name": "OPENAI_BASE_URL",
- "provider": "custom",
+ "provider": "openai",
"is_set": bool(base_url_val),
"preview": base_url_val,
- "description": "Custom API endpoint (for local Ollama, vLLM, DeepSeek, or proxies)",
+ "description": "OpenAI Responses 接口根地址(包含 /v1,不包含 /responses)",
},
{
"name": "VISION_API_KEY",
diff --git a/apps/admin_console/services/model_service.py b/apps/admin_console/services/model_service.py
index 0ccc0bc..b8d4ca0 100644
--- a/apps/admin_console/services/model_service.py
+++ b/apps/admin_console/services/model_service.py
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from artemis.config.settings import settings
+
import json
import logging
import time
@@ -26,7 +28,7 @@ class ModelService:
# parse_llm_config re-reads and re-validates the config file on every call;
# /api/sessions calls this once per session row, so cache (provider, model_id)
# for a short TTL. Config edits take effect within _LLM_INFO_TTL seconds.
- _LLM_INFO_TTL = 10.0
+ _LLM_INFO_TTL = settings.ARTEMIS_MODEL_CACHE_TTL_SECONDS
_llm_info_cache: tuple[float, str, str] | None = None
@classmethod
@@ -35,8 +37,8 @@ class ModelService:
now = time.monotonic()
if cached and now - cached[0] < cls._LLM_INFO_TTL:
return cached[1], cached[2]
- provider = "google"
- model_id = "gemini-3.7-flash"
+ provider = "openai"
+ model_id = settings.OPENAI_MODEL
try:
from artemis.config import parse_llm_config
diff --git a/apps/admin_console/services/model_settings_service.py b/apps/admin_console/services/model_settings_service.py
new file mode 100644
index 0000000..2564280
--- /dev/null
+++ b/apps/admin_console/services/model_settings_service.py
@@ -0,0 +1,161 @@
+"""Persist the single Responses endpoint without returning credentials to the browser."""
+
+import os
+import tempfile
+import threading
+from pathlib import Path
+from urllib.parse import urlsplit
+
+from pydantic import BaseModel, Field, SecretStr, field_validator
+
+from artemis.config.paths import get_env_file
+from artemis.config.settings import settings
+
+_write_lock = threading.Lock()
+
+
+class ModelSettingsUpdate(BaseModel):
+ base_url: str = "https://api.openai.com/v1"
+ model: str = Field(min_length=1, max_length=200)
+ api_key: SecretStr | None = None # Omitted/blank keeps the existing key.
+ timeout_seconds: float = Field(default=120, gt=0, le=3600)
+ max_retries: int = Field(default=0, ge=0, le=10)
+
+ @field_validator("base_url")
+ @classmethod
+ def validate_base_url(cls, value: str) -> str:
+ value = value.strip().rstrip("/")
+ parsed = urlsplit(value)
+ if (
+ parsed.scheme not in ("https", "http")
+ or not parsed.hostname
+ or parsed.username
+ or parsed.password
+ or parsed.query
+ or parsed.fragment
+ ):
+ raise ValueError("请输入有效的 HTTP(S) 接口根地址,不要包含密钥、查询参数或片段")
+ if value.endswith(("/responses", "/chat/completions")):
+ raise ValueError("请输入接口根地址,例如 https://api.openai.com/v1")
+ return value
+
+ @field_validator("model")
+ @classmethod
+ def validate_model(cls, value: str) -> str:
+ value = value.strip()
+ if not value or any(c in value for c in "\r\n\x00"):
+ raise ValueError("模型名称不能为空或包含换行")
+ return value
+
+
+def get_model_settings() -> dict:
+ return {
+ "provider": "openai",
+ "api_mode": "responses",
+ "base_url": settings.OPENAI_BASE_URL or "https://api.openai.com/v1",
+ "model": settings.OPENAI_MODEL,
+ "api_key_configured": bool(settings.OPENAI_API_KEY),
+ "timeout_seconds": settings.OPENAI_TIMEOUT_SECONDS,
+ "max_retries": settings.OPENAI_MAX_RETRIES,
+ "env_path": str(get_env_file()),
+ }
+
+
+def save_model_settings(update: ModelSettingsUpdate) -> dict:
+ if update.api_key and any(c in update.api_key.get_secret_value() for c in "\r\n\x00"):
+ raise ValueError("API Key 不能包含换行")
+ values = {
+ "OPENAI_BASE_URL": update.base_url,
+ "OPENAI_MODEL": update.model,
+ "OPENAI_TIMEOUT_SECONDS": str(update.timeout_seconds),
+ "OPENAI_MAX_RETRIES": str(update.max_retries),
+ }
+ if update.api_key and update.api_key.get_secret_value().strip():
+ values["OPENAI_API_KEY"] = update.api_key.get_secret_value().strip()
+ persist_environment(values)
+ settings.OPENAI_BASE_URL = update.base_url
+ settings.OPENAI_MODEL = update.model
+ settings.OPENAI_TIMEOUT_SECONDS = update.timeout_seconds
+ settings.OPENAI_MAX_RETRIES = update.max_retries
+ if "OPENAI_API_KEY" in values:
+ settings.OPENAI_API_KEY = SecretStr(values["OPENAI_API_KEY"])
+ from artemis.llm.router import ModelFactory
+ from apps.admin_console.services.model_service import ModelService
+
+ ModelFactory._cache.clear()
+ ModelService._llm_info_cache = None
+ return get_model_settings()
+
+
+def persist_environment(values: dict[str, str]) -> None:
+ with _write_lock:
+ env_path = get_env_file()
+ env_path.parent.mkdir(parents=True, exist_ok=True)
+ original = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
+ # Preserve comments and unrelated variables; quote values to avoid dotenv interpolation.
+ kept = [
+ line
+ for line in original.splitlines()
+ if line.removeprefix("export ").split("=", 1)[0].strip() not in values
+ ]
+ for key, value in values.items():
+ escaped = value.replace("\\", "\\\\").replace("'", "\\'")
+ kept.append(f"{key}='{escaped}'")
+ content = "\n".join(kept) + "\n"
+ fd, temp_name = tempfile.mkstemp(prefix=".env-", dir=env_path.parent)
+ try:
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as stream:
+ stream.write(content)
+ stream.flush()
+ os.fsync(stream.fileno())
+ os.replace(temp_name, env_path)
+ finally:
+ Path(temp_name).unlink(missing_ok=True)
+ for key, value in values.items():
+ os.environ[key] = value
+
+
+class CapabilitySettings(BaseModel):
+ video_enabled: bool = True
+ outputter_enabled: bool = True
+ step_summarizer_enabled: bool = True
+
+
+def get_capability_settings() -> CapabilitySettings:
+ return CapabilitySettings(
+ video_enabled=settings.ARTEMIS_VIDEO_ENABLED,
+ outputter_enabled=settings.ARTEMIS_OUTPUTTER_ENABLED,
+ step_summarizer_enabled=settings.ARTEMIS_STEP_SUMMARIZER_ENABLED,
+ )
+
+
+def save_capability_settings(update: CapabilitySettings) -> CapabilitySettings:
+ values = {
+ "ARTEMIS_VIDEO_ENABLED": update.video_enabled,
+ "ARTEMIS_OUTPUTTER_ENABLED": update.outputter_enabled,
+ "ARTEMIS_STEP_SUMMARIZER_ENABLED": update.step_summarizer_enabled,
+ }
+ persist_environment({key: str(value).lower() for key, value in values.items()})
+ for key, value in values.items():
+ setattr(settings, key, value)
+ return get_capability_settings()
+
+
+async def test_model_settings(update: ModelSettingsUpdate) -> dict:
+ from langchain_core.messages import HumanMessage
+ from artemis.llm.router import ModelEndpoint, ModelFactory
+
+ key = update.api_key.get_secret_value().strip() if update.api_key else None
+ endpoint = ModelEndpoint(
+ model_name=update.model,
+ api_key=key or None,
+ api_base=update.base_url,
+ timeout_seconds=update.timeout_seconds,
+ max_retries=0,
+ max_tokens=64,
+ )
+ model = ModelFactory.create_model(endpoint)
+ result = await model.ainvoke([HumanMessage(content="Reply with OK only.")])
+ if not result.content:
+ raise ValueError("Responses 接口返回空内容,请检查模型配置")
+ return {"success": True, "message": "Responses 连接测试成功", "model": update.model}
diff --git a/apps/showcase_ui/src/app/app.config.ts b/apps/showcase_ui/src/app/app.config.ts
index c5a0f87..29d6d98 100644
--- a/apps/showcase_ui/src/app/app.config.ts
+++ b/apps/showcase_ui/src/app/app.config.ts
@@ -1,3 +1,6 @@
+import { registerLocaleData } from '@angular/common';
+import zh from '@angular/common/locales/zh';
+registerLocaleData(zh);
/**
* Copyright 2026 Google LLC
*
@@ -14,7 +17,7 @@
* limitations under the License.
*/
-import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+import { ApplicationConfig, LOCALE_ID, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withXhr } from '@angular/common/http';
@@ -22,6 +25,7 @@ import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
+ { provide: LOCALE_ID, useValue: 'zh-CN' },
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withXhr())
diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts b/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts
index 5c915fd..faa5b8e 100644
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts
+++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts
@@ -4,17 +4,17 @@ import { buildStartupWorkItems } from './agent-stream.component';
describe('startup Work block', () => {
it('shows only the three device preparation operations', () => {
const events: StartupProgressEvent[] = [
- { stage: 'submitting', message: 'Submitting the task', timestamp: 100 },
+ { stage: 'submitting', message: '正在提交任务', timestamp: 100 },
{ stage: 'queued', message: 'Task received and queued', timestamp: 100.9 },
{ stage: 'launching', message: 'Starting the execution process', timestamp: 101 },
{ stage: 'configuration', message: 'Loading the run configuration', timestamp: 103.1 },
{ stage: 'device_check', message: 'Checking the Android device', timestamp: 103.2 },
- { stage: 'device_ready', message: 'Android device connected', timestamp: 123 },
+ { stage: 'device_ready', message: 'Android 设备已连接', timestamp: 123 },
{ stage: 'model_warmup', message: 'Warming the model connection', timestamp: 123 },
{ stage: 'uiautomator', message: 'Connecting to UI Automator', timestamp: 124 },
{ stage: 'uiautomator_ready', message: 'UI Automator is ready', timestamp: 128 },
{ stage: 'environment', message: 'Preparing the device environment', timestamp: 128 },
- { stage: 'environment_ready', message: 'Device environment is ready', timestamp: 131 }
+ { stage: 'environment_ready', message: '设备环境已就绪', timestamp: 131 }
];
const items = buildStartupWorkItems(events, 131, false, true);
@@ -90,7 +90,7 @@ describe('startup Work block', () => {
const [item] = buildStartupWorkItems(events, 20, false, true);
- expect(item.message).toBe('UI hierarchy service is ready');
+ expect(item.message).toBe('界面层级服务已就绪');
});
it('uses old first_response events only as a hidden completion boundary', () => {
@@ -101,7 +101,7 @@ describe('startup Work block', () => {
const [item] = buildStartupWorkItems(events, 24, false, true);
- expect(item.message).toBe('Device environment is ready');
+ expect(item.message).toBe('设备环境已就绪');
expect(item.elapsed).toBe('3.0s');
expect(item.isActive).toBeFalse();
});
diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
index 93172d3..ca4590c 100644
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
+++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
@@ -24,11 +24,11 @@ limitations under the License.
class="vscode-tab-btn"
[class.active]="isTaskDropdownOpen()"
(click)="toggleTaskDropdown($event)"
- title="Task Queue"
- aria-label="Task Queue"
+ title="任务队列"
+ aria-label="任务队列"
>
list_alt
- Task Queue
+ 任务队列
description
- Notes & Plans
+ 笔记与计划
@@ -57,7 +57,7 @@ limitations under the License.
- {{ session.initial_goal || 'Unnamed Task' }}
+ {{ session.initial_goal || '未命名任务' }}
@if (historyTasks().length === 0) {
- No past task history
+
+ 暂无历史任务
}
@@ -148,7 +150,7 @@ limitations under the License.
@if (getTaskStatus(session) === 'running') {
}
- {{ getTaskStatus(session) | uppercase }}
+ {{ getTaskStatus(session) | uiLabel }}
- {{ session.initial_goal || 'Unnamed Task' }}
+ {{ session.initial_goal || '未命名任务' }}
= 65">
@@ -847,7 +500,7 @@
3
}
- 3. Android OS Boot
+ 3. 系统启动
@@ -856,7 +509,7 @@
check_circle
- 4. Ready
+ 4. 就绪
@@ -877,7 +530,7 @@
info
- Android virtualization cold boot usually takes 20-45 seconds. Live progress is actively tracked above—please do not click launch repeatedly.
+ Android 模拟器冷启动通常需要 20–45 秒。上方会显示实时进度,请勿重复点击启动。
@@ -886,14 +539,14 @@
- @if (emulatorLaunchState()?.logs?.length) {@for (log of emulatorLaunchState()?.logs; track log) {{{ log }}
}} @else {Waiting for process output...
}
+ @if (emulatorLaunchState()?.logs?.length) {@for (log of emulatorLaunchState()?.logs; track log) {{{ log }}
}} @else {正在等待进程输出…
}
}
@@ -910,11 +563,11 @@
error
-
Emulator Launch Failed
+
模拟器启动失败
{{ emulatorLaunchState()?.error || 'The Android emulator process failed to start or crashed during initialization.' }}
-
+
close
@@ -922,7 +575,7 @@
@if (emulatorLaunchState()?.logs?.length) {
@@ -968,13 +621,14 @@
- Android Emulator
+ Android 模拟器
@if (installedAvds().length > 0) {
- {{ installedAvds().length }} local virtual device{{ installedAvds().length > 1 ? 's' : '' }} available
+ {{ installedAvds().length }} 个本地虚拟设备可用
} @else {
- Launch a local Android Virtual Device
+
+ 启动本地 Android 虚拟设备
}
@@ -996,9 +650,9 @@
- USB Cable
+ USB 连接
-
Connect physical phone via USB
+
通过 USB 连接实体手机
@@ -1018,9 +672,9 @@
- Wireless ADB
+ 无线 ADB
-
Connect over local Wi-Fi
+
通过局域网 Wi-Fi 连接
@@ -1040,9 +694,9 @@
- Remote Computer
+ 远程计算机
-
Phone connected to another computer
+
连接其他计算机上的手机
@@ -1056,9 +710,9 @@
hub
- Artemis is currently using
- {{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }} .
- Switch to local ADB before using a local emulator, USB cable, or Wireless ADB.
+
+ Artemis 当前使用
+ {{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }} 。使用本地模拟器、USB 或无线 ADB 前,请先切换至本地 ADB。
@if (activeAdbGuideTab() === 'emulator') {
-
+
@if (installedAvds().length > 0) {
@@ -1109,14 +763,14 @@
[class.launching]="launchingAvd() === avd || (isEmulatorLaunching() && emulatorLaunchState()?.avd_name === avd)"
[disabled]="isEmulatorLaunching() || isRemoteAdbServer()"
(click)="launchAvdEmulator(avd)"
- [title]="isEmulatorLaunching() ? 'An emulator is currently launching...' : 'Launch ' + avd"
+ [title]="isEmulatorLaunching() ? '正在启动模拟器…' : '启动' + avd"
>
@if (launchingAvd() === avd || (isEmulatorLaunching() && emulatorLaunchState()?.avd_name === avd)) {
{{ emulatorLaunchState()?.status === 'booting' ? 'Booting OS' : 'Starting' }} ({{ emulatorLaunchState()?.elapsed_seconds || 0 }}s)
} @else {
play_arrow
-
Launch
+
启动
}
@@ -1133,7 +787,7 @@
-
No Android Virtual Devices (AVD) Detected
+
未检测到 Android 虚拟设备(AVD)
Linux
@@ -1143,7 +797,7 @@
(click)="setSelectedOs('windows')">Windows
-
Follow the step-by-step instructions below to install and start an emulator:
+
按以下步骤安装并启动模拟器:
@@ -1155,7 +809,7 @@
(click)="setEmulatorSetupMode('studio')"
>
desktop_windows
- Android Studio (GUI)
+ Android Studio(图形界面)
terminal
- Command Line (CLI)
+ 命令行
@@ -1177,9 +831,9 @@
1
download
- Install Android Studio
+ 安装 Android Studio
- Download for {{ effectiveOs() === 'darwin' ? 'macOS' : (effectiveOs() === 'windows' ? 'Windows' : 'Linux') }} from
developer.android.com/studio and install.
+ Download for {{ effectiveOs() === 'darwin' ? 'macOS' : (effectiveOs() === 'windows' ? 'Windows' : 'Linux') }} from
developer.android.com/studio 并安装。
@@ -1188,9 +842,10 @@
2
developer_mode
- Open Device Manager
+ 打开设备管理器
- On Studio welcome screen, open More Actions > Virtual Device Manager (or Tools > Device Manager ).
+
+ 在 Android Studio 欢迎页面打开 更多操作 > 虚拟设备管理器 (或 工具 > 设备管理器 ).
@@ -1199,9 +854,10 @@
3
add_circle
- Create Virtual Device
+ 创建虚拟设备
- Click Create Device , choose Pixel 8 , select Android 14 (API 34) , and click Finish .
+
+ 点击 创建设备 ,选择 Pixel 8 ,选择 Android 14 (API 34) ,然后点击 完成 .
@@ -1210,9 +866,10 @@
4
play_arrow
- Launch Emulator
+ 启动模拟器
- Click ▶ Play in Device Manager. Artemis will detect and connect to it automatically!
+
+ 点击 ▶ 播放 。Artemis 会自动检测并连接设备。
@@ -1232,7 +889,7 @@
{{ copiedId() === 'emu_kvm' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'emu_kvm' ? 'Copied!' : 'Copy' }}
+ {{ copiedId() === 'emu_kvm' ? '已复制' : '复制' }}
@@ -1241,15 +898,15 @@
2
-
Download Emulator & System Image (API 34)
-
Install Android 14 Google APIs system image using SDK Manager:
+
下载模拟器与系统镜像(API 34)
+
通过 SDK 管理器安装 Android 14 Google APIs 系统镜像:
{{ effectiveOs() === 'windows' ? 'PS>' : '$' }}
{{ emuSdkInstallCmd() }}
{{ copiedId() === 'emu_sdk' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'emu_sdk' ? 'Copied!' : 'Copy' }}
+ {{ copiedId() === 'emu_sdk' ? '已复制' : '复制' }}
@@ -1258,15 +915,15 @@
3
-
Create Virtual Device (Pixel 8 AVD)
-
Initialize a new Android Virtual Device named Pixel_8_API_34:
+
创建虚拟设备(Pixel 8 AVD)
+
创建 Android 虚拟设备,名称为 Pixel_8_API_34:
{{ effectiveOs() === 'windows' ? 'PS>' : '$' }}
{{ emuCreateAvdCmd() }}
{{ copiedId() === 'emu_create' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'emu_create' ? 'Copied!' : 'Copy' }}
+ {{ copiedId() === 'emu_create' ? '已复制' : '复制' }}
@@ -1275,15 +932,15 @@
4
-
Start the Emulator
-
Launch the virtual device (or add -no-window for headless execution):
+
启动模拟器
+
启动虚拟设备(可添加 -no-window 以无界面模式运行):
{{ effectiveOs() === 'windows' ? 'PS>' : '$' }}
emulator -avd Pixel_8_API_34 -no-boot-anim
{{ copiedId() === 'emu_run' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'emu_run' ? 'Copied!' : 'Copy' }}
+ {{ copiedId() === 'emu_run' ? '已复制' : '复制' }}
@@ -1301,7 +958,7 @@
cable
- Connect your Android phone via USB cable and enable USB Debugging:
+ 通过 USB 数据线连接 Android 手机,并启用 USB 调试:
@@ -1310,8 +967,8 @@
1
usb
-
Connect USB Cable
-
Plug your Android phone into this computer with a data cable.
+
连接 USB 数据线
+
使用数据线将 Android 手机连接到此计算机。
@@ -1319,9 +976,8 @@
2
touch_app
-
Enable Developer Options
-
In phone Settings > About Phone , tap Build Number 7
- times.
+
启用开发者选项
+
在手机上打开 设置 > 关于手机 ,点击 版本号 7 次。
@@ -1329,8 +985,8 @@
3
toggle_on
-
Enable USB Debugging
-
In
Settings > Developer Options , turn
USB Debugging ON.
+
启用 USB 调试
+
在 设置 > 开发者选项 中,将 USB 调试 开启。
@@ -1339,8 +995,8 @@
4
verified_user
-
Authorize Computer
-
Check
"Always allow" and tap
Allow on the phone prompt.
+
授权计算机
+
勾选 “始终允许” ,然后点击 允许 完成手机上的授权提示。
@@ -1352,44 +1008,44 @@
wifi
- Connect your phone and computer on the same Wi-Fi network:
+ 将手机和计算机连接到同一 Wi-Fi 网络:
-
+
-
+
@if (wifiConnectMessage()) {
check_circle
@@ -1406,7 +1062,8 @@
help_outline
- Find IP & port in phone Settings > Developer Options > Wireless Debugging .
+
+ 在手机中查看 IP 地址与端口: 设置 > 开发者选项 > 无线调试 .
@@ -1417,13 +1074,13 @@
hub
- Enter the host and port of an ADB server endpoint that this computer can access, so Artemis can discover and control a phone physically connected to another computer.
+ 输入本机可访问的 ADB 服务器地址和端口,即可发现并控制连接在其他计算机上的手机。
}
@@ -1556,14 +1214,14 @@
refresh
- {{ isRefreshingDiagnostics() ? 'Checking...' : 'Check Again' }}
+ {{ isRefreshingDiagnostics() ? '正在检查…' : 'Check Again' }}
@if (connectedDevices().length > 1) {
-
Select Active Device:
+
选择当前设备:
@for (dev of connectedDevices(); track dev.serial) {
{{ dev.model || dev.serial }}
@if (dev.serial === activeDevice()?.serial) {
- ✓ Active
+ ✓ 当前设备
}
}
@@ -1593,14 +1251,15 @@
screen_lock_portrait
-
USB Debugging Authorization Pending
+
等待 USB 调试授权
- Your Android device was detected, but it requires authorization. Please unlock your phone screen and tap "Allow USB Debugging" .
+
+ 已检测到 Android 设备,但尚未授权。请解锁手机并点击 “允许 USB 调试” .
refresh
- {{ isRemoteAdbServer() ? 'Refresh Devices' : 'Restart ADB & Check' }}
+ {{ isRemoteAdbServer() ? '刷新设备' : '重启 ADB 并检查' }}
@@ -1612,7 +1271,7 @@
error
- Android Platform Tools (adb) not found. Please install ADB on your operating system:
+ 未找到 Android 平台工具(adb)。 请在当前操作系统中安装 ADB:
@@ -1621,7 +1280,7 @@
Linux (Ubuntu / Debian)
@if (effectiveOs() === 'linux') {
- Detected
+ 已检测到
}
@@ -1630,7 +1289,7 @@
{{ copiedId() === 'linux_adb' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'linux_adb' ? 'Copied!' : 'Copy' }}
+ {{ copiedId() === 'linux_adb' ? '已复制' : '复制' }}
@@ -1639,7 +1298,7 @@
macOS (Homebrew)
@if (effectiveOs() === 'darwin') {
- Detected
+ 已检测到
}
@@ -1648,7 +1307,7 @@
{{ copiedId() === 'mac_adb' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'mac_adb' ? 'Copied!' : 'Copy' }}
+ {{ copiedId() === 'mac_adb' ? '已复制' : '复制' }}
@@ -1657,7 +1316,7 @@
Windows (WinGet)
@if (effectiveOs() === 'windows') {
- Detected
+ 已检测到
}
@@ -1666,7 +1325,7 @@
{{ copiedId() === 'win_adb' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'win_adb' ? 'Copied!' : 'Copy' }}
+ {{ copiedId() === 'win_adb' ? '已复制' : '复制' }}
@@ -1685,12 +1344,12 @@
check
-
System Ready for Autonomous Tasks
-
All dependencies, AI model credentials, and Android device are verified and ready to run.
+
系统已就绪,可以开始自动化任务
+
依赖、模型凭据和 Android 设备均已就绪。
- Launch Autonomous Task
+ 启动自动化任务
rocket_launch
@@ -1711,8 +1370,8 @@
{{ adbProbe()?.summary === 'Device Locked' ? 'lock' : 'warning' }}
{{ adbProbe()?.summary === 'Device Locked'
- ? 'Android device is locked. Unlock it and enter the home screen before running a task.'
- : 'Prerequisites incomplete: runtime, credentials, or Android device is not ready. Click here to open Setup Guide.' }}
+ ? 'Android 设备已锁定,请先解锁并进入主屏幕。'
+ : '运行环境、模型凭据或 Android 设备尚未就绪。点击此处打开系统设置。' }}
arrow_forward
@@ -1720,18 +1379,19 @@
-
+
@if (showIntentSuggestion()) {
auto_awesome
- Continuous monitoring or complex task detected. Recommended: Pro Mode
+
+ 检测到持续监控或复杂任务,建议使用: Pro 专业模式
- Switch to Pro
+ 切换至 Pro
arrow_forward
@@ -1745,7 +1405,7 @@
rows="3"
[(ngModel)]="taskGoal"
[disabled]="isSubmitting()"
- placeholder="Describe what you want to achieve on your phone (e.g. Open Gmail and search for latest emails)..."
+ placeholder="描述你希望在手机上完成的任务,例如:打开 Gmail 并查找最新邮件…"
(keydown.control.enter)="runTask()"
(keydown.meta.enter)="runTask()"
>
@@ -1761,14 +1421,14 @@
Flash
·
- Token-efficient, rapid 3–5s/step decisions straight from the live screen. Best for everyday tasks with a clear path, however long they run.
- (No plan, no result check, no ADB commands)
+ 节省 Token,快速响应 每步 3–5 秒 直接根据实时屏幕执行操作,适合路径明确的日常任务。
+ (不规划、不检查结果、不执行 ADB 命令)
} @else {
Pro
·
- Plans first, checks its results and recovers when a tap is blocked. Handles 100+ step workflows and live phone monitoring (15–40s / step).
+ 先规划、再检查结果,遇到操作受阻时自动恢复。支持 超过 100 步的 工作流和实时手机监控(15–40 秒 / 步)。
}
@@ -1792,7 +1452,7 @@
class="model-toggle-btn btn-flash"
[class.active]="selectedProfile() === 'flash'"
(click)="setProfile('flash')"
- title="Flash: fast single-look loop for everyday app tasks (3–5s/step)"
+ title="Flash:快速执行日常应用任务(每步 3–5 秒)"
>
bolt
Flash
@@ -1802,7 +1462,7 @@
class="model-toggle-btn btn-pro"
[class.active]="selectedProfile() === 'pro'"
(click)="setProfile('pro')"
- title="Pro: plans first, checks results and recovers from blocked steps. For long or complex tasks (15–40s/step)"
+ title="Pro:先规划、检查结果并恢复受阻步骤,适合长流程或复杂任务(每步 15–40 秒)"
>
psychology
Pro
@@ -1821,13 +1481,13 @@
class="btn-run-composer"
[class.btn-run-pro]="selectedProfile() === 'pro'"
[disabled]="!taskGoal().trim() || isSubmitting() || !isReady()"
- [title]="!isReady() ? (adbProbe()?.summary === 'Device Locked' ? 'Unlock the Android device before running' : 'Complete System Setup before running') : 'Run task'"
+ [title]="!isReady() ? (adbProbe()?.summary === 'Device Locked' ? '请先解锁 Android 设备' : '请先完成系统设置') : '执行任务'"
(click)="runTask()"
>
@if (isSubmitting()) {
- Starting...
+ 正在启动…
} @else {
- play_arrow Run Task
+ play_arrow 执行任务
}
@@ -1838,8 +1498,8 @@
@if (selectedProfile() === 'pro') {
@@ -2071,7 +1731,7 @@
+ [title]="item.profile === 'pro' ? '使用 ARTEMIS Pro:多智能体协作' : '使用 ARTEMIS Flash:快速执行'">
{{ item.profile === 'pro' ? 'Pro' : 'Flash' }}
@@ -2086,7 +1746,7 @@
diff --git a/apps/showcase_ui/src/app/pages/home/home.component.ts b/apps/showcase_ui/src/app/pages/home/home.component.ts
index e189b9c..6667d1e 100644
--- a/apps/showcase_ui/src/app/pages/home/home.component.ts
+++ b/apps/showcase_ui/src/app/pages/home/home.component.ts
@@ -1,3 +1,5 @@
+import { CapabilitySettingsComponent } from '../../components/model-settings/capability-settings.component';
+import { ModelSettingsComponent } from '../../components/model-settings/model-settings.component';
/**
* Copyright 2026 Google LLC
*
@@ -14,9 +16,9 @@
* limitations under the License.
*/
-import { Component, signal, computed, effect, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
+import { Component, signal, computed, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { FormsModule } from '@angular/forms';
-import { Router, RouterLink } from '@angular/router';
+import { Router } from '@angular/router';
import { AgentService } from '../../services/agent.service';
import { SystemService } from '../../services/system.service';
import {
@@ -103,7 +105,7 @@ type AdbGuideTab = 'emulator' | 'usb' | 'wifi' | 'remote';
@Component({
selector: 'app-home',
standalone: true,
- imports: [FormsModule],
+ imports: [FormsModule, ModelSettingsComponent, CapabilitySettingsComponent],
templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './home.component.scss'
@@ -121,32 +123,6 @@ export class HomeComponent implements OnInit, OnDestroy {
public activeAdbGuideTab = signal('emulator');
public emulatorSetupMode = signal<'studio' | 'cli'>('studio');
- // Interactive guide tab for LLM / OCR credentials: 'gemini' | 'ocr'
- public modelSetupMode = signal<'gemini' | 'custom'>('gemini');
- public showOcrConfig = signal(false);
- public showFullConfigFile = signal(false);
-
- // Model & Environment configuration from backend
- public modelConfigEnv = computed(() => this.systemService.modelConfigEnv());
-
- // Google Gemini API Key State
- public geminiKeyInput = signal('');
- public showGeminiKey = signal(false);
- public isSavingGeminiKey = signal(false);
- public isTestingGeminiKey = signal(false);
- public geminiSaveMessage = signal(null);
- public geminiSaveError = signal(null);
- public isGeminiKeyEdited = signal(false);
-
- // Vision OCR API Key State
- public ocrKeyInput = signal('');
- public showOcrKey = signal(false);
- public isSavingOcrKey = signal(false);
- public isTestingOcrKey = signal(false);
- public ocrSaveMessage = signal(null);
- public ocrSaveError = signal(null);
- public isOcrKeyEdited = signal(false);
-
// Clipboard copy state tracker for interactive feedback
public copiedId = signal(null);
@@ -198,7 +174,6 @@ export class HomeComponent implements OnInit, OnDestroy {
// Pro Mode Outputter & Structured Output Configuration
public expectedOutput = signal('');
- public enableOutputter = signal(true);
public showOutputterDrawer = signal(false);
// Slider indexes map to the API ids in VERIFICATION_LEVELS / EXPLORER_MODES.
@@ -238,8 +213,8 @@ export class HomeComponent implements OnInit, OnDestroy {
return [
{
kind: 'verify',
- name: 'Result check',
- ends: ['Off', 'Strict'],
+ name: "结果检查",
+ ends: ["关闭", "严格检查"],
ladder: VERIFICATION_LEVELS,
index: vi,
level: VERIFICATION_LEVELS[vi],
@@ -247,8 +222,8 @@ export class HomeComponent implements OnInit, OnDestroy {
},
{
kind: 'explore',
- name: 'Screen reading',
- ends: ['Faster', 'Sharper'],
+ name: "屏幕识别",
+ ends: ["更快", "更精细"],
ladder: EXPLORER_MODES,
index: ei,
level: EXPLORER_MODES[ei],
@@ -352,13 +327,13 @@ export class HomeComponent implements OnInit, OnDestroy {
public emulatorLaunchState = computed(() => this.systemService.emulatorLaunchState());
public isEmulatorLaunching = computed(() => this.systemService.isEmulatorLaunching());
public showLaunchLogs = signal(false);
-
+
// Probes
public pythonProbe = computed(() => this.systemService.pythonProbe());
public configProbe = computed(() => this.systemService.configProbe());
public adbProbe = computed(() => this.systemService.adbProbe());
public llmProbe = computed(() => this.systemService.llmProbe());
- public geminiProbe = computed(() => this.systemService.geminiProbe());
+ public openaiProbe = computed(() => this.systemService.openaiProbe());
public ocrProbe = computed(() => this.systemService.ocrProbe());
public toolchainProbe = computed(() => this.systemService.toolchainProbe());
@@ -426,16 +401,16 @@ export class HomeComponent implements OnInit, OnDestroy {
public emuHypervisorTitle = computed(() => {
const os = this.effectiveOs();
- if (os === 'windows') return 'Enable Windows Hypervisor Platform (WHPX)';
- if (os === 'darwin') return 'Verify macOS Hypervisor / Install Tools';
- return 'Enable KVM Hardware Acceleration (Linux)';
+ if (os === 'windows') return "启用 Windows 虚拟机监控程序平台(WHPX)";
+ if (os === 'darwin') return "检查 macOS 虚拟化并安装工具";
+ return "启用 KVM 硬件加速(Linux)";
});
public emuHypervisorDesc = computed(() => {
const os = this.effectiveOs();
- if (os === 'windows') return 'Enable Windows Hypervisor Platform in PowerShell (Run as Administrator):';
- if (os === 'darwin') return 'macOS uses native Hypervisor.framework. Install SDK tools via Homebrew (or Studio):';
- return 'Ensure virtualization permissions are granted to your user account:';
+ if (os === 'windows') return "以管理员身份打开 PowerShell,启用 Windows 虚拟机监控程序平台:";
+ if (os === 'darwin') return "macOS 使用系统 Hypervisor.framework。通过 Homebrew 或 Android Studio 安装 SDK 工具:";
+ return "确保当前用户拥有虚拟化权限:";
});
public emuHypervisorCmd = computed(() => {
@@ -467,27 +442,6 @@ export class HomeComponent implements OnInit, OnDestroy {
return meta?.['configured'] === true;
});
- public currentApiKey = computed(() => this.systemService.currentApiKey());
- public apiKeysMap = computed>(() => this.systemService.apiKeysMap());
-
- public savedGeminiKey = computed(() => {
- const keys = this.apiKeysMap();
- return keys['google'] || this.currentApiKey() || '';
- });
-
- public isGeminiModified = computed(() => {
- return this.geminiKeyInput().trim() !== this.savedGeminiKey().trim();
- });
-
- public savedOcrKey = computed(() => {
- const keys = this.apiKeysMap();
- return keys['ocr'] || '';
- });
-
- public isOcrModified = computed(() => {
- return this.ocrKeyInput().trim() !== this.savedOcrKey().trim();
- });
-
// Rich Smart Suggestions Library (Device-Aware, Flash vs Pro Tailored)
public readonly allSuggestions = this.taskRecService.allTasks;
@@ -520,21 +474,6 @@ export class HomeComponent implements OnInit, OnDestroy {
this.systemService.fetchReadiness().subscribe();
};
- constructor() {
- effect(() => {
- const keys = this.systemService.apiKeysMap();
- const current = this.systemService.currentApiKey();
- const googleKey = keys['google'] || current || '';
- const ocrKey = keys['ocr'] || '';
- if (!this.isGeminiKeyEdited()) {
- this.geminiKeyInput.set(googleKey);
- }
- if (!this.isOcrKeyEdited()) {
- this.ocrKeyInput.set(ocrKey);
- }
- });
- }
-
ngOnInit(): void {
this.loadProTuningDefaults();
@@ -581,234 +520,15 @@ export class HomeComponent implements OnInit, OnDestroy {
this.emulatorSetupMode.set(mode);
}
- public setModelSetupMode(mode: 'gemini' | 'custom'): void {
- this.modelSetupMode.set(mode);
- if (mode === 'custom') {
- this.systemService.setSkipCredentialsCheck(true);
- this.systemService.fetchModelConfigEnv().subscribe();
- } else {
- this.systemService.setSkipCredentialsCheck(false);
- }
- }
- public toggleFullConfigFile(): void {
- this.showFullConfigFile.update(v => !v);
- }
- public toggleGeminiKeyVisibility(): void {
- this.showGeminiKey.update(v => !v);
- }
- public toggleOcrKeyVisibility(): void {
- this.showOcrKey.update(v => !v);
- }
- public toggleOcrConfig(): void {
- this.showOcrConfig.update(v => !v);
- }
-
- public onGeminiKeyChange(val: string): void {
- this.geminiKeyInput.set(val);
- this.isGeminiKeyEdited.set(true);
- this.geminiSaveError.set(null);
- this.geminiSaveMessage.set(null);
- }
-
- public onOcrKeyChange(val: string): void {
- this.ocrKeyInput.set(val);
- this.isOcrKeyEdited.set(true);
- this.ocrSaveError.set(null);
- this.ocrSaveMessage.set(null);
- }
-
- public saveGeminiKey(): void {
- const key = this.geminiKeyInput().trim();
- if (!key) return;
- this.isSavingGeminiKey.set(true);
- this.geminiSaveError.set(null);
- this.geminiSaveMessage.set(null);
-
- this.systemService.updateApiKey('google', key, true).subscribe({
- next: (res) => {
- this.isSavingGeminiKey.set(false);
- this.isGeminiKeyEdited.set(false);
- this.geminiSaveMessage.set(res?.message || '✓ Gemini API key verified & saved successfully.');
- setTimeout(() => this.geminiSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isSavingGeminiKey.set(false);
- this.geminiSaveError.set(err?.error?.detail || err?.message || 'Failed to update Gemini API key.');
- }
- });
- }
-
- public clearGeminiKey(): void {
- this.geminiKeyInput.set('');
- this.isGeminiKeyEdited.set(false);
- this.geminiSaveError.set(null);
- this.geminiSaveMessage.set(null);
-
- if (this.savedGeminiKey().trim()) {
- this.isSavingGeminiKey.set(true);
- this.systemService.updateApiKey('google', '', true).subscribe({
- next: (res) => {
- this.isSavingGeminiKey.set(false);
- this.geminiSaveMessage.set(res?.message || '✓ Gemini API key cleared.');
- setTimeout(() => this.geminiSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isSavingGeminiKey.set(false);
- this.geminiSaveError.set(err?.error?.detail || err?.message || 'Failed to clear Gemini API key.');
- }
- });
- } else {
- this.geminiSaveMessage.set('✓ Gemini API key cleared.');
- setTimeout(() => this.geminiSaveMessage.set(null), 3000);
- }
- }
-
- public saveOcrKey(): void {
- const key = this.ocrKeyInput().trim();
- if (!key) return;
- this.isSavingOcrKey.set(true);
- this.ocrSaveError.set(null);
- this.ocrSaveMessage.set(null);
-
- this.systemService.updateApiKey('ocr', key, true).subscribe({
- next: (res) => {
- this.isSavingOcrKey.set(false);
- this.isOcrKeyEdited.set(false);
- this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key verified & saved.');
- setTimeout(() => this.ocrSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isSavingOcrKey.set(false);
- this.ocrSaveError.set(err?.error?.detail || err?.message || 'Failed to update Vision OCR key.');
- }
- });
- }
-
- public clearOcrKey(): void {
- this.ocrKeyInput.set('');
- this.isOcrKeyEdited.set(false);
- this.ocrSaveError.set(null);
- this.ocrSaveMessage.set(null);
-
- if (this.savedOcrKey().trim()) {
- this.isSavingOcrKey.set(true);
- this.systemService.updateApiKey('ocr', '', true).subscribe({
- next: (res) => {
- this.isSavingOcrKey.set(false);
- this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key cleared.');
- setTimeout(() => this.ocrSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isSavingOcrKey.set(false);
- this.ocrSaveError.set(err?.error?.detail || err?.message || 'Failed to clear Vision OCR key.');
- }
- });
- } else {
- this.ocrSaveMessage.set('✓ Vision OCR API key cleared.');
- setTimeout(() => this.ocrSaveMessage.set(null), 3000);
- }
- }
-
- public testGeminiKey(): void {
- const key = this.geminiKeyInput().trim();
- if (!key) return;
- this.isTestingGeminiKey.set(true);
- this.geminiSaveError.set(null);
- this.geminiSaveMessage.set(null);
-
- this.systemService.testApiKey('google', key).subscribe({
- next: (res) => {
- this.isTestingGeminiKey.set(false);
- if (res?.valid) {
- this.geminiSaveMessage.set(res?.message || '✓ Gemini API key is valid!');
- } else {
- this.geminiSaveError.set(res?.message || 'Gemini API key verification failed.');
- }
- setTimeout(() => this.geminiSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isTestingGeminiKey.set(false);
- this.geminiSaveError.set(err?.error?.detail || err?.message || 'Gemini API key test failed.');
- }
- });
- }
-
- public testOcrKey(): void {
- const key = this.ocrKeyInput().trim();
- if (!key) return;
- this.isTestingOcrKey.set(true);
- this.ocrSaveError.set(null);
- this.ocrSaveMessage.set(null);
-
- this.systemService.testApiKey('ocr', key).subscribe({
- next: (res) => {
- this.isTestingOcrKey.set(false);
- if (res?.valid) {
- this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key is valid!');
- } else {
- this.ocrSaveError.set(res?.message || 'Vision OCR API key verification failed.');
- }
- setTimeout(() => this.ocrSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isTestingOcrKey.set(false);
- this.ocrSaveError.set(err?.error?.detail || err?.message || 'Vision OCR API key test failed.');
- }
- });
- }
-
- public getProviderDisplayName(tab: string): string {
- switch (tab) {
- case 'gemini': return 'Gemini';
- case 'ocr': return 'Vision OCR';
- default: return tab;
- }
- }
-
- public getProviderEnvVar(tab: string): string {
- switch (tab) {
- case 'gemini': return 'GEMINI_API_KEY';
- case 'ocr': return 'GOOGLE_VISION_API_KEY';
- default: return 'API_KEY';
- }
- }
-
- public getProviderHint(tab: string): string {
- switch (tab) {
- case 'gemini':
- return 'For a quick start, Google Gemini provides a free API key. Artemis also supports other models (OpenAI, Claude, OpenRouter, etc.)—you can configure your own API keys directly in .env or your environment.';
- case 'ocr':
- return 'Google Cloud Vision API key for on-screen OCR text detection and UI grounding.';
- default:
- return 'Configure your API key or use environment definitions.';
- }
- }
public skipCredentialsCheck(): void {
this.systemService.skipCredentialsCheck();
}
- public getApiKeyPlaceholder(tab: string): string {
- switch (tab) {
- case 'gemini': return 'Enter Gemini API Key (e.g. AIzaSy...)';
- case 'ocr': return 'Enter Vision OCR API Key (e.g. AIzaSy...)';
- default: return 'Enter API Key...';
- }
- }
-
- public isTabProviderActive(tab: string): boolean {
- if (tab === 'ocr') {
- return this.isOcrConfigured();
- }
- const targetProvider = tab === 'gemini' ? 'google' : tab;
- const providers = this.configuredLlmProviders();
- return providers.some(p => p.provider === targetProvider);
- }
-
public refreshReadiness(): void {
this.isRefreshingDiagnostics.set(true);
this.systemService.fetchReadiness(false, true).subscribe({
@@ -829,12 +549,12 @@ export class HomeComponent implements OnInit, OnDestroy {
this.systemService.restartAdb().subscribe({
next: (res) => {
this.adbRestartFeedback.set(
- res?.restart_result?.skipped ? 'Devices Refreshed ✓' : 'ADB Refreshed ✓'
+ res?.restart_result?.skipped ? "设备已刷新 ✓" : "ADB 已刷新 ✓"
);
setTimeout(() => this.adbRestartFeedback.set(null), 2500);
},
error: () => {
- this.adbRestartFeedback.set('Restart Failed');
+ this.adbRestartFeedback.set("重启失败");
setTimeout(() => this.adbRestartFeedback.set(null), 3000);
}
});
@@ -842,7 +562,7 @@ export class HomeComponent implements OnInit, OnDestroy {
public connectWifiDevice(): void {
if (this.isRemoteAdbServer()) {
- this.wifiConnectError.set('Switch to local ADB before connecting a Wireless ADB device.');
+ this.wifiConnectError.set("连接无线设备前,请先切换至本地 ADB。");
return;
}
const host = this.wifiHost().trim();
@@ -850,7 +570,7 @@ export class HomeComponent implements OnInit, OnDestroy {
const port = parseInt(portStr, 10) || 5555;
if (!host) {
- this.wifiConnectError.set('Please enter a valid IP address.');
+ this.wifiConnectError.set("请输入有效的 IP 地址。");
return;
}
@@ -863,15 +583,15 @@ export class HomeComponent implements OnInit, OnDestroy {
this.isConnectingWifi.set(false);
const cr = res?.connect_result;
if (cr?.success) {
- this.wifiConnectMessage.set(`Connected to ${host}:${port}!`);
+ this.wifiConnectMessage.set(`已连接至 ${host}:${port}!`);
setTimeout(() => this.wifiConnectMessage.set(null), 4000);
} else {
- this.wifiConnectError.set(cr?.message || 'Connection failed. Please check phone IP & Wi-Fi.');
+ this.wifiConnectError.set(cr?.message || "连接失败,请检查手机 IP 地址和 Wi-Fi。");
}
},
error: (err) => {
this.isConnectingWifi.set(false);
- this.wifiConnectError.set(err?.error?.detail || 'Failed to connect. Please check adb connection.');
+ this.wifiConnectError.set(err?.error?.detail || "连接失败,请检查 ADB 连接。");
}
});
}
@@ -881,11 +601,11 @@ export class HomeComponent implements OnInit, OnDestroy {
const port = Number(this.remoteAdbPort().trim());
if (!host) {
- this.remoteAdbError.set('Enter the host name or IP address of the ADB server.');
+ this.remoteAdbError.set("请输入 ADB 服务器的主机名或 IP 地址。");
return;
}
if (!Number.isInteger(port) || port < 1 || port > 65535) {
- this.remoteAdbError.set('Enter a port between 1 and 65535.');
+ this.remoteAdbError.set("请输入 1–65535 之间的端口。");
return;
}
@@ -910,7 +630,7 @@ export class HomeComponent implements OnInit, OnDestroy {
error: error => {
this.isConnectingRemoteAdb.set(false);
this.remoteAdbError.set(
- error?.error?.detail || 'Unable to test the ADB server endpoint.'
+ error?.error?.detail || "无法测试 ADB 服务器连接。"
);
}
});
@@ -919,7 +639,7 @@ export class HomeComponent implements OnInit, OnDestroy {
public activateRemoteAdbServer(): void {
const tested = this.remoteAdbProbeResult();
if (!tested?.success) {
- this.remoteAdbError.set('Test the endpoint before using it.');
+ this.remoteAdbError.set("请先测试服务器连接。");
return;
}
@@ -944,7 +664,7 @@ export class HomeComponent implements OnInit, OnDestroy {
error: error => {
this.isActivatingRemoteAdb.set(false);
this.remoteAdbError.set(
- error?.error?.detail || 'Unable to use the ADB server endpoint.'
+ error?.error?.detail || "无法使用此 ADB 服务器。"
);
}
});
@@ -979,7 +699,7 @@ export class HomeComponent implements OnInit, OnDestroy {
error: error => {
this.isSwitchingToLocalAdb.set(false);
this.remoteAdbError.set(
- error?.error?.detail || 'Unable to switch back to the local ADB server.'
+ error?.error?.detail || "无法切换回本地 ADB 服务器。"
);
}
});
@@ -1069,12 +789,12 @@ export class HomeComponent implements OnInit, OnDestroy {
public runTask(): void {
const goal = this.taskGoal().trim();
if (!goal) {
- this.errorMessage.set('Please enter a task goal before running.');
+ this.errorMessage.set("请先输入任务目标。");
return;
}
if (!this.isReady()) {
- this.errorMessage.set('System prerequisites are not satisfied. Please review System Setup first.');
+ this.errorMessage.set("运行条件尚未满足,请先检查系统设置。");
this.activeTab.set('diagnostics');
return;
}
@@ -1089,7 +809,7 @@ export class HomeComponent implements OnInit, OnDestroy {
this.selectedProfile() === 'pro' && this.expectedOutput().trim()
? this.expectedOutput().trim()
: undefined,
- this.selectedProfile() === 'pro' ? this.enableOutputter() : undefined,
+ undefined,
this.selectedProfile() === 'pro'
? { verificationLevel: this.verificationLevel().id, explorerMode: this.explorerMode().id }
: undefined
@@ -1103,7 +823,7 @@ export class HomeComponent implements OnInit, OnDestroy {
console.error('Failed to submit task from home page:', err);
this.isSubmitting.set(false);
this.errorMessage.set(
- err?.error?.detail || 'Failed to submit task. Please check server connection.'
+ err?.error?.detail || "提交任务失败,请检查服务器连接。"
);
}
});
diff --git a/apps/showcase_ui/src/app/pages/workspace/workspace.component.html b/apps/showcase_ui/src/app/pages/workspace/workspace.component.html
index a44cd84..fedc937 100644
--- a/apps/showcase_ui/src/app/pages/workspace/workspace.component.html
+++ b/apps/showcase_ui/src/app/pages/workspace/workspace.component.html
@@ -27,7 +27,7 @@ limitations under the License.
error
{{ errorMessage() }}
-
+
close
@@ -89,7 +89,7 @@ limitations under the License.
- Start a new task...
+ 开始新任务…
@@ -104,7 +104,7 @@ limitations under the License.
(blur)="onInputBlur()"
rows="1"
[disabled]="isSubmitting()"
- [placeholder]="selectedProfile() === 'pro'
+ [placeholder]="selectedProfile() === 'pro'
? 'Pro: plans first, checks results and recovers from blocked steps. For long or complex tasks'
: 'Flash: fast single-look loop for everyday app tasks'"
class="dock-textarea"
@@ -115,7 +115,7 @@ limitations under the License.
@if (taskInput.trim() && !isSubmitting()) {
+ (click)="clearInput($event)" title="清空输入" aria-label="清空输入">
close
}
@@ -137,7 +137,7 @@ limitations under the License.
[disabled]="!taskInput.trim() || isSubmitting()"
(click)="submitTask()"
[title]="isSubmitting() ? 'Starting task...' : 'Start task (Enter)'"
- aria-label="Start task"
+ aria-label="开始任务"
>
@if (isSubmitting()) {
sync
@@ -152,16 +152,16 @@ limitations under the License.