893 lines
33 KiB
TypeScript
893 lines
33 KiB
TypeScript
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 || "提交任务失败,请检查服务器连接。"
|
||
);
|
||
}
|
||
});
|
||
}
|
||
}
|