From 194f5f0c4e92c9a1c5d80c84df85245a7ec6a6ec Mon Sep 17 00:00:00 2001 From: mrveiss Date: Wed, 1 Apr 2026 22:42:05 +0300 Subject: [PATCH 1/2] feat(frontend): add AutoResearch experiment dashboard (#3201) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/autoresearch/ApprovalCard.vue | 85 ++++++ .../AutoResearchWorkflowAdapter.vue | 61 ++++ .../autoresearch/ExperimentTimeline.vue | 102 +++++++ .../components/autoresearch/InsightsPanel.vue | 79 +++++ .../autoresearch/PromptOptimizerPanel.vue | 135 +++++++++ .../__tests__/ApprovalCard.spec.ts | 43 +++ .../__tests__/useAutoResearch.spec.ts | 64 +++++ .../src/composables/useAutoResearch.ts | 271 ++++++++++++++++++ autobot-frontend/src/router/index.ts | 12 + .../src/stores/useAutoResearchStore.ts | 64 +++++ .../src/views/ExperimentDashboard.vue | 127 ++++++++ 11 files changed, 1043 insertions(+) create mode 100644 autobot-frontend/src/components/autoresearch/ApprovalCard.vue create mode 100644 autobot-frontend/src/components/autoresearch/AutoResearchWorkflowAdapter.vue create mode 100644 autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue create mode 100644 autobot-frontend/src/components/autoresearch/InsightsPanel.vue create mode 100644 autobot-frontend/src/components/autoresearch/PromptOptimizerPanel.vue create mode 100644 autobot-frontend/src/components/autoresearch/__tests__/ApprovalCard.spec.ts create mode 100644 autobot-frontend/src/composables/__tests__/useAutoResearch.spec.ts create mode 100644 autobot-frontend/src/composables/useAutoResearch.ts create mode 100644 autobot-frontend/src/stores/useAutoResearchStore.ts create mode 100644 autobot-frontend/src/views/ExperimentDashboard.vue diff --git a/autobot-frontend/src/components/autoresearch/ApprovalCard.vue b/autobot-frontend/src/components/autoresearch/ApprovalCard.vue new file mode 100644 index 000000000..e83482ee6 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/ApprovalCard.vue @@ -0,0 +1,85 @@ + + + + + + diff --git a/autobot-frontend/src/components/autoresearch/AutoResearchWorkflowAdapter.vue b/autobot-frontend/src/components/autoresearch/AutoResearchWorkflowAdapter.vue new file mode 100644 index 000000000..2ee78e4f0 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/AutoResearchWorkflowAdapter.vue @@ -0,0 +1,61 @@ + + + + + + diff --git a/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue b/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue new file mode 100644 index 000000000..bcd33c042 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue @@ -0,0 +1,102 @@ + + + + + + diff --git a/autobot-frontend/src/components/autoresearch/InsightsPanel.vue b/autobot-frontend/src/components/autoresearch/InsightsPanel.vue new file mode 100644 index 000000000..a06d5ab24 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/InsightsPanel.vue @@ -0,0 +1,79 @@ + + + + + + diff --git a/autobot-frontend/src/components/autoresearch/PromptOptimizerPanel.vue b/autobot-frontend/src/components/autoresearch/PromptOptimizerPanel.vue new file mode 100644 index 000000000..bbbff3e5f --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/PromptOptimizerPanel.vue @@ -0,0 +1,135 @@ + + + + + + diff --git a/autobot-frontend/src/components/autoresearch/__tests__/ApprovalCard.spec.ts b/autobot-frontend/src/components/autoresearch/__tests__/ApprovalCard.spec.ts new file mode 100644 index 000000000..af73c3799 --- /dev/null +++ b/autobot-frontend/src/components/autoresearch/__tests__/ApprovalCard.spec.ts @@ -0,0 +1,43 @@ +// AutoBot - AI-Powered Automation Platform +// Copyright (c) 2025 mrveiss +// Author: mrveiss + +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import ApprovalCard from '../ApprovalCard.vue' + +describe('ApprovalCard', () => { + const defaultProps = { + approval: { + sessionId: 's1', + experimentId: 'e1', + metrics: { + baseline_val_bpb: 5.0, + result_val_bpb: 4.5, + improvement_pct: 10.0, + }, + }, + } + + it('renders approval details', () => { + const wrapper = mount(ApprovalCard, { props: defaultProps }) + expect(wrapper.text()).toContain('Approval Required') + expect(wrapper.text()).toContain('5.0000') + expect(wrapper.text()).toContain('4.5000') + expect(wrapper.text()).toContain('10.00%') + }) + + it('emits approve event on button click', async () => { + const wrapper = mount(ApprovalCard, { props: defaultProps }) + await wrapper.find('button:first-of-type').trigger('click') + expect(wrapper.emitted('approve')).toBeTruthy() + expect(wrapper.emitted('approve')![0]).toEqual(['s1', 'e1']) + }) + + it('emits reject event on button click', async () => { + const wrapper = mount(ApprovalCard, { props: defaultProps }) + const buttons = wrapper.findAll('button') + await buttons[1].trigger('click') + expect(wrapper.emitted('reject')).toBeTruthy() + }) +}) diff --git a/autobot-frontend/src/composables/__tests__/useAutoResearch.spec.ts b/autobot-frontend/src/composables/__tests__/useAutoResearch.spec.ts new file mode 100644 index 000000000..eef9274e8 --- /dev/null +++ b/autobot-frontend/src/composables/__tests__/useAutoResearch.spec.ts @@ -0,0 +1,64 @@ +// AutoBot - AI-Powered Automation Platform +// Copyright (c) 2025 mrveiss +// Author: mrveiss + +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useAutoResearch } from '../useAutoResearch' + +const mockGet = vi.fn() +const mockPost = vi.fn() + +vi.mock('../useApi', () => ({ + useApi: () => ({ + get: mockGet, + post: mockPost, + }), +})) + +describe('useAutoResearch', () => { + beforeEach(() => { + vi.clearAllMocks() + // Re-apply mocks (mockReset: true wipes vi.mock factories) + mockGet.mockReset() + mockPost.mockReset() + }) + + it('fetchExperiments populates experiments ref', async () => { + mockGet.mockResolvedValue({ + experiments: [{ id: 'e1', state: 'completed', hypothesis: 'test' }], + }) + + const { experiments, fetchExperiments } = useAutoResearch() + await fetchExperiments() + + expect(experiments.value).toHaveLength(1) + expect(experiments.value[0].id).toBe('e1') + }) + + it('fetchStats populates stats ref', async () => { + mockGet.mockResolvedValue({ + total_experiments: 10, + kept: 3, + best_val_bpb: 4.5, + }) + + const { stats, fetchStats } = useAutoResearch() + await fetchStats() + + expect(stats.value).not.toBeNull() + expect(stats.value!.total_experiments).toBe(10) + }) + + it('approveExperiment sends correct request', async () => { + mockPost.mockResolvedValue({}) + mockGet.mockResolvedValue({ approvals: [] }) + + const { approveExperiment } = useAutoResearch() + await approveExperiment('s1', 'e1') + + expect(mockPost).toHaveBeenCalledWith( + '/api/autoresearch/approvals/s1/e1', + { decision: 'approved' }, + ) + }) +}) diff --git a/autobot-frontend/src/composables/useAutoResearch.ts b/autobot-frontend/src/composables/useAutoResearch.ts new file mode 100644 index 000000000..6cf89ffdd --- /dev/null +++ b/autobot-frontend/src/composables/useAutoResearch.ts @@ -0,0 +1,271 @@ +// AutoBot - AI-Powered Automation Platform +// Copyright (c) 2025 mrveiss +// Author: mrveiss + +import { ref, type Ref } from 'vue' +import { useApi } from './useApi' + +// --- Types --- + +export interface ExperimentResult { + val_bpb: number | null + train_loss: number | null + val_loss: number | null + steps_completed: number + tokens_per_second: number | null + wall_time_seconds: number + error_message: string | null +} + +export interface Experiment { + id: string + hypothesis: string + description: string + state: string + hyperparams: Record + result: ExperimentResult | null + baseline_val_bpb: number | null + tags: string[] + created_at: number + started_at: number | null + completed_at: number | null +} + +export interface ExperimentStats { + total_experiments: number + completed: number + failed: number + kept: number + discarded: number + best_val_bpb: number | null + baseline_val_bpb: number | null + avg_wall_time: number + total_wall_time: number + improvement_trend: number[] +} + +export interface PromptVariant { + id: string + prompt_text: string + output: string + scores: Record + final_score: number + round_number: number + created_at: number +} + +export interface OptimizationSession { + id: string + status: string + rounds_completed: number + max_rounds: number + best_variant: PromptVariant | null + baseline_score: number + all_variants: PromptVariant[] +} + +export interface ApprovalRequest { + session_id: string + experiment_id: string + details: Record + requested_at: number + status: string +} + +export interface ExperimentInsight { + id: string + statement: string + confidence: number + supporting_experiments: string[] + related_hyperparams: string[] + synthesized_at: number + session_id: string | null +} + +// --- Composable --- + +export function useAutoResearch() { + const api = useApi() + + const experiments: Ref = ref([]) + const stats: Ref = ref(null) + const loading = ref(false) + const error: Ref = ref(null) + + const optimizerStatus: Ref = ref(null) + const variants: Ref = ref([]) + + const pendingApprovals: Ref = ref([]) + + const insights: Ref = ref([]) + + let pollTimer: ReturnType | null = null + + // --- Experiments --- + + async function fetchExperiments(params?: { + limit?: number + offset?: number + state?: string + }): Promise { + loading.value = true + error.value = null + try { + const query = new URLSearchParams() + if (params?.limit) query.set('limit', String(params.limit)) + if (params?.offset) query.set('offset', String(params.offset)) + if (params?.state) query.set('state', params.state) + const response = await api.get(`/api/autoresearch/experiments?${query}`) + experiments.value = response.experiments ?? [] + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } finally { + loading.value = false + } + } + + async function fetchStats(): Promise { + try { + const response = await api.get('/api/autoresearch/experiments/stats') + stats.value = response + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } + } + + // --- Prompt Optimizer --- + + async function fetchOptimizerStatus(): Promise { + try { + const response = await api.get('/api/autoresearch/prompt-optimizer/status') + optimizerStatus.value = response.session ?? null + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } + } + + async function startOptimization( + agentName: string, + maxRounds: number = 3, + ): Promise { + await api.post('/api/autoresearch/prompt-optimizer/start', { + agent_name: agentName, + max_rounds: maxRounds, + }) + await fetchOptimizerStatus() + } + + async function cancelOptimization(): Promise { + await api.post('/api/autoresearch/prompt-optimizer/cancel') + await fetchOptimizerStatus() + } + + async function fetchVariants(sessionId: string): Promise { + const response = await api.get( + `/api/autoresearch/prompt-optimizer/variants/${sessionId}`, + ) + variants.value = response.variants ?? [] + } + + async function scoreVariant( + variantId: string, + sessionId: string, + score: number, + comment: string = '', + ): Promise { + await api.post( + `/api/autoresearch/prompt-optimizer/variants/${variantId}/score?session_id=${sessionId}`, + { score, comment }, + ) + } + + // --- Approvals --- + + async function fetchPendingApprovals(): Promise { + const response = await api.get('/api/autoresearch/approvals/pending') + pendingApprovals.value = response.approvals ?? [] + } + + async function approveExperiment( + sessionId: string, + experimentId: string, + ): Promise { + await api.post( + `/api/autoresearch/approvals/${sessionId}/${experimentId}`, + { decision: 'approved' }, + ) + await fetchPendingApprovals() + } + + async function rejectExperiment( + sessionId: string, + experimentId: string, + ): Promise { + await api.post( + `/api/autoresearch/approvals/${sessionId}/${experimentId}`, + { decision: 'rejected' }, + ) + await fetchPendingApprovals() + } + + // --- Insights --- + + async function fetchInsights(minConfidence: number = 0): Promise { + const response = await api.get( + `/api/autoresearch/insights?min_confidence=${minConfidence}`, + ) + insights.value = response.insights ?? [] + } + + async function searchInsights(query: string): Promise { + const response = await api.get( + `/api/autoresearch/insights/search?q=${encodeURIComponent(query)}`, + ) + insights.value = response.insights ?? [] + } + + // --- Polling --- + + function startPolling(intervalMs: number = 10000): void { + stopPolling() + pollTimer = setInterval(async () => { + await Promise.all([ + fetchExperiments(), + fetchStats(), + fetchOptimizerStatus(), + fetchPendingApprovals(), + ]) + }, intervalMs) + } + + function stopPolling(): void { + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } + } + + return { + experiments, + stats, + loading, + error, + fetchExperiments, + fetchStats, + optimizerStatus, + startOptimization, + cancelOptimization, + variants, + fetchVariants, + scoreVariant, + pendingApprovals, + fetchPendingApprovals, + approveExperiment, + rejectExperiment, + insights, + fetchInsights, + searchInsights, + startPolling, + stopPolling, + } +} diff --git a/autobot-frontend/src/router/index.ts b/autobot-frontend/src/router/index.ts index e9ff90a23..e8bf786cc 100644 --- a/autobot-frontend/src/router/index.ts +++ b/autobot-frontend/src/router/index.ts @@ -538,6 +538,18 @@ const routes: RouteRecordRaw[] = [ requiresAuth: true } }, + // Issue #3201: AutoResearch Experiment Dashboard + { + path: '/experiments', + name: 'experiments', + component: () => import('@/views/ExperimentDashboard.vue'), + meta: { + title: 'Experiments', + icon: 'BeakerIcon', + description: 'AutoResearch experiment dashboard', + requiresAuth: true, + }, + }, // Issue #729: Infrastructure routes redirected to slm-admin // These routes are kept as redirects for backwards compatibility diff --git a/autobot-frontend/src/stores/useAutoResearchStore.ts b/autobot-frontend/src/stores/useAutoResearchStore.ts new file mode 100644 index 000000000..592b07037 --- /dev/null +++ b/autobot-frontend/src/stores/useAutoResearchStore.ts @@ -0,0 +1,64 @@ +// AutoBot - AI-Powered Automation Platform +// Copyright (c) 2025 mrveiss +// Author: mrveiss + +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { + Experiment, + ExperimentStats, + OptimizationSession, + ApprovalRequest, + ExperimentInsight, +} from '@/composables/useAutoResearch' + +export const useAutoResearchStore = defineStore('autoResearch', () => { + const experiments = ref([]) + const stats = ref(null) + const optimizerSession = ref(null) + const pendingApprovals = ref([]) + const insights = ref([]) + const isPolling = ref(false) + const lastFetchedAt = ref(null) + + function setExperiments(data: Experiment[]) { + experiments.value = data + lastFetchedAt.value = Date.now() + } + + function setStats(data: ExperimentStats) { + stats.value = data + } + + function setOptimizerSession(session: OptimizationSession | null) { + optimizerSession.value = session + } + + function setPendingApprovals(approvals: ApprovalRequest[]) { + pendingApprovals.value = approvals + } + + function setInsights(data: ExperimentInsight[]) { + insights.value = data + } + + function setPolling(polling: boolean) { + isPolling.value = polling + } + + return { + experiments, + stats, + optimizerSession, + pendingApprovals, + insights, + isPolling, + lastFetchedAt, + setExperiments, + setStats, + setOptimizerSession, + setPendingApprovals, + setInsights, + setPolling, + } +}) diff --git a/autobot-frontend/src/views/ExperimentDashboard.vue b/autobot-frontend/src/views/ExperimentDashboard.vue new file mode 100644 index 000000000..068484f09 --- /dev/null +++ b/autobot-frontend/src/views/ExperimentDashboard.vue @@ -0,0 +1,127 @@ + + + + + + From b08b7df5b309befab7bbcfaed1feedf458f6d76b Mon Sep 17 00:00:00 2001 From: mrveiss Date: Wed, 1 Apr 2026 22:51:20 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(frontend):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20emit=20propagation,=20offset=20filter,=20remove=20u?= =?UTF-8?q?nused=20store=20(#3201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .../autoresearch/ExperimentTimeline.vue | 4 +- .../src/composables/useAutoResearch.ts | 4 +- .../src/stores/useAutoResearchStore.ts | 64 ------------------- 3 files changed, 4 insertions(+), 68 deletions(-) delete mode 100644 autobot-frontend/src/stores/useAutoResearchStore.ts diff --git a/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue b/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue index bcd33c042..973fc8862 100644 --- a/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue +++ b/autobot-frontend/src/components/autoresearch/ExperimentTimeline.vue @@ -94,8 +94,8 @@ function getApproval(experimentId: string) { : undefined, }" class="mt-3" - @approve="emit('approve', $event, exp.id)" - @reject="emit('reject', $event, exp.id)" + @approve="(sid: string, eid: string) => emit('approve', sid, eid)" + @reject="(sid: string, eid: string) => emit('reject', sid, eid)" /> diff --git a/autobot-frontend/src/composables/useAutoResearch.ts b/autobot-frontend/src/composables/useAutoResearch.ts index 6cf89ffdd..49f342dde 100644 --- a/autobot-frontend/src/composables/useAutoResearch.ts +++ b/autobot-frontend/src/composables/useAutoResearch.ts @@ -112,8 +112,8 @@ export function useAutoResearch() { error.value = null try { const query = new URLSearchParams() - if (params?.limit) query.set('limit', String(params.limit)) - if (params?.offset) query.set('offset', String(params.offset)) + if (params?.limit != null) query.set('limit', String(params.limit)) + if (params?.offset != null) query.set('offset', String(params.offset)) if (params?.state) query.set('state', params.state) const response = await api.get(`/api/autoresearch/experiments?${query}`) experiments.value = response.experiments ?? [] diff --git a/autobot-frontend/src/stores/useAutoResearchStore.ts b/autobot-frontend/src/stores/useAutoResearchStore.ts deleted file mode 100644 index 592b07037..000000000 --- a/autobot-frontend/src/stores/useAutoResearchStore.ts +++ /dev/null @@ -1,64 +0,0 @@ -// AutoBot - AI-Powered Automation Platform -// Copyright (c) 2025 mrveiss -// Author: mrveiss - -import { defineStore } from 'pinia' -import { ref } from 'vue' -import type { - Experiment, - ExperimentStats, - OptimizationSession, - ApprovalRequest, - ExperimentInsight, -} from '@/composables/useAutoResearch' - -export const useAutoResearchStore = defineStore('autoResearch', () => { - const experiments = ref([]) - const stats = ref(null) - const optimizerSession = ref(null) - const pendingApprovals = ref([]) - const insights = ref([]) - const isPolling = ref(false) - const lastFetchedAt = ref(null) - - function setExperiments(data: Experiment[]) { - experiments.value = data - lastFetchedAt.value = Date.now() - } - - function setStats(data: ExperimentStats) { - stats.value = data - } - - function setOptimizerSession(session: OptimizationSession | null) { - optimizerSession.value = session - } - - function setPendingApprovals(approvals: ApprovalRequest[]) { - pendingApprovals.value = approvals - } - - function setInsights(data: ExperimentInsight[]) { - insights.value = data - } - - function setPolling(polling: boolean) { - isPolling.value = polling - } - - return { - experiments, - stats, - optimizerSession, - pendingApprovals, - insights, - isPolling, - lastFetchedAt, - setExperiments, - setStats, - setOptimizerSession, - setPendingApprovals, - setInsights, - setPolling, - } -})