Files
Artemis-Eapil/apps/showcase_ui/src/app/components/featured-tasks/featured-tasks.component.ts
T

92 lines
7.2 KiB
TypeScript

import { Component, ChangeDetectionStrategy, computed, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { FavoritesService, Favorite } from '../../services/favorites.service';
import { AgentService } from '../../services/agent.service';
import { SystemService } from '../../services/system.service';
import { SettingSliderComponent, REASONING_VALUES, REASONING_LABELS } from '../setting-slider/setting-slider.component';
import { ModelSelectorComponent } from '../model-selector/model-selector.component';
interface ModelDraft {profile: string; model: string; reasoning_effort: string; device_serial: string;}
interface Draft extends ModelDraft {name: string; uniform: boolean; cases: ModelDraft[];}
@Component({
selector: 'app-featured-tasks', standalone: true, imports: [FormsModule, SettingSliderComponent, ModelSelectorComponent],
templateUrl: './featured-tasks.component.html', styleUrl: './featured-tasks.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FeaturedTasksComponent {
api = inject(FavoritesService); system = inject(SystemService); private agent = inject(AgentService); private router = inject(Router);
query = signal(''); filter = signal('all'); expanded = signal<string | null>(null);
busy = signal<string | null>(null); error = signal(''); notice = signal('');
drafts = new Map<string, Draft>();
readonly efforts = REASONING_VALUES; readonly effortLabels = REASONING_LABELS;
readonly filters = [{id:'all',label:'全部'},{id:'flash',label:'Flash'},{id:'pro',label:'Pro'},{id:'exploration',label:'探索'},{id:'batch',label:'批量'}];
filtered = computed(() => this.api.items().filter(item => {
const filter = this.filter(), query = this.query().trim().toLowerCase();
return (filter === 'all' || (filter === 'batch' ? item.kind === 'batch' : filter === 'exploration' ? ['exploration','exploration_case'].includes(item.kind) : item.kind !== 'batch' && this.options(item)['profile'] === filter)) && (!query || `${item.name} ${item.snapshot.goal || ''}`.toLowerCase().includes(query));
}));
constructor() { void this.api.load(); }
options(item: Favorite): Record<string, any> { return item.kind === 'exploration' ? {...item.snapshot.task_config,...item.snapshot.config,profile:'exploration'} : item.snapshot.runtime_overrides || {}; }
mode(item: Favorite) { return item.kind === 'batch' ? '批量' : item.kind === 'exploration' ? '重新探索' : (this.options(item)['profile'] === 'pro' ? 'Pro' : 'Flash') + (item.kind === 'exploration_case' ? ' · 用例复跑' : ''); }
summary(item: Favorite) { return item.kind === 'batch' ? `${item.snapshot.cases?.length || 0} 个用例 · ${item.snapshot.repeat_count || 1} 轮 · 保留各项配置` : `${this.options(item)['model'] || '当前默认'} / ${this.options(item)['reasoning_effort'] || '当前默认'}`; }
computer(options: Record<string, any>) { return options['execution_target'] === 'computer'; }
modelDraft(options: Record<string, any>): ModelDraft { return {profile:options['profile'] || 'flash', model:options['model'] || '', reasoning_effort:options['reasoning_effort'] || 'default', device_serial:this.computer(options) ? '' : options['device_serial'] || ''}; }
draft(item: Favorite): Draft {
let draft = this.drafts.get(item.id);
if (!draft) { draft = {...this.modelDraft(item.kind === 'batch' ? item.snapshot.cases?.[0]?.task_config || {} : this.options(item)), name:item.name, uniform:false, cases:(item.snapshot.cases || []).map(c => this.modelDraft(c.task_config))}; this.drafts.set(item.id,draft); }
return draft;
}
toggle(item: Favorite) { this.expanded.set(this.expanded() === item.id ? null : item.id); }
private changes(current: ModelDraft, original: Record<string, any>) {
const baseline = this.modelDraft(original), result: Record<string, unknown> = {};
for (const key of ['profile','model','reasoning_effort','device_serial'] as const) {
if (current[key] !== baseline[key] && current[key]) result[key] = current[key];
}
return result;
}
payload(item: Favorite): Record<string, unknown> {
const draft = this.drafts.get(item.id); if (!draft) return {};
if (item.kind === 'batch') {
const case_configs: Record<number, unknown> = {};
draft.cases.forEach((c,i) => { const changes = this.changes(c,item.snapshot.cases![i].task_config); if(Object.keys(changes).length)case_configs[i]=changes; });
return {case_configs, ...(draft.uniform ? {task_config:{model:draft.model,reasoning_effort:draft.reasoning_effort}} : {})};
}
return {task_config:this.changes(draft,this.options(item))};
}
private deviceMissing(item: Favorite) {
const devices = this.system.connectedDevices();
const cases = item.kind === 'batch' ? item.snapshot.cases!.map((c,i) => ({...c.task_config,...(this.payload(item)['case_configs'] as any)?.[i]})) : [{...this.options(item),...(this.payload(item)['task_config'] as any)}];
return cases.some(options => !this.computer(options) && (!options['device_serial'] || !devices.some(d => d.serial === options['device_serial'] && d.state === 'device')));
}
async save(item: Favorite) {
if(!this.validModels(item)){this.error.set('模型名称不能为空');return;}
if(this.busy())return;this.busy.set(item.id);this.error.set('');this.notice.set('');
try { const saved = await this.api.update(item.id,{...this.payload(item),name:this.draft(item).name});this.drafts.delete(item.id);this.draft(saved);this.notice.set('已保存收藏的默认配置'); }
catch(e){this.error.set((e as any)?.error?.detail || '保存失败,请重试');}finally{this.busy.set(null);}
}
async remove(item: Favorite) {
if(this.busy())return;this.busy.set(item.id);this.error.set('');
try{await this.api.remove(item.id);this.drafts.delete(item.id);}catch(e){this.error.set((e as any)?.error?.detail || '取消收藏失败');}finally{this.busy.set(null);}
}
async run(item: Favorite) {
if(this.busy())return;this.error.set('');this.notice.set('');
if(!this.validModels(item)){this.error.set('模型名称不能为空');return;}
if(this.deviceMissing(item)){this.expanded.set(item.id);this.error.set('原设备不在线,请在配置中选择可用设备后运行。');return;}
const draft=this.drafts.get(item.id);if(draft?.uniform && !draft.model.trim()){this.error.set('请填写统一使用的模型');return;}
this.busy.set(item.id);
try{
const result=await this.api.run(item.id,this.payload(item));
this.agent.fetchSessions();
if(result.batch_id){this.agent.batchFilter.set(result.batch_id);this.agent.activeTab.set('tasks');}
else{const id=result.session_id || result.admission?.run?.id;if(id)this.agent.selectSession(id,true);}
await this.router.navigate(['/workspace']);
}catch(e){this.error.set((e as any)?.error?.detail || '运行请求失败;重试将沿用相同请求编号,避免重复执行。');}
finally{this.busy.set(null);}
}
private validModels(item: Favorite) {
const draft=this.drafts.get(item.id);if(!draft)return true;
return item.kind === 'batch' ? draft.uniform ? !!draft.model.trim() : draft.cases.every(c => !!c.model.trim()) : !!draft.model.trim();
}
}