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

893 lines
33 KiB
TypeScript
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.
import { CapabilitySettingsComponent } from '../../components/model-settings/capability-settings.component';
import { SettingSliderComponent, REASONING_VALUES, REASONING_LABELS } from '../../components/setting-slider/setting-slider.component';
import { ModelSettingsComponent } from '../../components/model-settings/model-settings.component';
import { FeaturedTasksComponent } from '../../components/featured-tasks/featured-tasks.component';
/**
* 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.
*/
import { Component, signal, computed, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TaskPluginsComponent, TaskPluginOptions } from '../../components/task-plugins/task-plugins.component';
import { Router } from '@angular/router';
import { ExplorationService, ExplorationConfig } from '../../services/exploration.service';
import { AgentService } from '../../services/agent.service';
import { SystemService, formatAdbRestartFeedback } from '../../services/system.service';
import {
AdbServerConnectionResult,
AdbServerDevice,
DeviceInfo,
ProbeResult
} from '../../core/models/system.model';
import {
AppReference,
SmartSuggestion,
SuggestionCategory
} from '../../core/data/smart-tasks.data';
import { TaskRecommendationService } from '../../core/services/task-recommendation.service';
import {
DEFAULT_EXPLORER_MODE,
DEFAULT_VERIFICATION_LEVEL,
EXPLORER_MODES,
ExplorerModeId,
TuningLevel,
VERIFICATION_LEVELS,
VerificationLevelId,
levelIndex,
notchPercent
} from '../../core/models/pro-tuning.model';
export type TuningKind = 'verify' | 'explore';
/** One 2x2 px square of the "maxed out" dither texture drawn over a slider rail. */
export interface DitherPixel {
/** Horizontal position as a percentage of the rail width. */
x: number;
/** Row offset in px (rail is 6 px tall, three 2 px rows). */
y: number;
/** Resting opacity; squares near the thumb are stronger. */
opacity: number;
/** Animation delay in ms so the texture spreads leftwards from the thumb. */
delay: number;
}
/**
* Seeded pixels keep the slider texture stable across renders.
* Delays increase with distance from the thumb to animate from right to left.
*/
function buildDitherPixels(count = 260, seed = 7): DitherPixel[] {
let state = seed >>> 0;
const rand = (): number => {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
return state / 0x100000000;
};
const pixels: DitherPixel[] = [];
for (let i = 0; i < count; i++) {
const x = rand() * 100;
const y = Math.floor(rand() * 3) * 2;
pixels.push({
x: Math.round(x * 10) / 10,
y,
opacity: Math.round((0.35 + 0.4 * rand()) * 100) / 100,
// 8 ms per percent: the front takes ~0.8 s to reach the left end.
delay: Math.round((100 - x) * 8)
});
}
return pixels;
}
/** Shared view model for the verification and screen-reading sliders. */
export interface TuningSliderVm {
kind: TuningKind;
name: string;
/** One-word meaning of each end of the track, e.g. ["Off", "Strict"]. */
ends: readonly [string, string];
ladder: readonly TuningLevel[];
index: number;
level: TuningLevel;
/** 0..1 position of the thumb along the track. */
fraction: number;
}
export type { AppReference, SmartSuggestion, SuggestionCategory };
type AdbGuideTab = 'emulator' | 'usb' | 'wifi' | 'remote';
@Component({
selector: 'app-home',
standalone: true,
imports: [FormsModule, ModelSettingsComponent, CapabilitySettingsComponent, TaskPluginsComponent, SettingSliderComponent, FeaturedTasksComponent],
templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './home.component.scss'
})
export class HomeComponent implements OnInit, OnDestroy {
public agentService = inject(AgentService);
public systemService = inject(SystemService);
public taskRecService = inject(TaskRecommendationService);
private router = inject(Router);
// High-level navigation mode: 'diagnostics' (System Setup Guide) vs 'launcher' (Task Execution)
public activeTab = signal<'diagnostics' | 'launcher'>('launcher');
// Interactive guide sub-tab inside the ADB section
public activeAdbGuideTab = signal<AdbGuideTab>('emulator');
public emulatorSetupMode = signal<'studio' | 'cli'>('studio');
// Clipboard copy state tracker for interactive feedback
public copiedId = signal<string | null>(null);
// Diagnostic re-check state
public isRefreshingDiagnostics = signal<boolean>(false);
// Wireless ADB Interactive connection signals
public wifiHost = signal<string>('192.168.1.100');
public wifiPort = signal<string>('5555');
public isConnectingWifi = signal<boolean>(false);
public wifiConnectMessage = signal<string | null>(null);
public wifiConnectError = signal<string | null>(null);
public adbRestartFeedback = signal<string | null>(null);
public showConnectionMethods = signal<boolean>(false);
// ADB server endpoint connection state
public remoteAdbHost = signal<string>('127.0.0.1');
public remoteAdbPort = signal<string>('5038');
public rememberRemoteAdb = signal<boolean>(true);
public isConnectingRemoteAdb = signal<boolean>(false);
public isActivatingRemoteAdb = signal<boolean>(false);
public isSwitchingToLocalAdb = signal<boolean>(false);
public remoteAdbMessage = signal<string | null>(null);
public remoteAdbError = signal<string | null>(null);
public remoteAdbDevices = signal<AdbServerDevice[]>([]);
public remoteAdbProbeResult = signal<AdbServerConnectionResult | null>(null);
public adbServerStatus = computed(() => this.systemService.adbServerStatus());
public isRemoteAdbServer = computed(() => this.systemService.isRemoteAdbServer());
public remoteAdbHasReadyDevice = computed(() =>
this.remoteAdbDevices().some(device => device.state === 'device')
);
public isProbedRemoteAdbActive = computed(() => {
const tested = this.remoteAdbProbeResult()?.endpoint;
const active = this.adbServerStatus()?.endpoint;
return !!tested && !!active && tested.identity === active.identity;
});
public wifiCommand = computed(() => {
const h = this.wifiHost().trim() || '<phone-ip>';
const p = this.wifiPort().trim() || '5555';
return `adb connect ${h}:${p}`;
});
// Task execution parameters
public selectedProfile = signal<'flash' | 'pro' | 'exploration'>('flash');
public taskGoal = signal<string>('');
public isSubmitting = signal<boolean>(false);
public errorMessage = signal<string | null>(null);
// Pro Mode Outputter & Structured Output Configuration
public expectedOutput = signal<string>('');
public showOutputterDrawer = signal<boolean>(false);
// Slider indexes map to the API ids in VERIFICATION_LEVELS / EXPLORER_MODES.
public readonly verificationLevels = VERIFICATION_LEVELS;
public readonly explorerModes = EXPLORER_MODES;
public verificationIndex = signal<number>(
levelIndex(VERIFICATION_LEVELS, DEFAULT_VERIFICATION_LEVEL, DEFAULT_VERIFICATION_LEVEL)
);
public explorerIndex = signal<number>(
levelIndex(EXPLORER_MODES, DEFAULT_EXPLORER_MODE, DEFAULT_EXPLORER_MODE)
);
/** Effective defaults from the backend config (`GET /api/run/defaults`). */
private tuningDefaults = signal<{ verification: VerificationLevelId; explorer: ExplorerModeId }>({
verification: DEFAULT_VERIFICATION_LEVEL,
explorer: DEFAULT_EXPLORER_MODE
});
/** True once the user moved a slider; backend defaults then stop overriding it. */
private tuningTouched = signal<boolean>(false);
/** Which slider's hover card is open (while hovering, dragging, or focused). */
public activeTuningTip = signal<TuningKind | null>(null);
/** Pixel cloud drawn over a rail once its slider reaches the last notch. */
public readonly ditherPixels: readonly DitherPixel[] = buildDitherPixels();
public verificationLevel = computed<TuningLevel<VerificationLevelId>>(
() => VERIFICATION_LEVELS[this.verificationIndex()]
);
public explorerMode = computed<TuningLevel<ExplorerModeId>>(
() => EXPLORER_MODES[this.explorerIndex()]
);
public isTuningDefault = computed<boolean>(() => {
const d = this.tuningDefaults();
return this.verificationLevel().id === d.verification && this.explorerMode().id === d.explorer;
});
public tuningSliders = computed<TuningSliderVm[]>(() => {
const vi = this.verificationIndex();
const ei = this.explorerIndex();
return [
{
kind: 'verify',
name: "结果检查",
ends: ["关闭", "严格检查"],
ladder: VERIFICATION_LEVELS,
index: vi,
level: VERIFICATION_LEVELS[vi],
fraction: notchPercent(vi, VERIFICATION_LEVELS.length) / 100
},
{
kind: 'explore',
name: "屏幕识别",
ends: ["更快", "更精细"],
ladder: EXPLORER_MODES,
index: ei,
level: EXPLORER_MODES[ei],
fraction: notchPercent(ei, EXPLORER_MODES.length) / 100
}
];
});
public notchFraction(index: number, count: number): number {
return notchPercent(index, count) / 100;
}
public setTuningIndex(kind: TuningKind, raw: number | string): void {
const idx = Math.round(Number(raw));
if (!Number.isFinite(idx)) return;
const ladder = kind === 'verify' ? VERIFICATION_LEVELS : EXPLORER_MODES;
const clamped = Math.min(Math.max(idx, 0), ladder.length - 1);
this.tuningTouched.set(true);
if (kind === 'verify') {
this.verificationIndex.set(clamped);
} else {
this.explorerIndex.set(clamped);
}
// Keyboard nudges and drags should keep the explanation visible.
this.activeTuningTip.set(kind);
}
public showTuningTip(kind: TuningKind): void {
this.activeTuningTip.set(kind);
}
public hideTuningTip(kind: TuningKind): void {
if (this.activeTuningTip() === kind) {
this.activeTuningTip.set(null);
}
}
public resetTuning(): void {
const d = this.tuningDefaults();
this.verificationIndex.set(levelIndex(VERIFICATION_LEVELS, d.verification, DEFAULT_VERIFICATION_LEVEL));
this.explorerIndex.set(levelIndex(EXPLORER_MODES, d.explorer, DEFAULT_EXPLORER_MODE));
this.tuningTouched.set(false);
}
/** Pull the effective config defaults so the sliders start where artemis.jsonc is. */
private loadProTuningDefaults(): void {
this.agentService.getProTuningDefaults().subscribe({
next: (res) => {
const vIdx = levelIndex(VERIFICATION_LEVELS, res?.verification_level, DEFAULT_VERIFICATION_LEVEL);
const eIdx = levelIndex(EXPLORER_MODES, res?.explorer_mode, DEFAULT_EXPLORER_MODE);
this.tuningDefaults.set({
verification: VERIFICATION_LEVELS[vIdx].id,
explorer: EXPLORER_MODES[eIdx].id
});
if (!this.tuningTouched()) {
this.verificationIndex.set(vIdx);
this.explorerIndex.set(eIdx);
}
},
// Defaults are a convenience; the built-in ladder defaults already apply.
error: () => undefined
});
}
public toggleOutputterDrawer(): void {
this.showOutputterDrawer.update((v) => !v);
}
public applyOutputPreset(preset: string): void {
if (this.expectedOutput() === preset) {
this.expectedOutput.set('');
} else {
this.expectedOutput.set(preset);
this.showOutputterDrawer.set(true);
}
}
// Smart intent detection for model recommendation
public isIntentSuggestingPro = computed<boolean>(() => {
const text = this.taskGoal().toLowerCase();
if (!text.trim()) return false;
const keywords = [
'monitor', 'polling', 'poll', 'wait until', 'loop', 'keep watching',
'crash', 'logcat', 'troubleshoot', 'diagnose', 'debug', 'investigate',
'compare', 'extract', 'summarize', 'report',
'监控', '轮询', '等待', '一直', '直到', '崩溃', '闪退', '排查', '分析日志', '对比', '总结'
];
return keywords.some(k => text.includes(k));
});
public showIntentSuggestion = computed<boolean>(() => {
return this.isIntentSuggestingPro() && this.selectedProfile() === 'flash';
});
// Computed helper states delegating to SystemService
taskPlugins = signal<TaskPluginOptions>({});
public isReady = computed(() => this.systemService.isReady());
public hasReadinessReport = computed(() => this.systemService.hasReadinessReport());
public isLoading = computed(() => this.systemService.isLoading());
public isRestartingAdb = computed(() => this.systemService.isRestartingAdb());
public launchingAvd = computed(() => this.systemService.launchingAvd());
public emulatorLaunchState = computed(() => this.systemService.emulatorLaunchState());
public isEmulatorLaunching = computed(() => this.systemService.isEmulatorLaunching());
public showLaunchLogs = signal<boolean>(false);
// Probes
public pythonProbe = computed(() => this.systemService.pythonProbe());
public configProbe = computed(() => this.systemService.configProbe());
public adbProbe = computed(() => this.systemService.adbProbe());
public llmProbe = computed(() => this.systemService.llmProbe());
public openaiProbe = computed(() => this.systemService.openaiProbe());
public ocrProbe = computed(() => this.systemService.ocrProbe());
public toolchainProbe = computed(() => this.systemService.toolchainProbe());
// Step-level readiness
public isEnvironmentReady = computed(() => this.systemService.isEnvironmentReady());
public isCredentialsReady = computed(() => this.systemService.isCredentialsReady());
public isSkipCredentialsCheck = computed(() => this.systemService.isSkipCredentialsCheck());
public isDeviceReady = computed(() => this.systemService.isDeviceReady());
// Device information
public activeDevice = computed(() => this.systemService.activeDevice());
public connectedDevices = computed(() => this.systemService.connectedDevices());
public installedAvds = computed(() => this.systemService.installedAvds());
public emulatorPath = computed(() => this.systemService.emulatorPath());
public isEmulatorInPath = computed(() => this.systemService.isEmulatorInPath());
public totalStepCount = computed(() => this.systemService.totalStepCount());
public passedStepCount = computed(() => this.systemService.passedStepCount());
public blockerCount = computed(() => this.systemService.blockerCount());
public passedBlockerCount = computed(() => this.systemService.passedBlockerCount());
// Configured LLM providers from probe metadata
public configuredLlmProviders = computed<any[]>(() => {
const meta = this.llmProbe()?.metadata;
if (meta && Array.isArray(meta['providers'])) {
return meta['providers'];
}
return [];
});
// Multi-OS detection & active OS selection
public selectedOs = signal<'linux' | 'darwin' | 'windows' | null>(null);
public effectiveOs = computed<'linux' | 'darwin' | 'windows'>(() => {
return this.selectedOs() || this.systemService.osType();
});
public oneClickSetupCmd = computed(() => {
const os = this.effectiveOs();
if (os === 'windows') {
return 'powershell -ExecutionPolicy Bypass -File scripts/install_deps.ps1';
}
return 'bash scripts/install_deps.sh';
});
public adbInstallCmd = computed(() => {
const os = this.effectiveOs();
if (os === 'windows') {
return 'winget install Google.PlatformTools';
}
if (os === 'darwin') {
return 'brew install android-platform-tools';
}
return 'sudo apt-get install -y adb';
});
public toolchainInstallCmd = computed(() => {
const os = this.effectiveOs();
if (os === 'windows') {
return 'winget install Gyan.FFmpeg Genymobile.scrcpy';
}
if (os === 'darwin') {
return 'brew install ffmpeg scrcpy';
}
return 'sudo apt-get install -y ffmpeg scrcpy';
});
public emuHypervisorTitle = computed(() => {
const os = this.effectiveOs();
if (os === 'windows') return "启用 Windows 虚拟机监控程序平台(WHPX)";
if (os === 'darwin') return "检查 macOS 虚拟化并安装工具";
return "启用 KVM 硬件加速(Linux)";
});
public emuHypervisorDesc = computed(() => {
const os = this.effectiveOs();
if (os === 'windows') return "以管理员身份打开 PowerShell,启用 Windows 虚拟机监控程序平台:";
if (os === 'darwin') return "macOS 使用系统 Hypervisor.framework。通过 Homebrew 或 Android Studio 安装 SDK 工具:";
return "确保当前用户拥有虚拟化权限:";
});
public emuHypervisorCmd = computed(() => {
const os = this.effectiveOs();
if (os === 'windows') return 'Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform';
if (os === 'darwin') return 'brew install --cask android-commandlinetools';
return 'sudo apt-get install -y qemu-kvm libvirt-daemon-system && sudo adduser $USER kvm';
});
public emuSdkInstallCmd = computed(() => {
const os = this.effectiveOs();
if (os === 'darwin') {
return 'sdkmanager --install "system-images;android-34;google_apis;arm64-v8a" "emulator" "platform-tools"';
}
return 'sdkmanager --install "system-images;android-34;google_apis;x86_64" "emulator" "platform-tools"';
});
public emuCreateAvdCmd = computed(() => {
const os = this.effectiveOs();
if (os === 'darwin') {
return 'avdmanager create avd -n Pixel_8_API_34 -k "system-images;android-34;google_apis;arm64-v8a" --device "pixel_8"';
}
return 'avdmanager create avd -n Pixel_8_API_34 -k "system-images;android-34;google_apis;x86_64" --device "pixel_8"';
});
// Flag indicating whether Google Cloud Vision OCR is configured
public isOcrConfigured = computed<boolean>(() => {
const meta = this.ocrProbe()?.metadata;
return meta?.['configured'] === true;
});
// Rich Smart Suggestions Library (Device-Aware, Flash vs Pro Tailored)
public readonly allSuggestions = this.taskRecService.allTasks;
// Suggestion category filter & shuffle state
public selectedCategory = signal<SuggestionCategory>('all');
public shuffleOffset = signal<number>(0);
// Set of installed package strings from active device
public installedPackages = computed<Set<string>>(() => {
const pkgs = this.activeDevice()?.installed_packages;
if (pkgs && Array.isArray(pkgs)) {
return new Set(pkgs);
}
return new Set();
});
public filteredSuggestions = computed<SmartSuggestion[]>(() => {
return this.taskRecService.filterAndRankTasks(
this.installedPackages(),
this.selectedCategory(),
this.shuffleOffset()
);
});
private focusListener = () => {
// Silently re-check environment when user returns to the browser tab
this.systemService.fetchReadiness().subscribe();
};
ngOnInit(): void {
const savedProfile = localStorage.getItem('artemis.taskProfile');
if (savedProfile === 'exploration' || savedProfile === 'flash' || savedProfile === 'pro') this.setProfile(savedProfile);
this.loadProTuningDefaults();
// Initial fetch of system readiness & model configuration
this.systemService.fetchReadiness().subscribe();
this.systemService.fetchModelConfigEnv().subscribe();
this.systemService.fetchAdbServerStatus().subscribe({
next: status => {
if (status.endpoint.mode === 'remote') {
this.remoteAdbHost.set(status.endpoint.host);
this.remoteAdbPort.set(String(status.endpoint.port));
this.activeAdbGuideTab.set('remote');
}
},
error: () => {}
});
window.addEventListener('focus', this.focusListener);
}
ngOnDestroy(): void {
window.removeEventListener('focus', this.focusListener);
}
public setTab(tab: 'diagnostics' | 'launcher'): void {
this.activeTab.set(tab);
if (tab === 'diagnostics') {
this.systemService.fetchReadiness().subscribe();
this.systemService.fetchModelConfigEnv().subscribe();
}
}
public setSelectedOs(os: 'linux' | 'darwin' | 'windows'): void {
this.selectedOs.set(os);
}
public setAdbGuideTab(tab: AdbGuideTab): void {
this.activeAdbGuideTab.set(tab);
this.remoteAdbError.set(null);
this.remoteAdbMessage.set(null);
}
public setEmulatorSetupMode(mode: 'studio' | 'cli'): void {
this.emulatorSetupMode.set(mode);
}
public skipCredentialsCheck(): void {
this.systemService.skipCredentialsCheck();
}
public refreshReadiness(): void {
this.isRefreshingDiagnostics.set(true);
this.systemService.fetchReadiness(false, true).subscribe({
next: () => {
this.systemService.fetchModelConfigEnv().subscribe({
next: () => {
setTimeout(() => this.isRefreshingDiagnostics.set(false), 450);
},
error: () => this.isRefreshingDiagnostics.set(false)
});
},
error: () => this.isRefreshingDiagnostics.set(false)
});
}
public restartAdbServer(): void {
this.adbRestartFeedback.set(null);
this.systemService.restartAdb().subscribe({
next: (res) => {
this.adbRestartFeedback.set(formatAdbRestartFeedback(res?.restart_result));
setTimeout(() => this.adbRestartFeedback.set(null), 6000);
},
error: () => {
this.adbRestartFeedback.set("重启失败");
setTimeout(() => this.adbRestartFeedback.set(null), 3000);
}
});
}
public connectWifiDevice(): void {
if (this.isRemoteAdbServer()) {
this.wifiConnectError.set("连接无线设备前,请先切换至本地 ADB。");
return;
}
const host = this.wifiHost().trim();
const portStr = this.wifiPort().trim() || '5555';
const port = parseInt(portStr, 10) || 5555;
if (!host) {
this.wifiConnectError.set("请输入有效的 IP 地址。");
return;
}
this.isConnectingWifi.set(true);
this.wifiConnectError.set(null);
this.wifiConnectMessage.set(null);
this.systemService.connectWirelessAdb(host, port).subscribe({
next: (res) => {
this.isConnectingWifi.set(false);
const cr = res?.connect_result;
if (cr?.success) {
this.wifiConnectMessage.set(`已连接至 ${host}:${port}!`);
setTimeout(() => this.wifiConnectMessage.set(null), 4000);
} else {
this.wifiConnectError.set(cr?.message || "连接失败,请检查手机 IP 地址和 Wi-Fi。");
}
},
error: (err) => {
this.isConnectingWifi.set(false);
this.wifiConnectError.set(err?.error?.detail || "连接失败,请检查 ADB 连接。");
}
});
}
public connectRemoteAdbServer(): void {
const host = this.remoteAdbHost().trim();
const port = Number(this.remoteAdbPort().trim());
if (!host) {
this.remoteAdbError.set("请输入 ADB 服务器的主机名或 IP 地址。");
return;
}
if (!Number.isInteger(port) || port < 1 || port > 65535) {
this.remoteAdbError.set("请输入 1–65535 之间的端口。");
return;
}
this.isConnectingRemoteAdb.set(true);
this.remoteAdbError.set(null);
this.remoteAdbMessage.set(null);
this.remoteAdbDevices.set([]);
this.remoteAdbProbeResult.set(null);
this.systemService.probeAdbServer(host, port).subscribe({
next: response => {
this.isConnectingRemoteAdb.set(false);
const result = response.connection_result;
if (result.success) {
this.remoteAdbProbeResult.set(result);
this.remoteAdbDevices.set(result.devices || []);
this.remoteAdbMessage.set(result.message);
} else {
this.remoteAdbError.set(result.message);
}
},
error: error => {
this.isConnectingRemoteAdb.set(false);
this.remoteAdbError.set(
error?.error?.detail || "无法测试 ADB 服务器连接。"
);
}
});
}
public activateRemoteAdbServer(): void {
const tested = this.remoteAdbProbeResult();
if (!tested?.success) {
this.remoteAdbError.set("请先测试服务器连接。");
return;
}
this.isActivatingRemoteAdb.set(true);
this.remoteAdbError.set(null);
this.systemService.connectAdbServer(
tested.endpoint.host,
tested.endpoint.port,
this.rememberRemoteAdb()
).subscribe({
next: response => {
this.isActivatingRemoteAdb.set(false);
const result = response.connection_result;
if (result.success) {
this.remoteAdbProbeResult.set(result);
this.remoteAdbDevices.set(result.devices || []);
this.remoteAdbMessage.set(result.message);
} else {
this.remoteAdbError.set(result.message);
}
},
error: error => {
this.isActivatingRemoteAdb.set(false);
this.remoteAdbError.set(
error?.error?.detail || "无法使用此 ADB 服务器。"
);
}
});
}
public updateRemoteAdbHost(value: string): void {
this.remoteAdbHost.set(value);
this.clearRemoteAdbProbe();
}
public updateRemoteAdbPort(value: string): void {
this.remoteAdbPort.set(value);
this.clearRemoteAdbProbe();
}
private clearRemoteAdbProbe(): void {
this.remoteAdbProbeResult.set(null);
this.remoteAdbDevices.set([]);
this.remoteAdbMessage.set(null);
this.remoteAdbError.set(null);
}
public switchToLocalAdbServer(): void {
this.isSwitchingToLocalAdb.set(true);
this.remoteAdbError.set(null);
this.systemService.useLocalAdbServer(true).subscribe({
next: response => {
this.isSwitchingToLocalAdb.set(false);
this.remoteAdbDevices.set([]);
this.remoteAdbMessage.set(response.connection_result.message);
},
error: error => {
this.isSwitchingToLocalAdb.set(false);
this.remoteAdbError.set(
error?.error?.detail || "无法切换回本地 ADB 服务器。"
);
}
});
}
public toggleConnectionMethods(): void {
this.showConnectionMethods.update(value => !value);
}
public launchAvdEmulator(avdName: string): void {
if (this.isRemoteAdbServer()) {
return;
}
this.systemService.launchEmulator(avdName).subscribe();
}
public toggleLaunchLogs(): void {
this.showLaunchLogs.update(v => !v);
}
public stopEmulator(): void {
this.systemService.stopEmulator().subscribe();
}
public dismissEmulatorStatus(): void {
this.systemService.dismissEmulatorStatus().subscribe();
}
public selectTargetDevice(serial: string): void {
this.systemService.selectDevice(serial).subscribe();
}
public getEmulatorCommand(avdName: string): string {
const p = this.emulatorPath();
const cmd = this.isEmulatorInPath() ? 'emulator' : (p || '~/Android/Sdk/emulator/emulator');
return `${cmd} -avd ${avdName}`;
}
public copyToClipboard(text: string, id: string): void {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
this.copiedId.set(id);
setTimeout(() => {
if (this.copiedId() === id) {
this.copiedId.set(null);
}
}, 2000);
});
}
}
public setProfile(profile: 'flash' | 'pro' | 'exploration'): void {
this.selectedProfile.set(profile);
localStorage.setItem('artemis.taskProfile', profile);
if (profile === 'exploration' && !this.explorationConfig()) void this.loadExplorationSettings();
}
public setCategory(cat: SuggestionCategory): void {
this.selectedCategory.set(cat);
}
public shuffleSuggestions(): void {
this.shuffleOffset.update(v => v + 3);
}
public getAppNamesDisplay(apps: AppReference[]): string {
return apps.map(a => a.name).join(' + ');
}
public applySuggestion(item: SmartSuggestion): void {
this.taskGoal.set(item.goal);
this.selectedProfile.set(item.profile);
this.errorMessage.set(null);
}
public applyQuickPrompt(promptGoal: string, profile?: 'flash' | 'pro'): void {
this.taskGoal.set(promptGoal);
if (profile) {
this.selectedProfile.set(profile);
}
this.errorMessage.set(null);
}
public proceedToLauncher(): void {
this.activeTab.set('launcher');
}
exploration = inject(ExplorationService);
readonly reasoningValues = REASONING_VALUES;
readonly reasoningLabels = REASONING_LABELS;
readonly executionTimes = [30,60,300,600,900,1800,3600,7200,14400,28800,86400];
readonly executionTimeLabels = ['30 秒','1 分钟','5 分钟','10 分钟','15 分钟','30 分钟','1 小时','2 小时','4 小时','8 小时','24 小时'];
readonly demoTimes = this.executionTimes.slice(1);
readonly demoTimeLabels = this.executionTimeLabels.slice(1);
readonly warningTimes = [10,30,60,120,300,600];
readonly warningLabels = ['10 秒','30 秒','1 分钟','2 分钟','5 分钟','10 分钟'];
explorationSettingsOpen = signal(false);
explorationConfigValid() {
const c = this.explorationConfig();
return !!c?.model.trim() && c.demonstration_warning_seconds < c.demonstration_timeout_seconds;
}
explorationConfig = signal<ExplorationConfig | null>(null);
explorationSettingsBusy = signal(false);
explorationNotice = signal('');
private explorationRequests = new Map<string, {create: string; start: string}>();
async loadExplorationSettings() {
this.explorationSettingsBusy.set(true);
try { this.explorationConfig.set(await this.exploration.settings()); }
catch { this.errorMessage.set('探索配置读取失败,请重试。'); }
finally { this.explorationSettingsBusy.set(false); }
}
async saveExplorationSettings() {
const config = this.explorationConfig(); if (!config || this.explorationSettingsBusy() || !this.explorationConfigValid()) return;
this.explorationSettingsBusy.set(true); this.explorationNotice.set('');
try { this.explorationConfig.set(await this.exploration.saveSettings(config)); this.explorationNotice.set('已记住探索配置'); }
catch(e) { this.errorMessage.set((e as any)?.error?.detail || '配置保存失败'); }
finally { this.explorationSettingsBusy.set(false); }
}
private async startExploration(goal: string) {
const config = this.explorationConfig();
if (!config) { this.errorMessage.set('请先读取探索配置。'); this.isSubmitting.set(false); return; }
if (!this.explorationConfigValid()) { this.errorMessage.set('示范提前提醒须小于人工示范时限。'); this.isSubmitting.set(false); return; }
const taskConfig = {...this.taskPlugins()};
const key = JSON.stringify([goal, config, taskConfig]);
const ids = this.explorationRequests.get(key) || {create: crypto.randomUUID(), start: crypto.randomUUID()};
this.explorationRequests.set(key, ids);
try {
const conversation = await this.exploration.create(goal, config, taskConfig, ids.create);
const result = await this.exploration.act<{run: {id: string}}>(conversation.id, 'start', {}, ids.start);
this.explorationRequests.delete(key);
this.agentService.selectSession(result.run.id, true);
this.agentService.fetchSessions(); this.agentService.fetchStatus();
await this.router.navigate(['/workspace']);
} catch(e) { this.errorMessage.set((e as any)?.error?.detail || '探索启动失败,可重试。'); }
finally { this.isSubmitting.set(false); }
}
public runTask(): void {
if (this.isSubmitting()) return;
const goal = this.taskGoal().trim();
if (!goal) {
this.errorMessage.set("请先输入任务目标。");
return;
}
if (!this.isReady()) {
this.errorMessage.set("运行条件尚未满足,请先检查系统设置。");
this.activeTab.set('diagnostics');
return;
}
this.isSubmitting.set(true);
this.errorMessage.set(null);
const profile = this.selectedProfile();
if (profile === 'exploration') { void this.startExploration(goal); return; }
this.agentService
.runTask(
goal,
profile,
this.selectedProfile() === 'pro' && this.expectedOutput().trim()
? this.expectedOutput().trim()
: undefined,
undefined,
this.selectedProfile() === 'pro'
? { verificationLevel: this.verificationLevel().id, explorerMode: this.explorerMode().id }
: undefined,
{ ...this.taskPlugins() }
)
.subscribe({
next: () => {
this.isSubmitting.set(false);
this.router.navigate(['/workspace']);
},
error: (err) => {
console.error('Failed to submit task from home page:', err);
this.isSubmitting.set(false);
this.errorMessage.set(
err?.error?.detail || "提交任务失败,请检查服务器连接。"
);
}
});
}
}