[eric] patterns: offer toast with evidence-citing copy, accept opens the created workflow

This commit is contained in:
ciregenz
2026-07-28 11:56:52 -07:00
parent ea93e261b7
commit e0c6f796ff
6 changed files with 210 additions and 1 deletions
@@ -0,0 +1,91 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { API_BASE } from '@/shared/config';
export interface PatternCadence {
kind: 'weekly' | 'daily' | 'irregular';
on_days: number[];
hour: number;
}
export interface PatternSuggestion {
id: string;
description: string;
evidence_count: number;
first_seen: string | null;
last_seen: string | null;
cadence: PatternCadence;
workflow_title: string;
workflow_steps: string[];
}
interface PatternsState {
suggestions: PatternSuggestion[];
toastOpen: boolean;
accepting: boolean;
}
const initialState: PatternsState = {
suggestions: [],
toastOpen: false,
accepting: false,
};
export const fetchPatternSuggestions = createAsyncThunk(
'patterns/fetch',
async (): Promise<{ suggestions: PatternSuggestion[] }> => {
const r = await fetch(`${API_BASE}/patterns/suggestions`);
return (await r.json()) as { suggestions: PatternSuggestion[] };
},
);
export const acceptPatternSuggestion = createAsyncThunk(
'patterns/accept',
async (suggestionId: string): Promise<{ workflow: { id: string } }> => {
const r = await fetch(`${API_BASE}/patterns/suggestions/${suggestionId}/accept`, { method: 'POST' });
if (!r.ok) throw new Error(`accept failed: ${r.status}`);
return (await r.json()) as { workflow: { id: string } };
},
);
export const dismissPatternSuggestion = createAsyncThunk(
'patterns/dismiss',
async (suggestionId: string): Promise<string> => {
await fetch(`${API_BASE}/patterns/suggestions/${suggestionId}/dismiss`, { method: 'POST' });
return suggestionId;
},
);
const patternsSlice = createSlice({
name: 'patterns',
initialState,
reducers: {
hidePatternToast(state) {
state.toastOpen = false;
},
},
extraReducers: (builder) => {
builder.addCase(fetchPatternSuggestions.fulfilled, (state, action) => {
state.suggestions = action.payload.suggestions ?? [];
state.toastOpen = state.suggestions.length > 0;
});
builder.addCase(acceptPatternSuggestion.pending, (state) => {
state.accepting = true;
});
builder.addCase(acceptPatternSuggestion.fulfilled, (state, action) => {
state.accepting = false;
state.suggestions = state.suggestions.filter((s) => s.id !== action.meta.arg);
// The user acted; don't immediately push the next offer in their face.
state.toastOpen = false;
});
builder.addCase(acceptPatternSuggestion.rejected, (state) => {
state.accepting = false;
});
builder.addCase(dismissPatternSuggestion.fulfilled, (state, action) => {
state.suggestions = state.suggestions.filter((s) => s.id !== action.payload);
state.toastOpen = state.suggestions.length > 0;
});
},
});
export const { hidePatternToast } = patternsSlice.actions;
export default patternsSlice.reducer;
+2
View File
@@ -17,6 +17,7 @@ import interactionReducer from './interactionSlice';
import subscriptionsReducer from './subscriptionsSlice';
import workflowsReducer from './workflowsSlice';
import missedRunsReducer from './missedRunsSlice';
import patternsReducer from './patternsSlice';
import onboardingProgressReducer from '@/shared/state/onboardingProgressSlice';
import onboardingV3Reducer from '@/shared/state/onboardingV3Slice';
@@ -40,6 +41,7 @@ export const store = configureStore({
subscriptions: subscriptionsReducer,
workflows: workflowsReducer,
missedRuns: missedRunsReducer,
patterns: patternsReducer,
onboardingProgress: onboardingProgressReducer,
onboardingV3: onboardingV3Reducer,
},
@@ -33,6 +33,7 @@ import { upsertOutput } from '../state/outputsSlice';
import { fetchSettings } from '../state/settingsSlice';
import { displaySessionName } from '../state/sessionDisplay';
import { upsertRun, ackRun, runWorkflowNow, openWorkflowCard, upsertWorkflow, removeWorkflow } from '../state/workflowsSlice';
import { fetchPatternSuggestions } from '../state/patternsSlice';
import { stepsSignature } from '@/app/pages/Workflows/scheduleUtils';
import { getAuthToken } from '../config';
import { notifyAgentCompletion } from '../notifications';
@@ -756,6 +757,11 @@ class WebSocketManager {
}
break;
case 'patterns:suggestions_updated':
// The miner just found something; refetch so the offer can appear this launch, not next.
store.dispatch(fetchPatternSuggestions());
break;
case 'workflow:deleted':
if (data.workflow_id) {
store.dispatch(removeWorkflow(data.workflow_id));