| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { create } from 'zustand'; |
| import type { User } from '@/types/agent'; |
|
|
| export interface PlanItem { |
| id: string; |
| content: string; |
| status: 'pending' | 'in_progress' | 'completed'; |
| } |
|
|
| export interface PanelSection { |
| content: string; |
| language: string; |
| } |
|
|
| export interface PanelData { |
| title: string; |
| script?: PanelSection; |
| output?: PanelSection; |
| input?: PanelSection; |
| parameters?: Record<string, unknown>; |
| } |
|
|
| export type PanelView = 'script' | 'output'; |
|
|
| export interface LLMHealthError { |
| error: string; |
| errorType: 'auth' | 'credits' | 'rate_limit' | 'network' | 'unknown'; |
| model: string; |
| } |
|
|
| export interface JobsUpgradeState { |
| message: string; |
| namespace?: string | null; |
| } |
|
|
| export interface ToolBudgetBlockState { |
| reason?: string | null; |
| estimatedCostUsd?: number | null; |
| remainingCapUsd?: number | null; |
| } |
|
|
| export type ActivityStatus = |
| | { type: 'idle' } |
| | { type: 'thinking' } |
| | { type: 'tool'; toolName: string; description?: string } |
| | { type: 'waiting-approval' } |
| | { type: 'streaming' } |
| | { type: 'cancelled' }; |
|
|
| export interface ResearchAgentStats { |
| toolCount: number; |
| tokenCount: number; |
| startedAt: number | null; |
| finalElapsed: number | null; |
| } |
|
|
| export interface ResearchAgentState { |
| label: string; |
| steps: string[]; |
| stats: ResearchAgentStats; |
| } |
|
|
| |
| export interface PerSessionState { |
| isProcessing: boolean; |
| activityStatus: ActivityStatus; |
| panelData: PanelData | null; |
| panelView: PanelView; |
| panelEditable: boolean; |
| plan: PlanItem[]; |
| |
| researchAgents: Record<string, ResearchAgentState>; |
| |
| researchSteps: string[]; |
| |
| researchStats: ResearchAgentStats; |
| } |
|
|
| const defaultResearchStats: ResearchAgentStats = { toolCount: 0, tokenCount: 0, startedAt: null, finalElapsed: null }; |
|
|
| const defaultSessionState: PerSessionState = { |
| isProcessing: false, |
| activityStatus: { type: 'idle' }, |
| panelData: null, |
| panelView: 'script', |
| panelEditable: false, |
| plan: [], |
| researchAgents: {}, |
| researchSteps: [], |
| researchStats: { ...defaultResearchStats }, |
| }; |
|
|
| interface AgentStore { |
| |
| sessionStates: Record<string, PerSessionState>; |
| activeSessionId: string | null; |
|
|
| |
| isProcessing: boolean; |
| isConnected: boolean; |
| activityStatus: ActivityStatus; |
| user: User | null; |
| llmHealthError: LLMHealthError | null; |
| |
| claudeQuotaExhausted: boolean; |
| jobsUpgradeRequired: JobsUpgradeState | null; |
|
|
| |
| panelData: PanelData | null; |
| panelView: PanelView; |
| panelEditable: boolean; |
|
|
| |
| plan: PlanItem[]; |
|
|
| |
| editedScripts: Record<string, string>; |
|
|
| |
| jobUrls: Record<string, string>; |
|
|
| |
| jobStatuses: Record<string, string>; |
|
|
| |
| |
| |
| trackioDashboards: Record<string, { spaceId: string; project?: string }>; |
|
|
| |
| toolErrors: Record<string, boolean>; |
|
|
| |
| rejectedTools: Record<string, boolean>; |
|
|
| |
| budgetBlocks: Record<string, ToolBudgetBlockState>; |
|
|
| |
|
|
| |
| updateSession: (sessionId: string, updates: Partial<PerSessionState>) => void; |
|
|
| |
| getSessionState: (sessionId: string) => PerSessionState; |
|
|
| |
| switchActiveSession: (sessionId: string) => void; |
|
|
| |
| clearSessionState: (sessionId: string) => void; |
|
|
| |
| setProcessing: (isProcessing: boolean) => void; |
| setConnected: (isConnected: boolean) => void; |
| setActivityStatus: (status: ActivityStatus) => void; |
| setUser: (user: User | null) => void; |
| setLlmHealthError: (error: LLMHealthError | null) => void; |
| setClaudeQuotaExhausted: (exhausted: boolean) => void; |
| setJobsUpgradeRequired: (state: JobsUpgradeState | null) => void; |
|
|
| setPanel: (data: PanelData, view?: PanelView, editable?: boolean) => void; |
| setPanelView: (view: PanelView) => void; |
| setPanelOutput: (output: PanelSection) => void; |
| updatePanelScript: (content: string) => void; |
| lockPanel: () => void; |
| clearPanel: () => void; |
|
|
| setPlan: (plan: PlanItem[]) => void; |
|
|
| setEditedScript: (toolCallId: string, content: string) => void; |
| getEditedScript: (toolCallId: string) => string | undefined; |
| clearEditedScripts: () => void; |
|
|
| setJobUrl: (toolCallId: string, jobUrl: string) => void; |
| getJobUrl: (toolCallId: string) => string | undefined; |
|
|
| setJobStatus: (toolCallId: string, status: string) => void; |
| getJobStatus: (toolCallId: string) => string | undefined; |
|
|
| setTrackioDashboard: (toolCallId: string, spaceId: string, project?: string) => void; |
| getTrackioDashboard: (toolCallId: string) => { spaceId: string; project?: string } | undefined; |
|
|
| setToolError: (toolCallId: string, hasError: boolean) => void; |
| getToolError: (toolCallId: string) => boolean | undefined; |
|
|
| setToolRejected: (toolCallId: string, isRejected: boolean) => void; |
| getToolRejected: (toolCallId: string) => boolean | undefined; |
|
|
| setToolBudgetBlock: (toolCallId: string, block: ToolBudgetBlockState | null) => void; |
| getToolBudgetBlock: (toolCallId: string) => ToolBudgetBlockState | undefined; |
| } |
|
|
| |
| |
| |
| |
| |
| function syncSnapshot( |
| state: AgentStore, |
| patch: Partial<PerSessionState>, |
| ): { sessionStates: Record<string, PerSessionState> } | Record<string, never> { |
| const { activeSessionId, sessionStates } = state; |
| if (!activeSessionId || !sessionStates[activeSessionId]) return {}; |
| return { |
| sessionStates: { |
| ...sessionStates, |
| [activeSessionId]: { ...sessionStates[activeSessionId], ...patch }, |
| }, |
| }; |
| } |
|
|
| |
| function loadToolErrors(): Record<string, boolean> { |
| try { |
| const stored = localStorage.getItem('hf-agent-tool-errors'); |
| return stored ? JSON.parse(stored) : {}; |
| } catch { |
| return {}; |
| } |
| } |
|
|
| |
| function saveToolErrors(errors: Record<string, boolean>): void { |
| try { |
| localStorage.setItem('hf-agent-tool-errors', JSON.stringify(errors)); |
| } catch (e) { |
| console.warn('Failed to persist tool errors:', e); |
| } |
| } |
|
|
| |
| function loadRejectedTools(): Record<string, boolean> { |
| try { |
| const stored = localStorage.getItem('hf-agent-rejected-tools'); |
| return stored ? JSON.parse(stored) : {}; |
| } catch { |
| return {}; |
| } |
| } |
|
|
| |
| function saveRejectedTools(rejected: Record<string, boolean>): void { |
| try { |
| localStorage.setItem('hf-agent-rejected-tools', JSON.stringify(rejected)); |
| } catch (e) { |
| console.warn('Failed to persist rejected tools:', e); |
| } |
| } |
|
|
| |
| |
| |
| function loadTrackioDashboards(): Record<string, { spaceId: string; project?: string }> { |
| try { |
| const stored = localStorage.getItem('hf-agent-trackio-dashboards'); |
| return stored ? JSON.parse(stored) : {}; |
| } catch { |
| return {}; |
| } |
| } |
|
|
| function saveTrackioDashboards(dashboards: Record<string, { spaceId: string; project?: string }>): void { |
| try { |
| localStorage.setItem('hf-agent-trackio-dashboards', JSON.stringify(dashboards)); |
| } catch (e) { |
| console.warn('Failed to persist trackio dashboards:', e); |
| } |
| } |
|
|
| export const useAgentStore = create<AgentStore>()((set, get) => ({ |
| sessionStates: {}, |
| activeSessionId: null, |
|
|
| isProcessing: false, |
| isConnected: false, |
| activityStatus: { type: 'idle' }, |
| user: null, |
| llmHealthError: null, |
| claudeQuotaExhausted: false, |
| jobsUpgradeRequired: null, |
|
|
| panelData: null, |
| panelView: 'script', |
| panelEditable: false, |
|
|
| plan: [], |
|
|
| editedScripts: {}, |
| jobUrls: {}, |
| jobStatuses: {}, |
| trackioDashboards: loadTrackioDashboards(), |
| toolErrors: loadToolErrors(), |
| rejectedTools: loadRejectedTools(), |
| budgetBlocks: {}, |
|
|
| |
|
|
| updateSession: (sessionId, updates) => { |
| const state = get(); |
| const current = state.sessionStates[sessionId] || { ...defaultSessionState }; |
| const updated = { ...current, ...updates }; |
|
|
| |
| const processingCleared = 'isProcessing' in updates && !updates.isProcessing; |
| if (processingCleared) { |
| if (updated.activityStatus.type !== 'waiting-approval' && updated.activityStatus.type !== 'cancelled') { |
| updated.activityStatus = { type: 'idle' }; |
| } |
| } |
|
|
| const isActive = state.activeSessionId === sessionId; |
|
|
| |
| |
| |
| |
| const flatMirror: Record<string, unknown> = {}; |
| if (isActive) { |
| for (const key of Object.keys(updates)) { |
| flatMirror[key] = updated[key as keyof PerSessionState]; |
| } |
| |
| if (processingCleared) { |
| flatMirror.activityStatus = updated.activityStatus; |
| } |
| } |
|
|
| set({ |
| sessionStates: { ...state.sessionStates, [sessionId]: updated }, |
| ...flatMirror, |
| }); |
| }, |
|
|
| getSessionState: (sessionId) => { |
| return get().sessionStates[sessionId] || { ...defaultSessionState }; |
| }, |
|
|
| switchActiveSession: (sessionId) => { |
| const state = get(); |
|
|
| |
| const updatedStates = { ...state.sessionStates }; |
|
|
| |
| if (state.activeSessionId && state.activeSessionId !== sessionId) { |
| updatedStates[state.activeSessionId] = { |
| isProcessing: state.isProcessing, |
| activityStatus: state.activityStatus, |
| panelData: state.panelData, |
| panelView: state.panelView, |
| panelEditable: state.panelEditable, |
| plan: state.plan, |
| researchAgents: state.sessionStates[state.activeSessionId]?.researchAgents ?? {}, |
| researchSteps: state.sessionStates[state.activeSessionId]?.researchSteps ?? [], |
| researchStats: state.sessionStates[state.activeSessionId]?.researchStats ?? { ...defaultResearchStats }, |
| }; |
| } |
|
|
| |
| const incoming = updatedStates[sessionId] || { ...defaultSessionState }; |
| set({ |
| activeSessionId: sessionId, |
| sessionStates: updatedStates, |
| isProcessing: incoming.isProcessing, |
| activityStatus: incoming.activityStatus, |
| panelData: incoming.panelData, |
| panelView: incoming.panelView, |
| panelEditable: incoming.panelEditable, |
| plan: incoming.plan, |
| }); |
| }, |
|
|
| clearSessionState: (sessionId) => { |
| set((state) => { |
| const rest = { ...state.sessionStates }; |
| delete rest[sessionId]; |
| return { sessionStates: rest }; |
| }); |
| }, |
|
|
| |
|
|
| setProcessing: (isProcessing) => { |
| const current = get().activityStatus; |
| const preserveStatus = current.type === 'waiting-approval' || current.type === 'cancelled'; |
| set({ isProcessing, ...(!isProcessing && !preserveStatus ? { activityStatus: { type: 'idle' } } : {}) }); |
| }, |
| setConnected: (isConnected) => set({ isConnected }), |
| setActivityStatus: (status) => set({ activityStatus: status }), |
| setUser: (user) => set({ user }), |
| setLlmHealthError: (error) => set({ llmHealthError: error }), |
| setClaudeQuotaExhausted: (exhausted) => set({ claudeQuotaExhausted: exhausted }), |
| setJobsUpgradeRequired: (state) => set({ jobsUpgradeRequired: state }), |
|
|
| |
| |
| |
|
|
| setPanel: (data, view, editable) => set((state) => { |
| const patch: Partial<PerSessionState> = { |
| panelData: data, |
| panelView: view ?? (data.script ? 'script' : 'output'), |
| panelEditable: editable ?? false, |
| }; |
| return { ...patch, ...syncSnapshot(state, patch) }; |
| }), |
|
|
| setPanelView: (view) => set((state) => { |
| const patch: Partial<PerSessionState> = { panelView: view }; |
| return { ...patch, ...syncSnapshot(state, patch) }; |
| }), |
|
|
| setPanelOutput: (output) => set((state) => { |
| const panelData = state.panelData |
| ? { ...state.panelData, output } |
| : { title: 'Output', output }; |
| const patch: Partial<PerSessionState> = { panelData, panelView: 'output' }; |
| return { ...patch, ...syncSnapshot(state, patch) }; |
| }), |
|
|
| updatePanelScript: (content) => set((state) => { |
| const panelData = state.panelData?.script |
| ? { ...state.panelData, script: { ...state.panelData.script, content } } |
| : state.panelData; |
| if (!panelData) return {}; |
| const patch: Partial<PerSessionState> = { panelData }; |
| return { ...patch, ...syncSnapshot(state, patch) }; |
| }), |
|
|
| lockPanel: () => set((state) => { |
| const patch: Partial<PerSessionState> = { panelEditable: false }; |
| return { ...patch, ...syncSnapshot(state, patch) }; |
| }), |
|
|
| clearPanel: () => set((state) => { |
| const patch: Partial<PerSessionState> = { panelData: null, panelView: 'script', panelEditable: false }; |
| return { ...patch, ...syncSnapshot(state, patch) }; |
| }), |
|
|
| |
|
|
| setPlan: (plan) => set((state) => { |
| const patch: Partial<PerSessionState> = { plan }; |
| return { ...patch, ...syncSnapshot(state, patch) }; |
| }), |
|
|
| |
|
|
| setEditedScript: (toolCallId, content) => { |
| set((state) => ({ |
| editedScripts: { ...state.editedScripts, [toolCallId]: content }, |
| })); |
| }, |
|
|
| getEditedScript: (toolCallId) => get().editedScripts[toolCallId], |
|
|
| clearEditedScripts: () => set({ editedScripts: {} }), |
|
|
| |
|
|
| setJobUrl: (toolCallId, jobUrl) => { |
| set((state) => ({ |
| jobUrls: { ...state.jobUrls, [toolCallId]: jobUrl }, |
| })); |
| }, |
|
|
| getJobUrl: (toolCallId) => get().jobUrls[toolCallId], |
|
|
| |
|
|
| setJobStatus: (toolCallId, status) => { |
| set((state) => ({ |
| jobStatuses: { ...state.jobStatuses, [toolCallId]: status }, |
| })); |
| }, |
|
|
| getJobStatus: (toolCallId) => get().jobStatuses[toolCallId], |
|
|
| |
|
|
| setTrackioDashboard: (toolCallId, spaceId, project) => { |
| set((state) => { |
| const existing = state.trackioDashboards[toolCallId]; |
| |
| if (existing && existing.spaceId === spaceId && existing.project === project) { |
| return {}; |
| } |
| const updated = { |
| ...state.trackioDashboards, |
| [toolCallId]: { spaceId, ...(project ? { project } : {}) }, |
| }; |
| saveTrackioDashboards(updated); |
| return { trackioDashboards: updated }; |
| }); |
| }, |
|
|
| getTrackioDashboard: (toolCallId) => get().trackioDashboards[toolCallId], |
|
|
| |
|
|
| setToolError: (toolCallId, hasError) => { |
| set((state) => { |
| const updated = { ...state.toolErrors }; |
| if (hasError) { |
| updated[toolCallId] = true; |
| } else { |
| delete updated[toolCallId]; |
| } |
| saveToolErrors(updated); |
| return { toolErrors: updated }; |
| }); |
| }, |
|
|
| getToolError: (toolCallId) => get().toolErrors[toolCallId], |
|
|
| |
|
|
| setToolRejected: (toolCallId, isRejected) => { |
| set((state) => { |
| const updated = { ...state.rejectedTools, [toolCallId]: isRejected }; |
| saveRejectedTools(updated); |
| return { rejectedTools: updated }; |
| }); |
| }, |
|
|
| getToolRejected: (toolCallId) => get().rejectedTools[toolCallId], |
|
|
| |
|
|
| setToolBudgetBlock: (toolCallId, block) => { |
| set((state) => { |
| if (!block) { |
| const next = { ...state.budgetBlocks }; |
| delete next[toolCallId]; |
| return { budgetBlocks: next }; |
| } |
| return { |
| budgetBlocks: { |
| ...state.budgetBlocks, |
| [toolCallId]: block, |
| }, |
| }; |
| }); |
| }, |
|
|
| getToolBudgetBlock: (toolCallId) => get().budgetBlocks[toolCallId], |
| })); |
|
|