92 lines
7.2 KiB
TypeScript
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();
|
|
}
|
|
}
|