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(null); busy = signal(null); error = signal(''); notice = signal(''); drafts = new Map(); 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 { 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) { return options['execution_target'] === 'computer'; } modelDraft(options: Record): 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) { const baseline = this.modelDraft(original), result: Record = {}; 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 { const draft = this.drafts.get(item.id); if (!draft) return {}; if (item.kind === 'batch') { const case_configs: Record = {}; 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(); } }