Files
Artemis-Eapil/apps/showcase_ui/src/app/pages/home/home.component.html
T

1689 lines
88 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
Copyright 2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="home-container" [class.system-ready]="isReady()" [class.system-pending]="!isReady()" [class.pro-theme-active]="selectedProfile() === 'pro'">
<!-- Dynamic Aurora Glow Background -->
<div class="aurora-background">
<div class="glow glow-1"></div>
<div class="glow glow-2"></div>
<div class="glow glow-3"></div>
<div class="glow glow-pending"></div>
<div class="glow glow-ready"></div>
<div class="glow glow-pro"></div>
</div>
<!-- Main Content Layout -->
<main class="home-content">
<!-- Task entry section -->
<section class="hero-section">
<h1 class="hero-title">
你想在{{ pcOnly() ? '电脑' : '手机' }}上完成什么任务?
</h1>
</section>
<!-- Top View Mode Stepper / Tab Switcher (Frosted Glass Segmented Control matching top-left nav) -->
<div class="mode-tabs-container">
<div class="mode-tabs">
<button
type="button"
class="mode-tab-btn"
[class.active]="activeTab() === 'diagnostics'"
(click)="setTab('diagnostics')"
>
@if (isReady()) {
<span class="material-symbols-outlined tab-icon icon-ready">check_circle</span>
} @else {
<span class="material-symbols-outlined tab-icon">tune</span>
}
<span class="tab-label">系统设置与运行准备</span>
</button>
<button
type="button"
class="mode-tab-btn"
[class.active]="activeTab() === 'launcher'"
(click)="setTab('launcher')"
>
@if (!isReady()) {
<span class="material-symbols-outlined tab-icon icon-locked">lock</span>
} @else {
<span class="material-symbols-outlined tab-icon">rocket_launch</span>
}
<span class="tab-label">启动自动化任务</span>
</button>
</div>
</div>
<!-- ========================================================================= -->
<!-- VIEW 1: SYSTEM SETUP & FOOLPROOF STEP-BY-STEP GUIDANCE -->
<!-- ========================================================================= -->
@if (activeTab() === 'diagnostics') {
<section class="setup-guide-view">
<!-- =================================================================== -->
<!-- STEP 1: ENVIRONMENT & SYSTEM DEPENDENCIES (FIRST & FOUNDATIONAL) -->
<!-- =================================================================== -->
<div class="guide-card" [class.card-pass]="isEnvironmentReady()" [class.card-warn]="!isEnvironmentReady()">
<div class="guide-card-header">
<div class="step-badge" [class.step-pass]="isEnvironmentReady()">
@if (isEnvironmentReady()) {
<span class="material-symbols-outlined step-check-icon">check</span>
} @else {
<span>1</span>
}
</div>
<div class="step-info">
<div class="step-title-row">
<h3 class="step-title">系统与环境</h3>
</div>
<p class="step-desc">基础工具、Android 调试桥与录屏环境</p>
</div>
<button
type="button"
class="btn-header-action"
[disabled]="isRefreshingDiagnostics()"
(click)="refreshReadiness()"
title="重新检查全部系统依赖"
>
<span class="material-symbols-outlined" [class.spinning]="isRefreshingDiagnostics()">refresh</span>
<span>{{ isRefreshingDiagnostics() ? '正在检查…' : '重新检查' }}</span>
</button>
</div>
<div class="guide-card-body">
<!-- One-Click Dependency Installer Bar when any env dep is missing -->
@if (hasReadinessReport() && !isEnvironmentReady()) {
<div class="one-click-install-bar">
<div class="install-bar-left">
<div class="install-bar-icon-box">
<span class="material-symbols-outlined bolt-icon">bolt</span>
</div>
<div class="install-bar-text">
<div class="install-bar-title-row">
<div class="install-bar-title">自动安装依赖</div>
<div class="os-selector-pills">
<button type="button" class="os-pill" [class.active]="effectiveOs() === 'linux'"
(click)="setSelectedOs('linux')">Linux</button>
<button type="button" class="os-pill" [class.active]="effectiveOs() === 'darwin'"
(click)="setSelectedOs('darwin')">macOS</button>
<button type="button" class="os-pill" [class.active]="effectiveOs() === 'windows'"
(click)="setSelectedOs('windows')">Windows</button>
</div>
</div>
<div class="install-bar-desc">在终端运行以下命令,安装 ADB、FFmpeg、scrcpy 和项目依赖:</div>
</div>
</div>
<div class="code-box small-box">
<code>{{ oneClickSetupCmd() }}</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'one_click_deps'"
(click)="copyToClipboard(oneClickSetupCmd(), 'one_click_deps')">
<span class="material-symbols-outlined">{{ copiedId() === 'one_click_deps' ? 'check' : 'content_copy'
}}</span>
<span>{{ copiedId() === 'one_click_deps' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
}
<!-- 4-Card Environment Matrix Grid -->
<div class="env-matrix-grid">
<!-- 1. Python Runtime Probe Card -->
<div class="env-item-card" [class.item-pass]="pythonProbe()?.status === 'pass'"
[class.item-fail]="pythonProbe()?.status === 'fail'">
<div class="item-header">
<div class="item-icon-box">
<span class="material-symbols-outlined item-icon">terminal</span>
</div>
<div class="item-title-group">
<div class="item-name">Python 运行环境</div>
<div class="item-subtitle">{{ pythonProbe()?.summary || '正在检查…' }}</div>
</div>
<div class="item-status">
<span class="status-dot" [class.dot-pass]="pythonProbe()?.status === 'pass'" [class.dot-fail]="pythonProbe()?.status === 'fail'"></span>
</div>
</div>
@if (pythonProbe()?.status === 'fail') {
<div class="item-body">
<div class="item-action-box">
<div class="code-box small-box">
<span class="terminal-prefix">$</span>
<code class="code-text">uv python install 3.12</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'py_install'"
(click)="copyToClipboard('uv python install 3.12', 'py_install')">
<span class="material-symbols-outlined">{{ copiedId() === 'py_install' ? 'check' : 'content_copy'
}}</span>
<span>{{ copiedId() === 'py_install' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
}
</div>
<!-- 2. Android ADB Platform-Tools Card -->
<div class="env-item-card" [class.item-pass]="adbProbe()?.metadata?.['installed']"
[class.item-fail]="adbProbe()?.status === 'fail'">
<div class="item-header">
<div class="item-icon-box">
<span class="material-symbols-outlined item-icon">construction</span>
</div>
<div class="item-title-group">
<div class="item-name">Android 平台工具</div>
<div class="item-subtitle">{{ !adbProbe() ? '正在检查…' : (adbProbe()?.metadata?.['installed'] ? 'ADB 已安装' : '缺少 ADB') }}</div>
</div>
<div class="item-status">
<span class="status-dot" [class.dot-pass]="adbProbe()?.metadata?.['installed']" [class.dot-fail]="adbProbe()?.status === 'fail'"></span>
</div>
</div>
@if (adbProbe()?.status === 'fail') {
<div class="item-body">
<div class="item-action-box">
<div class="code-box small-box">
<span class="terminal-prefix">$</span>
<code class="code-text">{{ adbInstallCmd() }}</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'env_adb_cmd'"
(click)="copyToClipboard(adbInstallCmd(), 'env_adb_cmd')">
<span class="material-symbols-outlined">{{ copiedId() === 'env_adb_cmd' ? 'check' : 'content_copy'
}}</span>
<span>{{ copiedId() === 'env_adb_cmd' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
}
</div>
<!-- 3. System Configuration Card -->
<div class="env-item-card" [class.item-pass]="configProbe()?.status === 'pass'"
[class.item-fail]="configProbe()?.status === 'fail'">
<div class="item-header">
<div class="item-icon-box">
<span class="material-symbols-outlined item-icon">tune</span>
</div>
<div class="item-title-group">
<div class="item-name">系统配置</div>
<div class="item-subtitle">{{ configProbe()?.summary || '配置有效' }}</div>
</div>
<div class="item-status">
<span class="status-dot" [class.dot-pass]="configProbe()?.status === 'pass'" [class.dot-fail]="configProbe()?.status === 'fail'"></span>
</div>
</div>
@if (configProbe()?.status === 'fail') {
<div class="item-body">
<div class="item-action-box">
<div class="code-box small-box">
<span class="terminal-prefix">$</span>
<code class="code-text">artemis init</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'cfg_init'"
(click)="copyToClipboard('artemis init', 'cfg_init')">
<span class="material-symbols-outlined">{{ copiedId() === 'cfg_init' ? 'check' : 'content_copy'
}}</span>
<span>{{ copiedId() === 'cfg_init' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
}
</div>
<!-- 4. Video Recording Toolchain Card (FFmpeg & scrcpy) -->
<div class="env-item-card" [class.item-pass]="toolchainProbe()?.status === 'pass'"
[class.item-fail]="toolchainProbe()?.status !== 'pass'">
<div class="item-header">
<div class="item-icon-box">
<span class="material-symbols-outlined item-icon">videocam</span>
</div>
<div class="item-title-group">
<div class="item-name">录屏工具</div>
<div class="item-subtitle">{{ toolchainProbe()?.summary || 'FFmpeg & scrcpy' }}</div>
</div>
<div class="item-status">
<span class="status-dot" [class.dot-pass]="toolchainProbe()?.status === 'pass'" [class.dot-fail]="toolchainProbe()?.status !== 'pass'"></span>
</div>
</div>
@if (toolchainProbe()?.status !== 'pass') {
<div class="item-body">
<div class="item-action-box">
<div class="code-box small-box">
<span class="terminal-prefix">$</span>
<code class="code-text">{{ toolchainInstallCmd() }}</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'install_toolchain'"
(click)="copyToClipboard(toolchainInstallCmd(), 'install_toolchain')">
<span class="material-symbols-outlined">{{ copiedId() === 'install_toolchain' ? 'check' : 'content_copy'
}}</span>
<span>{{ copiedId() === 'install_toolchain' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
<!-- =================================================================== -->
<!-- STEP 2: AI MODEL SETUP -->
<!-- =================================================================== -->
<div class="guide-card" [class.card-pass]="isCredentialsReady()" [class.card-warn]="!isCredentialsReady()">
<div class="guide-card-header">
<div class="step-badge" [class.step-pass]="isCredentialsReady()">
@if (isCredentialsReady()) {
<span class="material-symbols-outlined step-check-icon">check</span>
} @else {
<span>2</span>
}
</div>
<div class="step-info">
<div class="step-title-row">
<h3 class="step-title">模型配置</h3>
</div>
<p class="step-desc">OpenAI Responses 接口与模型设置</p>
</div>
</div>
<div class="guide-card-body">
<app-model-settings (saved)="refreshReadiness()" />
<app-capability-settings />
</div>
</div>
<!-- =================================================================== -->
<!-- STEP 3: ANDROID DEVICE & EMULATOR CONNECTION -->
<!-- =================================================================== -->
<div class="guide-card" [class.card-pass]="isDeviceReady()" [class.card-warn]="!isDeviceReady()">
<div class="guide-card-header">
<div class="step-badge" [class.step-pass]="isDeviceReady()">
@if (isDeviceReady()) {
<span class="material-symbols-outlined step-check-icon">check</span>
} @else {
<span>3</span>
}
</div>
<div class="step-info">
<div class="step-title-row">
<h3 class="step-title">Android 设备与模拟器连接</h3>
</div>
<p class="step-desc">连接本地模拟器、USB 手机、无线设备或远程 ADB 服务器</p>
</div>
<button
type="button"
class="btn-restart-adb btn-connection-options"
(click)="toggleConnectionMethods()"
>
<span class="material-symbols-outlined">swap_horiz</span>
<span>{{ showConnectionMethods() ? '收起选项' : '切换连接' }}</span>
</button>
<button
type="button"
class="btn-restart-adb"
[disabled]="isRestartingAdb()"
(click)="restartAdbServer()"
[title]="isRemoteAdbServer() ? '刷新远程 ADB 设备列表' : '重启本地 ADB 服务'"
>
@if (isRestartingAdb()) {
<span class="material-symbols-outlined spinning">sync</span>
<span>正在重启…</span>
} @else if (adbRestartFeedback()) {
<span class="material-symbols-outlined feedback-done">check</span>
<span class="feedback-text">{{ adbRestartFeedback() }}</span>
} @else {
<span class="material-symbols-outlined">sync</span>
<span>{{ isRemoteAdbServer() ? '刷新设备' : '重启 ADB' }}</span>
}
</button>
</div>
<div class="guide-card-body">
<!-- STATE A: CONNECTED & READY -->
@if (isDeviceReady()) {
<div class="connected-device-panel">
<div class="device-hero-card">
<div class="device-avatar">
<span class="material-symbols-outlined device-hero-icon">
phone_android
</span>
</div>
<div class="device-hero-details">
<div class="device-hero-name">
{{ activeDevice()?.model || activeDevice()?.serial }}
</div>
<div class="device-hero-meta">
@if (activeDevice()?.is_emulator) {
<span>虚拟设备</span>
<span class="meta-dot">·</span>
}
<span>{{ activeDevice()?.serial }}</span>
@if (activeDevice()?.screen_resolution) {
<span class="meta-dot">·</span>
<span>{{ activeDevice()?.screen_resolution }}</span>
}
@if (activeDevice()?.android_version) {
<span class="meta-dot">·</span>
<span>Android {{ activeDevice()?.android_version }}</span>
}
@if (isRemoteAdbServer()) {
<span class="meta-dot">·</span>
<span class="remote-endpoint-label">
Remote via {{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }}
</span>
}
</div>
</div>
<div class="device-hero-status">
<span class="status-dot dot-pass"></span>
<span class="status-text">已连接</span>
</div>
</div>
<!-- Multi-Device Switcher if multiple devices detected -->
@if (connectedDevices().length > 1) {
<div class="multi-device-switcher">
<span class="switcher-label">选择当前设备:</span>
<div class="switcher-chips">
@for (dev of connectedDevices(); track dev.serial) {
<button
type="button"
class="device-chip-btn"
[class.selected]="dev.serial === activeDevice()?.serial"
(click)="selectTargetDevice(dev.serial)"
>
<span class="material-symbols-outlined chip-icon">
phone_android
</span>
<span>{{ dev.model || dev.serial }}</span>
@if (dev.serial === activeDevice()?.serial) {
<span class="active-check">✓ 当前设备</span>
}
</button>
}
</div>
</div>
}
</div>
}
<!-- STATE B-1: EMULATOR LAUNCHING / DEVICE BOOTING LIVE TRACKER -->
@if (isEmulatorLaunching() || (!isDeviceReady() && adbProbe()?.summary === 'Device Booting')) {
<div class="emulator-boot-tracker-container">
<div class="emulator-boot-tracker-card">
<div class="tracker-header">
<div class="tracker-title-group">
<div class="tracker-icon-box">
<span class="material-symbols-outlined spinning-icon">sync</span>
</div>
<div class="tracker-titles">
<div class="tracker-main-heading">
<h4 class="tracker-title">
正在启动 Android 模拟器: <span class="avd-highlight">{{ emulatorLaunchState()?.avd_name || activeDevice()?.serial || '虚拟设备' }}</span>
</h4>
<span class="stage-tag-badge">{{ emulatorLaunchState()?.status === 'booting' ? 'OS Booting' : 'Starting' }}</span>
</div>
<p class="tracker-subtitle">{{ emulatorLaunchState()?.stage_message || 'Initializing Android Virtual Device...' }}</p>
</div>
</div>
<div class="tracker-actions">
<button type="button" class="btn-toggle-logs" [class.active]="showLaunchLogs()" (click)="toggleLaunchLogs()">
<span class="material-symbols-outlined">terminal</span>
<span>{{ showLaunchLogs() ? 'Hide Logs' : 'View Boot Logs' }}</span>
</button>
<button type="button" class="btn-stop-emu" (click)="stopEmulator()" title="终止模拟器进程">
<span class="material-symbols-outlined">cancel</span>
<span>停止</span>
</button>
</div>
</div>
<!-- Boot Stepper Progress Flow -->
<div class="boot-stepper">
<div class="step-item" [class.done]="emulatorLaunchState()?.progress_percent! >= 35" [class.active]="emulatorLaunchState()?.status === 'starting'">
<div class="step-circle">
@if (emulatorLaunchState()?.progress_percent! >= 35) {
<span class="material-symbols-outlined check-icon">check</span>
} @else {
<span class="spinner-tiny"></span>
}
</div>
<span class="step-label">1. 启动进程</span>
</div>
<div class="step-line" [class.done]="emulatorLaunchState()?.progress_percent! >= 35"></div>
<div class="step-item" [class.done]="emulatorLaunchState()?.progress_percent! >= 65" [class.active]="emulatorLaunchState()?.status === 'waiting_for_adb'">
<div class="step-circle">
@if (emulatorLaunchState()?.progress_percent! >= 65) {
<span class="material-symbols-outlined check-icon">check</span>
} @else if (emulatorLaunchState()?.status === 'waiting_for_adb') {
<span class="spinner-tiny"></span>
} @else {
<span>2</span>
}
</div>
<span class="step-label">2. ADB 连接</span>
</div>
<div class="step-line" [class.done]="emulatorLaunchState()?.progress_percent! >= 65"></div>
<div class="step-item" [class.done]="emulatorLaunchState()?.status === 'ready'" [class.active]="emulatorLaunchState()?.status === 'booting' || adbProbe()?.summary === 'Device Booting'">
<div class="step-circle">
@if (emulatorLaunchState()?.status === 'ready') {
<span class="material-symbols-outlined check-icon">check</span>
} @else if (emulatorLaunchState()?.status === 'booting' || adbProbe()?.summary === 'Device Booting') {
<span class="spinner-tiny"></span>
} @else {
<span>3</span>
}
</div>
<span class="step-label">3. 系统启动</span>
</div>
<div class="step-line" [class.done]="emulatorLaunchState()?.status === 'ready'"></div>
<div class="step-item" [class.done]="emulatorLaunchState()?.status === 'ready'">
<div class="step-circle">
<span class="material-symbols-outlined check-icon">check_circle</span>
</div>
<span class="step-label">4. 就绪</span>
</div>
</div>
<!-- Animated Progress Bar -->
<div class="progress-bar-container">
<div class="progress-bar-track">
<div class="progress-bar-fill" [style.width.%]="emulatorLaunchState()?.progress_percent || 25"></div>
</div>
<div class="progress-bar-meta">
<span class="progress-percent">{{ emulatorLaunchState()?.progress_percent || 25 }}% Complete</span>
<span class="progress-timer">
<span class="material-symbols-outlined timer-icon">timer</span>
Elapsed: {{ emulatorLaunchState()?.elapsed_seconds || 0 }}s
</span>
</div>
</div>
<!-- Helpful Status Notice -->
<div class="boot-tip-banner">
<span class="material-symbols-outlined tip-icon">info</span>
<span>Android 模拟器冷启动通常需要 20–45 秒。上方会显示实时进度,请勿重复点击启动。</span>
</div>
<!-- Expandable Console Log Stream -->
@if (showLaunchLogs()) {
<div class="boot-logs-drawer">
<div class="logs-header">
<div class="logs-header-left">
<span class="material-symbols-outlined">terminal</span>
<span>模拟器控制台输出(stdout / stderr)</span>
</div>
<button type="button" class="btn-copy-logs" (click)="copyToClipboard(emulatorLaunchState()?.logs?.join('\n') || '', 'boot_logs')">
<span class="material-symbols-outlined">{{ copiedId() === 'boot_logs' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'boot_logs' ? 'Copied' : 'Copy Logs' }}</span>
</button>
</div>
<pre class="logs-body">@if (emulatorLaunchState()?.logs?.length) {@for (log of emulatorLaunchState()?.logs; track log) {<div class="log-line">{{ log }}</div>}} @else {<div class="log-empty">正在等待进程输出…</div>}</pre>
</div>
}
</div>
</div>
}
<!-- STATE B-2: EMULATOR LAUNCH FAILED DIAGNOSTIC CARD -->
@if (!isDeviceReady() && !isEmulatorLaunching() && emulatorLaunchState()?.status === 'failed') {
<div class="emulator-failed-container">
<div class="emulator-failed-card">
<div class="failed-header">
<div class="failed-header-left">
<div class="failed-icon-box">
<span class="material-symbols-outlined failed-icon">error</span>
</div>
<div class="failed-titles">
<h4 class="failed-title">模拟器启动失败</h4>
<p class="failed-desc">{{ emulatorLaunchState()?.error || 'The Android emulator process failed to start or crashed during initialization.' }}</p>
</div>
</div>
<button type="button" class="btn-dismiss-failed" (click)="dismissEmulatorStatus()" title="关闭错误提示">
<span class="material-symbols-outlined">close</span>
</button>
</div>
@if (emulatorLaunchState()?.logs?.length) {
<div class="failed-logs-box">
<div class="logs-header">
<span>进程输出与错误日志:</span>
<button type="button" class="btn-copy-logs" (click)="copyToClipboard(emulatorLaunchState()?.logs?.join('\n') || '', 'fail_logs')">
<span class="material-symbols-outlined">{{ copiedId() === 'fail_logs' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'fail_logs' ? 'Copied' : 'Copy Logs' }}</span>
</button>
</div>
<pre class="failed-logs-pre">@for (log of emulatorLaunchState()?.logs; track log) {<div class="log-line">{{ log }}</div>}</pre>
</div>
}
<div class="failed-actions-bar">
@if (emulatorLaunchState()?.avd_name) {
<button type="button" class="btn-retry-launch" [disabled]="isRemoteAdbServer()" (click)="launchAvdEmulator(emulatorLaunchState()!.avd_name!)">
<span class="material-symbols-outlined">refresh</span>
<span>Retry Launch ({{ emulatorLaunchState()?.avd_name }})</span>
</button>
}
<button type="button" class="btn-restart-adb-sub" (click)="restartAdbServer()">
<span class="material-symbols-outlined">sync</span>
<span>重启 ADB 服务器</span>
</button>
<button type="button" class="btn-dismiss-flat" (click)="dismissEmulatorStatus()">
<span>关闭提示</span>
</button>
</div>
</div>
</div>
}
<!-- STATE B-3: NO DEVICE DETECTED / READY TO CONNECT GUIDE -->
@if (showConnectionMethods() || (!isDeviceReady() && !isEmulatorLaunching() && adbProbe()?.summary !== 'Device Booting' && adbProbe()?.summary !== 'Device Unauthorized' && adbProbe()?.summary !== 'Device Locked' && adbProbe()?.summary !== 'Lock State Unknown' && adbProbe()?.status !== 'fail' && emulatorLaunchState()?.status !== 'failed')) {
<div class="foolproof-guide-container">
<!-- Method Selection Nav Cards -->
<div class="connection-methods-nav">
<button
type="button"
class="method-nav-card"
[class.active]="activeAdbGuideTab() === 'emulator'"
(click)="setAdbGuideTab('emulator')"
>
<div class="method-card-left">
<div class="method-icon-box">
<span class="material-symbols-outlined">smartphone</span>
</div>
<div class="method-info">
<div class="method-title-row">
<span class="method-title">Android 模拟器</span>
</div>
<span class="method-desc">
@if (installedAvds().length > 0) {
{{ installedAvds().length }} 个本地虚拟设备可用
} @else {
启动本地 Android 虚拟设备
}
</span>
</div>
</div>
<div class="method-card-right">
<span class="radio-indicator" [class.checked]="activeAdbGuideTab() === 'emulator'"></span>
</div>
</button>
<button
type="button"
class="method-nav-card"
[class.active]="activeAdbGuideTab() === 'usb'"
(click)="setAdbGuideTab('usb')"
>
<div class="method-card-left">
<div class="method-icon-box">
<span class="material-symbols-outlined">usb</span>
</div>
<div class="method-info">
<div class="method-title-row">
<span class="method-title">USB 连接</span>
</div>
<span class="method-desc">通过 USB 连接实体手机</span>
</div>
</div>
<div class="method-card-right">
<span class="radio-indicator" [class.checked]="activeAdbGuideTab() === 'usb'"></span>
</div>
</button>
<button
type="button"
class="method-nav-card"
[class.active]="activeAdbGuideTab() === 'wifi'"
(click)="setAdbGuideTab('wifi')"
>
<div class="method-card-left">
<div class="method-icon-box">
<span class="material-symbols-outlined">wifi</span>
</div>
<div class="method-info">
<div class="method-title-row">
<span class="method-title">无线 ADB</span>
</div>
<span class="method-desc">通过局域网 Wi-Fi 连接</span>
</div>
</div>
<div class="method-card-right">
<span class="radio-indicator" [class.checked]="activeAdbGuideTab() === 'wifi'"></span>
</div>
</button>
<button
type="button"
class="method-nav-card"
[class.active]="activeAdbGuideTab() === 'remote'"
(click)="setAdbGuideTab('remote')"
>
<div class="method-card-left">
<div class="method-icon-box">
<span class="material-symbols-outlined">devices</span>
</div>
<div class="method-info">
<div class="method-title-row">
<span class="method-title">远程计算机</span>
</div>
<span class="method-desc">连接其他计算机上的手机</span>
</div>
</div>
<div class="method-card-right">
<span class="radio-indicator" [class.checked]="activeAdbGuideTab() === 'remote'"></span>
</div>
</button>
</div>
@if (activeAdbGuideTab() !== 'remote' && isRemoteAdbServer()) {
<div class="active-remote-notice">
<div class="active-remote-copy">
<span class="material-symbols-outlined">hub</span>
<span>
Artemis 当前使用
<strong>{{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }}</strong>。使用本地模拟器、USB 或无线 ADB 前,请先切换至本地 ADB。
</span>
</div>
<button
type="button"
class="btn-use-local-adb"
[disabled]="isSwitchingToLocalAdb()"
(click)="switchToLocalAdbServer()"
>
<span class="material-symbols-outlined" [class.spinning]="isSwitchingToLocalAdb()">sync</span>
<span>{{ isSwitchingToLocalAdb() ? 'Switching...' : 'Use Local ADB' }}</span>
</button>
</div>
}
<!-- Method 1: Emulator -->
@if (activeAdbGuideTab() === 'emulator') {
<div class="method-details-panel">
@if (installedAvds().length > 0) {
<div class="avds-section">
<div class="section-header-bar">
<div class="section-title-group">
<span class="material-symbols-outlined header-icon">devices</span>
<span class="section-title">已安装的虚拟设备(AVD)</span>
<span class="count-badge">{{ installedAvds().length }} 个可用</span>
</div>
</div>
<div class="avds-list">
@for (avd of installedAvds(); track avd) {
<div class="avd-card-item">
<div class="avd-item-main">
<div class="avd-avatar">
<span class="material-symbols-outlined">smartphone</span>
</div>
<div class="avd-meta">
<div class="avd-heading">
<span class="avd-name">{{ avd }}</span>
<span class="avd-type-pill">虚拟设备</span>
</div>
</div>
</div>
<div class="avd-item-actions">
<button
type="button"
class="btn-avd-launch"
[class.launching]="launchingAvd() === avd || (isEmulatorLaunching() && emulatorLaunchState()?.avd_name === avd)"
[disabled]="isEmulatorLaunching() || isRemoteAdbServer()"
(click)="launchAvdEmulator(avd)"
[title]="isEmulatorLaunching() ? '正在启动模拟器…' : '启动' + avd"
>
@if (launchingAvd() === avd || (isEmulatorLaunching() && emulatorLaunchState()?.avd_name === avd)) {
<span class="spinner-icon"></span>
<span>{{ emulatorLaunchState()?.status === 'booting' ? 'Booting OS' : 'Starting' }} ({{ emulatorLaunchState()?.elapsed_seconds || 0 }}s)</span>
} @else {
<span class="material-symbols-outlined action-icon">play_arrow</span>
<span>启动</span>
}
</button>
</div>
</div>
}
</div>
</div>
} @else {
<div class="emulator-setup-guide-container">
<div class="empty-avd-banner">
<div class="banner-left">
<div class="empty-avd-icon-box">
<span class="material-symbols-outlined">devices_off</span>
</div>
<div class="banner-titles">
<div class="banner-title-row">
<h4 class="empty-title">未检测到 Android 虚拟设备(AVD)</h4>
<div class="os-selector-pills">
<button type="button" class="os-pill" [class.active]="effectiveOs() === 'linux'"
(click)="setSelectedOs('linux')">Linux</button>
<button type="button" class="os-pill" [class.active]="effectiveOs() === 'darwin'"
(click)="setSelectedOs('darwin')">macOS</button>
<button type="button" class="os-pill" [class.active]="effectiveOs() === 'windows'"
(click)="setSelectedOs('windows')">Windows</button>
</div>
</div>
<p class="empty-desc">按以下步骤安装并启动模拟器:</p>
</div>
</div>
<div class="mode-switch-pills">
<button
type="button"
class="pill-btn"
[class.active]="emulatorSetupMode() === 'studio'"
(click)="setEmulatorSetupMode('studio')"
>
<span class="material-symbols-outlined pill-icon">desktop_windows</span>
<span>Android Studio(图形界面)</span>
</button>
<button
type="button"
class="pill-btn"
[class.active]="emulatorSetupMode() === 'cli'"
(click)="setEmulatorSetupMode('cli')"
>
<span class="material-symbols-outlined pill-icon">terminal</span>
<span>命令行</span>
</button>
</div>
</div>
<!-- Option 1: Android Studio Guide -->
@if (emulatorSetupMode() === 'studio') {
<div class="steps-flow-grid emu-flow-grid">
<div class="flow-step-card">
<div class="step-card-header">
<span class="step-number-badge">1</span>
<span class="material-symbols-outlined step-card-icon">download</span>
</div>
<div class="step-card-title">安装 Android Studio</div>
<div class="step-card-desc">
Download for {{ effectiveOs() === 'darwin' ? 'macOS' : (effectiveOs() === 'windows' ? 'Windows' : 'Linux') }} from <a href="https://developer.android.com/studio" target="_blank" rel="noopener" class="guide-link">developer.android.com/studio</a> 并安装。
</div>
</div>
<div class="flow-step-card">
<div class="step-card-header">
<span class="step-number-badge">2</span>
<span class="material-symbols-outlined step-card-icon">developer_mode</span>
</div>
<div class="step-card-title">打开设备管理器</div>
<div class="step-card-desc">
在 Android Studio 欢迎页面打开 <strong>更多操作 > 虚拟设备管理器</strong> (或 <strong>工具 > 设备管理器</strong>).
</div>
</div>
<div class="flow-step-card">
<div class="step-card-header">
<span class="step-number-badge">3</span>
<span class="material-symbols-outlined step-card-icon">add_circle</span>
</div>
<div class="step-card-title">创建虚拟设备</div>
<div class="step-card-desc">
点击 <strong>创建设备</strong>,选择 <strong>Pixel 8</strong>,选择 <strong>Android 14 (API 34)</strong>,然后点击 <strong>完成</strong>.
</div>
</div>
<div class="flow-step-card">
<div class="step-card-header">
<span class="step-number-badge">4</span>
<span class="material-symbols-outlined step-card-icon">play_arrow</span>
</div>
<div class="step-card-title">启动模拟器</div>
<div class="step-card-desc">
点击 ▶ <strong>播放</strong> 。Artemis 会自动检测并连接设备。
</div>
</div>
</div>
}
<!-- Option 2: Command Line Guide -->
@if (emulatorSetupMode() === 'cli') {
<div class="cli-steps-list">
<div class="cli-step-item">
<div class="step-badge-num">1</div>
<div class="step-content">
<div class="step-title">{{ emuHypervisorTitle() }}</div>
<p class="step-text">{{ emuHypervisorDesc() }}</p>
<div class="code-box small-box">
<span class="terminal-prefix">{{ effectiveOs() === 'windows' ? 'PS>' : '$' }}</span>
<code class="code-text">{{ emuHypervisorCmd() }}</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'emu_kvm'"
(click)="copyToClipboard(emuHypervisorCmd(), 'emu_kvm')">
<span class="material-symbols-outlined">{{ copiedId() === 'emu_kvm' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'emu_kvm' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
<div class="cli-step-item">
<div class="step-badge-num">2</div>
<div class="step-content">
<div class="step-title">下载模拟器与系统镜像(API 34)</div>
<p class="step-text">通过 SDK 管理器安装 Android 14 Google APIs 系统镜像:</p>
<div class="code-box small-box">
<span class="terminal-prefix">{{ effectiveOs() === 'windows' ? 'PS>' : '$' }}</span>
<code class="code-text">{{ emuSdkInstallCmd() }}</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'emu_sdk'"
(click)="copyToClipboard(emuSdkInstallCmd(), 'emu_sdk')">
<span class="material-symbols-outlined">{{ copiedId() === 'emu_sdk' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'emu_sdk' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
<div class="cli-step-item">
<div class="step-badge-num">3</div>
<div class="step-content">
<div class="step-title">创建虚拟设备(Pixel 8 AVD)</div>
<p class="step-text">创建 Android 虚拟设备,名称为 <code>Pixel_8_API_34</code>:</p>
<div class="code-box small-box">
<span class="terminal-prefix">{{ effectiveOs() === 'windows' ? 'PS>' : '$' }}</span>
<code class="code-text">{{ emuCreateAvdCmd() }}</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'emu_create'"
(click)="copyToClipboard(emuCreateAvdCmd(), 'emu_create')">
<span class="material-symbols-outlined">{{ copiedId() === 'emu_create' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'emu_create' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
<div class="cli-step-item">
<div class="step-badge-num">4</div>
<div class="step-content">
<div class="step-title">启动模拟器</div>
<p class="step-text">启动虚拟设备(可添加 <code>-no-window</code> 以无界面模式运行):</p>
<div class="code-box small-box">
<span class="terminal-prefix">{{ effectiveOs() === 'windows' ? 'PS>' : '$' }}</span>
<code class="code-text">emulator -avd Pixel_8_API_34 -no-boot-anim</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'emu_run'"
(click)="copyToClipboard('emulator -avd Pixel_8_API_34 -no-boot-anim', 'emu_run')">
<span class="material-symbols-outlined">{{ copiedId() === 'emu_run' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'emu_run' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
</div>
}
</div>
}
</div>
}
<!-- Method 2: Physical Phone via USB -->
@if (activeAdbGuideTab() === 'usb') {
<div class="method-details-panel">
<div class="usb-guide-intro">
<span class="material-symbols-outlined intro-icon">cable</span>
<span>通过 USB 数据线连接 Android 手机,并启用 USB 调试:</span>
</div>
<div class="steps-flow-grid">
<div class="flow-step-card">
<div class="step-card-header">
<span class="step-number-badge">1</span>
<span class="material-symbols-outlined step-card-icon">usb</span>
</div>
<div class="step-card-title">连接 USB 数据线</div>
<div class="step-card-desc">使用数据线将 Android 手机连接到此计算机。</div>
</div>
<div class="flow-step-card">
<div class="step-card-header">
<span class="step-number-badge">2</span>
<span class="material-symbols-outlined step-card-icon">touch_app</span>
</div>
<div class="step-card-title">启用开发者选项</div>
<div class="step-card-desc">在手机上打开 <strong>设置 > 关于手机</strong>,点击 <strong>版本号</strong> 7 次。</div>
</div>
<div class="flow-step-card">
<div class="step-card-header">
<span class="step-number-badge">3</span>
<span class="material-symbols-outlined step-card-icon">toggle_on</span>
</div>
<div class="step-card-title">启用 USB 调试</div>
<div class="step-card-desc">在 <strong>设置 > 开发者选项</strong>中,将 <strong>USB 调试</strong> 开启。
</div>
</div>
<div class="flow-step-card">
<div class="step-card-header">
<span class="step-number-badge">4</span>
<span class="material-symbols-outlined step-card-icon">verified_user</span>
</div>
<div class="step-card-title">授权计算机</div>
<div class="step-card-desc">勾选 <strong>“始终允许”</strong> ,然后点击 <strong>允许</strong> 完成手机上的授权提示。
</div>
</div>
</div>
</div>
}
<!-- Method 3: Wireless ADB -->
@if (activeAdbGuideTab() === 'wifi') {
<div class="method-details-panel">
<div class="wifi-guide-intro">
<span class="material-symbols-outlined intro-icon">wifi</span>
<span>将手机和计算机连接到同一 Wi-Fi 网络:</span>
</div>
<!-- Interactive Wi-Fi Connection Generator Box -->
<div class="wifi-interactive-box">
<div class="wifi-form-row">
<div class="wifi-input-col ip-col">
<label class="wifi-label">设备 IP 地址</label>
<div class="wifi-input-wrap">
<span class="material-symbols-outlined wrap-icon">router</span>
<input type="text" class="wifi-input" [ngModel]="wifiHost()" (ngModelChange)="wifiHost.set($event)"
placeholder="例如:192.168.1.108" spellcheck="false" />
</div>
</div>
<div class="wifi-input-col port-col">
<label class="wifi-label">端口</label>
<div class="wifi-input-wrap">
<span class="material-symbols-outlined wrap-icon">tag</span>
<input type="text" class="wifi-input" [ngModel]="wifiPort()" (ngModelChange)="wifiPort.set($event)"
placeholder="5555" spellcheck="false" />
</div>
</div>
<div class="wifi-btn-col">
<button type="button" class="btn-wifi-connect" [disabled]="isConnectingWifi() || isRemoteAdbServer() || !wifiHost().trim()"
(click)="connectWifiDevice()">
@if (isConnectingWifi()) {
<span class="material-symbols-outlined spinning">sync</span>
<span>正在连接…</span>
} @else {
<span class="material-symbols-outlined">bolt</span>
<span>连接</span>
}
</button>
</div>
</div>
@if (wifiConnectMessage()) {
<div class="wifi-status-alert success">
<span class="material-symbols-outlined">check_circle</span>
<span>{{ wifiConnectMessage() }}</span>
</div>
}
@if (wifiConnectError()) {
<div class="wifi-status-alert error">
<span class="material-symbols-outlined">error</span>
<span>{{ wifiConnectError() }}</span>
</div>
}
</div>
<div class="info-callout-box">
<span class="material-symbols-outlined callout-icon">help_outline</span>
<div class="callout-text">
在手机中查看 IP 地址与端口: <strong>设置 > 开发者选项 > 无线调试</strong>.
</div>
</div>
</div>
}
<!-- Method 4: ADB server on another computer -->
@if (activeAdbGuideTab() === 'remote') {
<div class="method-details-panel remote-adb-panel">
<div class="remote-guide-intro">
<span class="material-symbols-outlined intro-icon">hub</span>
<span>输入本机可访问的 ADB 服务器地址和端口,即可发现并控制连接在其他计算机上的手机。</span>
</div>
<div class="remote-adb-interactive-box">
<div class="remote-adb-form-row">
<div class="remote-adb-input-col host-col">
<label class="remote-adb-label" for="remote-adb-host">ADB 服务器地址</label>
<div class="remote-adb-input-wrap">
<span class="material-symbols-outlined wrap-icon">dns</span>
<input
id="remote-adb-host"
type="text"
class="remote-adb-input"
[ngModel]="remoteAdbHost()"
(ngModelChange)="updateRemoteAdbHost($event)"
placeholder="127.0.0.1"
autocomplete="off"
spellcheck="false"
/>
</div>
</div>
<div class="remote-adb-input-col port-col">
<label class="remote-adb-label" for="remote-adb-port">ADB 服务器端口</label>
<div class="remote-adb-input-wrap">
<span class="material-symbols-outlined wrap-icon">tag</span>
<input
id="remote-adb-port"
type="text"
inputmode="numeric"
class="remote-adb-input"
[ngModel]="remoteAdbPort()"
(ngModelChange)="updateRemoteAdbPort($event)"
placeholder="5038"
autocomplete="off"
spellcheck="false"
/>
</div>
</div>
<div class="remote-adb-btn-col">
<button
type="button"
class="btn-remote-adb-connect"
[disabled]="isConnectingRemoteAdb() || isActivatingRemoteAdb() || !remoteAdbHost().trim()"
(click)="connectRemoteAdbServer()"
>
@if (isConnectingRemoteAdb()) {
<span class="material-symbols-outlined spinning">sync</span>
<span>正在检查…</span>
} @else {
<span class="material-symbols-outlined">bolt</span>
<span>测试连接</span>
}
</button>
</div>
</div>
<label class="remember-remote-adb">
<input
type="checkbox"
[ngModel]="rememberRemoteAdb()"
(ngModelChange)="rememberRemoteAdb.set($event)"
/>
<span>保存此地址,供后续启动使用</span>
</label>
@if (remoteAdbMessage()) {
<div class="remote-adb-status-alert success">
<span class="material-symbols-outlined">check_circle</span>
<span>{{ remoteAdbMessage() }}</span>
</div>
}
@if (remoteAdbError()) {
<div class="remote-adb-status-alert error" role="alert">
<span class="material-symbols-outlined">error</span>
<span>{{ remoteAdbError() }}</span>
</div>
}
@if (remoteAdbDevices().length > 0) {
<div class="remote-device-results">
@for (device of remoteAdbDevices(); track device.serial) {
<div class="remote-device-row">
<span class="material-symbols-outlined">phone_android</span>
<div class="remote-device-copy">
<strong>{{ device.model || device.serial }}</strong>
<span>{{ device.serial }}</span>
</div>
<span class="remote-device-state" [class.ready]="device.state === 'device'">
{{ device.state === 'device' ? '就绪' : device.state }}
</span>
</div>
}
</div>
}
@if (remoteAdbProbeResult()?.success && !isProbedRemoteAdbActive()) {
<button
type="button"
class="btn-remote-adb-connect btn-use-tested-endpoint"
[disabled]="isActivatingRemoteAdb()"
(click)="activateRemoteAdbServer()"
>
@if (isActivatingRemoteAdb()) {
<span class="material-symbols-outlined spinning">sync</span>
<span>正在连接…</span>
} @else {
<span class="material-symbols-outlined">check</span>
<span>{{ remoteAdbHasReadyDevice() ? '使用此连接' : '仍然使用' }}</span>
}
</button>
}
</div>
<p class="remote-adb-explanation">
Artemis 将向此服务器发送 ADB 命令,并使用该服务器能够发现的手机。
</p>
</div>
}
</div>
}
<!-- STATE C: CONNECTED DEVICE IS LOCKED / LOCK STATE UNVERIFIED -->
@if (adbProbe()?.status === 'warn' && (adbProbe()?.summary === 'Device Locked' || adbProbe()?.summary === 'Lock State Unknown')) {
<div class="device-locked-panel">
<div class="device-locked-card">
<span class="material-symbols-outlined alert-icon">lock</span>
<div class="locked-details">
<div class="locked-title">
{{ adbProbe()?.summary === 'Device Locked' ? 'Device Is Locked' : 'Unable to Verify Lock State' }}
</div>
<div class="locked-desc">
{{ adbProbe()?.description }}
</div>
</div>
<button type="button" class="btn-check-unlock" [disabled]="isRefreshingDiagnostics()" (click)="refreshReadiness()">
<span class="material-symbols-outlined" [class.spinning]="isRefreshingDiagnostics()">refresh</span>
<span>{{ isRefreshingDiagnostics() ? '正在检查…' : 'Check Again' }}</span>
</button>
</div>
<!-- Multi-Device Switcher if multiple devices detected -->
@if (connectedDevices().length > 1) {
<div class="multi-device-switcher" style="margin-top: 12px;">
<span class="switcher-label">选择当前设备:</span>
<div class="switcher-chips">
@for (dev of connectedDevices(); track dev.serial) {
<button
type="button"
class="device-chip-btn"
[class.selected]="dev.serial === activeDevice()?.serial"
(click)="selectTargetDevice(dev.serial)"
>
<span class="material-symbols-outlined chip-icon">
phone_android
</span>
<span>{{ dev.model || dev.serial }}</span>
@if (dev.serial === activeDevice()?.serial) {
<span class="active-check">✓ 当前设备</span>
}
</button>
}
</div>
</div>
}
</div>
}
<!-- STATE C: UNAUTHORIZED DEVICE -->
@if (adbProbe()?.status === 'warn' && adbProbe()?.summary === 'Device Unauthorized') {
<div class="unauthorized-panel">
<div class="unauthorized-card">
<span class="material-symbols-outlined alert-icon">screen_lock_portrait</span>
<div class="unauth-details">
<div class="unauth-title">等待 USB 调试授权</div>
<div class="unauth-desc">
已检测到 Android 设备,但尚未授权。请解锁手机并点击 <strong>“允许 USB 调试”</strong>.
</div>
</div>
<button type="button" class="btn-retry-auth" (click)="restartAdbServer()">
<span class="material-symbols-outlined">refresh</span>
<span>{{ isRemoteAdbServer() ? '刷新设备' : '重启 ADB 并检查' }}</span>
</button>
</div>
</div>
}
<!-- STATE D: ADB MISSING -->
@if (adbProbe()?.status === 'fail') {
<div class="adb-missing-panel">
<div class="error-msg-box">
<span class="material-symbols-outlined err-icon">error</span>
<div>
<strong>未找到 Android 平台工具(adb)。</strong> 请在当前操作系统中安装 ADB:
</div>
</div>
<div class="install-commands-row">
<div class="install-card" [class.highlight-os]="effectiveOs() === 'linux'">
<div class="install-os">
<span>Linux (Ubuntu / Debian)</span>
@if (effectiveOs() === 'linux') {
<span class="detected-badge">已检测到</span>
}
</div>
<div class="code-box small-box">
<span class="terminal-prefix">$</span>
<code class="code-text">sudo apt-get install -y adb</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'linux_adb'"
(click)="copyToClipboard('sudo apt-get install -y adb', 'linux_adb')">
<span class="material-symbols-outlined">{{ copiedId() === 'linux_adb' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'linux_adb' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
<div class="install-card" [class.highlight-os]="effectiveOs() === 'darwin'">
<div class="install-os">
<span>macOS (Homebrew)</span>
@if (effectiveOs() === 'darwin') {
<span class="detected-badge">已检测到</span>
}
</div>
<div class="code-box small-box">
<span class="terminal-prefix">$</span>
<code class="code-text">brew install android-platform-tools</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'mac_adb'"
(click)="copyToClipboard('brew install android-platform-tools', 'mac_adb')">
<span class="material-symbols-outlined">{{ copiedId() === 'mac_adb' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'mac_adb' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
<div class="install-card" [class.highlight-os]="effectiveOs() === 'windows'">
<div class="install-os">
<span>Windows (WinGet)</span>
@if (effectiveOs() === 'windows') {
<span class="detected-badge">已检测到</span>
}
</div>
<div class="code-box small-box">
<span class="terminal-prefix">></span>
<code class="code-text">winget install Google.PlatformTools</code>
<button type="button" class="btn-copy-code" [class.copied]="copiedId() === 'win_adb'"
(click)="copyToClipboard('winget install Google.PlatformTools', 'win_adb')">
<span class="material-symbols-outlined">{{ copiedId() === 'win_adb' ? 'check' : 'content_copy' }}</span>
<span>{{ copiedId() === 'win_adb' ? '已复制' : '复制' }}</span>
</button>
</div>
</div>
</div>
</div>
}
</div>
</div>
<!-- Ready Banner Footer: Direct One-Click Launch -->
@if (isReady()) {
<div class="ready-banner-footer">
<div class="ready-banner-left">
<div class="step-badge step-pass">
<span class="material-symbols-outlined step-check-icon">check</span>
</div>
<div class="ready-text-group">
<h4 class="ready-title">系统已就绪,可以开始自动化任务</h4>
<p class="ready-desc">依赖、模型凭据和 Android 设备均已就绪。</p>
</div>
</div>
<button type="button" class="btn-proceed-launch" (click)="setTab('launcher')">
<span>启动自动化任务</span>
<span class="material-symbols-outlined rocket-icon">rocket_launch</span>
</button>
</div>
}
</section>
}
<!-- ========================================================================= -->
<!-- VIEW 2: TASK EXECUTION LAUNCHER (UNIFIED SCULPTURED COMPOSER) -->
<!-- ========================================================================= -->
@if (activeTab() === 'launcher') {
<section class="task-launcher-view">
<!-- Prerequisites Drop Alert if any blocker probe is not passing -->
@if (hasReadinessReport() && !isReady()) {
<div class="launcher-warning-bar" (click)="setTab('diagnostics')">
<span class="material-symbols-outlined warn-icon">{{ !pcOnly() && adbProbe()?.summary === 'Device Locked' ? 'lock' : 'warning' }}</span>
<span>
{{ pcOnly() ? '运行环境或模型凭据尚未就绪。点击此处打开系统设置。' : adbProbe()?.summary === 'Device Locked'
? 'Android 设备已锁定,请先解锁并进入主屏幕。'
: '运行环境、模型凭据或 Android 设备尚未就绪。点击此处打开系统设置。' }}
</span>
<span class="material-symbols-outlined arrow">arrow_forward</span>
</div>
}
<!-- Task composer -->
<div class="composer-card" [class.is-pro-active]="selectedProfile() === 'pro'">
<div class="composer-options">
<app-task-plugins [target]="pcOnly() ? 'computer' : 'android'" (changed)="taskPlugins.set($event)" />
<label class="pc-only-option" [class.is-selected]="pcOnly()" title="仅测试 PC 桌面端,无需手机或模拟器在线">
<input type="checkbox" [ngModel]="pcOnly()" (ngModelChange)="pcOnly.set($event)" [disabled]="isSubmitting()" [attr.aria-describedby]="pcOnly() ? 'pc-only-description' : null" />
<span class="material-symbols-outlined" aria-hidden="true">desktop_windows</span>
<span>仅 PC</span>
</label>
</div>
@if (pcOnly()) {
<p class="pc-only-hint" id="pc-only-description">通过已启用并授权的电脑插件执行,无需手机或模拟器在线。</p>
}
<!-- Smart Intent Recommendation Strip (if detected) -->
@if (showIntentSuggestion()) {
<div class="intent-suggestion-strip">
<div class="suggestion-left">
<span class="material-symbols-outlined sparkle-icon">auto_awesome</span>
<span class="suggestion-text">
检测到持续监控或复杂任务,建议使用: <strong>Pro 专业模式</strong>
</span>
</div>
<button type="button" class="btn-switch-pro" (click)="setProfile('pro')">
<span>切换至 Pro</span>
<span class="material-symbols-outlined btn-arrow">arrow_forward</span>
</button>
</div>
}
<!-- Borderless task input -->
<div class="composer-input-area">
<textarea
id="task-input-field"
class="composer-textarea"
rows="3"
[(ngModel)]="taskGoal"
[disabled]="isSubmitting()"
[placeholder]="pcOnly() ? '描述你希望在电脑上完成的任务,例如:打开浏览器并检查网页…' : '描述你希望在手机上完成的任务,例如:打开 Gmail 并查找最新邮件…'"
(keydown.control.enter)="runTask()"
(keydown.meta.enter)="runTask()"
></textarea>
</div>
<!-- Contextual Model Descriptor Strip (Clean & Unified for Flash & Pro) -->
<div class="composer-model-descriptor" [class.is-pro]="selectedProfile() === 'pro'">
<div class="descriptor-icon-wrap">
<span class="material-symbols-outlined">{{ selectedProfile() === 'flash' ? 'bolt' : 'psychology' }}</span>
</div>
<div class="descriptor-body">
@if (selectedProfile() === 'flash') {
<p class="desc-line">
<span class="mode-name">Flash</span>
<span class="bullet">·</span>
<span>节省 Token,快速响应 <strong>每步 3–5 秒</strong> 直接根据实时屏幕执行操作,适合路径明确的日常任务。</span>
<span class="limitation-tag">(最终 AI 判定默认关闭,可在功能配置中开启)</span>
</p>
} @else if(selectedProfile() === 'exploration') {
<p class="desc-line"><span class="mode-name">探索</span><span class="bullet">·</span><span>边执行边交流,支持人工示范,成功后生成可复用用例。</span></p>
} @else {
<p class="desc-line">
<span class="mode-name">Pro</span>
<span class="bullet">·</span>
<span>先规划、再检查结果,遇到操作受阻时自动恢复。支持 <strong>超过 100 步的</strong> 工作流和实时{{ pcOnly() ? '电脑' : '手机' }}监控(<strong>15–40 秒</strong> / 步)。</span>
</p>
}
</div>
</div>
<!-- Error Banner if any -->
@if (errorMessage()) {
<div class="error-banner">
<span class="material-symbols-outlined error-icon">error</span>
<span>{{ errorMessage() }}</span>
</div>
}
<!-- Composer Bottom Action Bar -->
<div class="composer-action-bar">
<div class="bar-left">
<!-- Frosted Glass Architecture Toggle Capsule -->
<div class="model-toggle-capsule">
<button
type="button"
class="model-toggle-btn btn-flash"
[class.active]="selectedProfile() === 'flash'"
(click)="setProfile('flash')"
title="Flash:快速执行日常应用任务(每步 3–5 秒)"
>
<span class="material-symbols-outlined btn-icon">bolt</span>
<span class="btn-text">Flash</span>
</button>
<button
type="button"
class="model-toggle-btn btn-pro"
[class.active]="selectedProfile() === 'pro'"
(click)="setProfile('pro')"
title="Pro:先规划、检查结果并恢复受阻步骤,适合长流程或复杂任务(每步 15–40 秒)"
>
<span class="material-symbols-outlined btn-icon">psychology</span>
<span class="btn-text">Pro</span>
</button>
<button type="button" class="model-toggle-btn btn-pro" [class.active]="selectedProfile() === 'exploration'" (click)="setProfile('exploration')" title="探索:边执行边交流,可人工示范,完成后生成可复用用例">
<span class="material-symbols-outlined btn-icon">explore</span><span class="btn-text">探索</span>
</button>
</div>
</div>
<div class="bar-right">
@if (taskGoal().length > 0) {
<span class="char-count">{{ taskGoal().length }} chars</span>
}
<!-- Run Button -->
<button
type="button"
class="btn-run-composer"
[class.btn-run-pro]="selectedProfile() === 'pro'"
[disabled]="!taskGoal().trim() || isSubmitting() || !isReady()"
[title]="!isReady() ? (!pcOnly() && adbProbe()?.summary === 'Device Locked' ? '请先解锁 Android 设备' : '请先完成系统设置') : '执行任务'"
(click)="runTask()"
>
@if (isSubmitting()) {
<span class="spinner"></span><span>正在启动…</span>
} @else {
<span class="material-symbols-outlined run-icon">play_arrow</span><span>{{selectedProfile() === 'exploration' ? '开始探索' : '执行任务'}}</span>
}
</button>
</div>
</div>
</div>
@if(selectedProfile() === 'exploration') {
<section class="exploration-settings">
<div class="exploration-intro"><span class="material-symbols-outlined">explore</span><div><strong>一起探索复杂用例</strong><p>描述大概流程,边执行边交流。可以纠正方向或亲自示范,结束后生成可复用的用例。</p></div></div>
@if(explorationConfig(); as config) {
<button type="button" class="exploration-settings-toggle" (click)="explorationSettingsOpen.set(!explorationSettingsOpen())" [attr.aria-expanded]="explorationSettingsOpen()"><span class="material-symbols-outlined">tune</span><span>{{config.model}} · {{config.reasoning_effort}}</span><span class="settings-toggle-label">调整探索设置</span><span class="material-symbols-outlined">{{explorationSettingsOpen() ? 'expand_less' : 'expand_more'}}</span></button>
@if(explorationSettingsOpen()) {
<div class="exploration-settings-body">
<label class="model-label">探索模型<input list="explore-models" [(ngModel)]="config.model" [disabled]="explorationSettingsBusy()" /></label>
<datalist id="explore-models"><option value="gpt-6.1-terra"></option><option value="gpt-6.1-sol"></option><option value="gpt-6-astra"></option><option value="gpt-6-luna"></option><option value="mimo-v2.6-pro"></option></datalist>
<div class="exploration-fields">
<app-setting-slider label="思考强度" [(value)]="config.reasoning_effort" [values]="reasoningValues" [labels]="reasoningLabels" hint="越高越适合复杂推理,也可能等待更久。" [disabled]="explorationSettingsBusy()" />
<app-setting-slider label="每段执行时限" [(value)]="config.timeout_seconds" [values]="executionTimes" [labels]="executionTimeLabels" hint="到时暂停并保存进度,点击继续即可接着探索。" [disabled]="explorationSettingsBusy()" />
<app-setting-slider label="人工示范时限" [(value)]="config.demonstration_timeout_seconds" [values]="demoTimes" [labels]="demoTimeLabels" hint="点击“我来操作”后,允许录制的示范时长。" [disabled]="explorationSettingsBusy()" />
<app-setting-slider label="示范提前提醒" [(value)]="config.demonstration_warning_seconds" [values]="warningTimes" [labels]="warningLabels" hint="示范即将到时时提醒,可手动延长。" [disabled]="explorationSettingsBusy()" />
</div>
<p class="settings-note">等待回答和人工示范不占执行时间。这组设置只用于探索。</p>
@if(!explorationConfigValid()){<p role="alert" class="settings-error">请填写模型,并将提前提醒设为小于示范时限。</p>}
<div class="settings-footer"><button type="button" class="save-settings" (click)="saveExplorationSettings()" [disabled]="explorationSettingsBusy() || !explorationConfigValid()">{{explorationSettingsBusy() ? '正在保存…' : '记住此配置'}}</button><span role="status">{{explorationNotice()}}</span></div>
</div>
}
} @else {<button (click)="loadExplorationSettings()" [disabled]="explorationSettingsBusy()">{{explorationSettingsBusy() ? '正在读取配置…' : '重新读取探索配置'}}</button>}
</section>
}
<!-- Pro Mode Standalone Expected Output Card (Clean & Minimalist) -->
@if (selectedProfile() === 'pro') {
<div class="pro-outputter-standalone-card">
<div class="outputter-card-header">
<span class="outputter-title">预期输出</span>
<span class="outputter-optional-tag">(可选)</span>
</div>
<div class="outputter-input-wrapper">
<input
type="text"
class="outputter-standalone-input"
[ngModel]="expectedOutput()"
(ngModelChange)="expectedOutput.set($event)"
placeholder="Specify expected output or schema (e.g. 'Extract SMS verification code as JSON' or 'Summary of discounts and total price')..."
spellcheck="false"
/>
@if (expectedOutput().trim()) {
<button
type="button"
class="btn-clear-standalone"
(click)="expectedOutput.set('')"
title="清空预期输出"
>
<span class="material-symbols-outlined">close</span>
</button>
}
</div>
</div>
<!-- Pro Mode Tuning Card: verification depth & explorer perception sliders -->
<div class="pro-tuning-card" [class.tip-open]="activeTuningTip() !== null">
<div class="tuning-card-header">
<span class="tuning-title">Pro 设置</span>
@if (!isTuningDefault()) {
<button type="button" class="btn-tuning-reset" (click)="resetTuning()" title="恢复默认设置">重置</button>
}
</div>
<div class="tuning-sliders">
@for (slider of tuningSliders(); track slider.kind) {
<div
class="tuning-slider"
[class.verify]="slider.kind === 'verify'"
[class.explore]="slider.kind === 'explore'"
[class.dragging]="activeTuningTip() === slider.kind"
[style.--pct]="slider.fraction"
>
<div class="slider-head">
<span class="slider-name">{{ slider.name }}</span>
<span class="slider-value">{{ slider.level.label }}</span>
</div>
<div class="slider-ends" aria-hidden="true">
<span>{{ slider.ends[0] }}</span>
<span>{{ slider.ends[1] }}</span>
</div>
<div
class="slider-track-wrap"
(pointerenter)="showTuningTip(slider.kind)"
(pointerleave)="hideTuningTip(slider.kind)"
>
<div class="slider-rail" aria-hidden="true">
<div class="slider-fill"></div>
@for (lvl of slider.ladder; track lvl.id; let i = $index) {
<span
class="slider-notch"
[class.reached]="i <= slider.index"
[style.--n]="notchFraction(i, slider.ladder.length)"
></span>
}
@if (slider.index === slider.ladder.length - 1) {
<!-- Re-created each time the thumb reaches the end, so the spread replays. -->
<div class="slider-solid"></div>
<svg class="slider-dither" width="100%" height="6" aria-hidden="true">
@for (px of ditherPixels; track $index) {
<rect
[attr.x]="px.x + '%'"
[attr.y]="px.y"
width="2"
height="2"
[style.opacity]="px.opacity"
[style.animation-delay.ms]="px.delay"
/>
}
</svg>
}
</div>
<div class="slider-thumb" aria-hidden="true"></div>
@if (slider.index === slider.ladder.length - 1) {
<span class="thumb-ripple" aria-hidden="true"></span>
}
<input
type="range"
class="slider-input"
min="0"
[max]="slider.ladder.length - 1"
step="1"
[value]="slider.index"
(input)="setTuningIndex(slider.kind, $any($event.target).value)"
(focus)="showTuningTip(slider.kind)"
(blur)="hideTuningTip(slider.kind)"
[attr.aria-label]="slider.name"
[attr.aria-valuetext]="slider.level.label + ': ' + slider.level.tagline"
/>
@if (activeTuningTip() === slider.kind) {
<div class="tuning-tip-arrow" aria-hidden="true"></div>
<div class="tuning-tip" role="tooltip">
<div class="tip-heading">
<span class="tip-title">{{ slider.level.label }}</span>
<span class="tip-latency">{{ slider.level.latency }}</span>
</div>
<p class="tip-tagline">{{ slider.level.tagline }}</p>
<div class="tip-section">
<span class="tip-section-title">运行方式</span>
<ul>
@for (line of slider.level.runs; track $index) {
<li>{{ line }}</li>
}
</ul>
</div>
@if (slider.level.skips?.length) {
<div class="tip-section skips">
<span class="tip-section-title">不包含的能力</span>
<ul>
@for (line of slider.level.skips; track $index) {
<li>{{ line }}</li>
}
</ul>
</div>
}
<div class="tip-best">
<span class="tip-section-title">适用场景</span>
<span>{{ slider.level.bestFor }}</span>
</div>
</div>
}
</div>
</div>
}
</div>
</div>
}
<app-featured-tasks />
</section>
}
</main>
</div>