1689 lines
88 KiB
HTML
1689 lines
88 KiB
HTML
<!--
|
||
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>
|