import React, { useCallback, useEffect, useRef, useState } from "react"; import { Alert, Box, Button, CircularProgress, Paper, Stack, Typography } from "@mui/material"; import { api, getApiErrorMessage } from "../api"; import { useAccountPlan } from "../accountPlan"; import { useI18n } from "../i18n/I18nProvider"; import { statusLabel } from "../pipeline"; import type { FocusPlanResponse, StatusSuggestion, StrategySnapshotOperationResponse, UserOperation } from "../types"; import { useToast } from "../toast"; import { DraftCard, ListCard, TwoColumnSection } from "./JobDetailsPanels"; const terminal = (status: UserOperation["status"]) => ["succeeded", "failed", "cancelled"].includes(status); export function ApplicationStatusSuggestion({ jobId, onApplied }: { jobId: number; onApplied: () => void }) { const [suggestion, setSuggestion] = useState(null); const [busy, setBusy] = useState(false); const { toast } = useToast(); const { t } = useI18n(); useEffect(() => { let active = true; api.get(`/jobapplications/${jobId}/status-suggestion`) .then(({ data }) => { if (active) setSuggestion(data.hasSuggestion ? data : null); }) .catch(() => { if (active) setSuggestion(null); }); return () => { active = false; }; }, [jobId]); if (!suggestion?.suggestedStatus) return null; const apply = async () => { setBusy(true); try { await api.patch(`/jobapplications/${jobId}/status`, { status: suggestion.suggestedStatus }); setSuggestion(null); onApplied(); toast(t("workspaceStatusSuggestionApplied"), "success"); } catch (error) { toast(getApiErrorMessage(error, t("workspaceStatusSuggestionFailed")), "error"); } finally { setBusy(false); } }; return ( void apply()}>{t("workspaceStatusSuggestionApply", { status: statusLabel(t, suggestion.suggestedStatus) })}} > {t("workspaceStatusSuggestionBody", { current: suggestion.currentStatus ? statusLabel(t, suggestion.currentStatus) : t("workspaceStatusSuggestionCurrent"), suggested: statusLabel(t, suggestion.suggestedStatus), })} ); } export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) { const { canUseAi } = useAccountPlan(); const { toast } = useToast(); const { t } = useI18n(); const [plan, setPlan] = useState(null); const [operation, setOperation] = useState(null); const [loading, setLoading] = useState(true); const [readingPlan, setReadingPlan] = useState(false); const [readError, setReadError] = useState<{ cause: unknown; retryable: boolean } | null>(null); const announced = useRef(null); const owner = useRef({ mounted: false, jobId, generation: 0 }); const readSequence = useRef(0); const readPending = useRef(false); const headingRef = useRef(null); const loadPlan = useCallback(async () => { if (!owner.current.mounted || owner.current.jobId !== jobId || readPending.current) return; const lifetime = owner.current.generation; const request = ++readSequence.current; const active = () => owner.current.mounted && owner.current.jobId === jobId && owner.current.generation === lifetime && request === readSequence.current; readPending.current = true; setReadingPlan(true); try { const { data } = await api.get(`/jobapplications/${jobId}/focus-plan`); if (!active()) return; setPlan(data); setReadError(null); } catch (cause) { if (!active()) return; const response = (cause as { response?: { status?: number; data?: { code?: string } } })?.response; if (response?.status === 404 && response.data?.code === "strategy_not_generated") { setPlan(null); setReadError(null); } else setReadError({ cause, retryable: !response?.status || response.status >= 500 }); } finally { if (active()) { readPending.current = false; setReadingPlan(false); } } }, [jobId]); useEffect(() => { let active = true; owner.current = { mounted: true, jobId, generation: owner.current.generation + 1 }; readPending.current = false; setPlan(null); setReadError(null); setOperation(null); setLoading(true); Promise.all([ loadPlan(), api.get(`/jobapplications/${jobId}/focus-plan/operation`) .then(({ data }) => { if (active) setOperation(data); }) .catch(() => { if (active) setOperation(null); }), ]).finally(() => { if (active) setLoading(false); }); return () => { active = false; owner.current.mounted = false; owner.current.generation += 1; readSequence.current += 1; readPending.current = false; }; }, [jobId, loadPlan]); useEffect(() => { if (!operation || terminal(operation.status)) return; let active = true; const timer = window.setTimeout(() => { api.get(`/operations/${operation.id}`) .then(({ data }) => { if (active) setOperation(data); }) .catch(() => undefined); }, 1000); return () => { active = false; window.clearTimeout(timer); }; }, [operation]); useEffect(() => { if (!operation || !terminal(operation.status)) return; const key = `${operation.id}:${operation.status}`; if (announced.current === key) return; announced.current = key; if (operation.status === "succeeded") { void loadPlan(); toast(t("workspaceStrategyCompleted"), "success"); } else if (operation.status === "failed") toast(t("workspaceStrategyFailed"), "error"); else toast(t("workspaceStrategyCancelled"), "info"); }, [loadPlan, operation, t, toast]); const generate = async () => { setLoading(true); try { const { data } = await api.post(`/jobapplications/${jobId}/focus-plan/operations`, { attachmentIds: null }); announced.current = null; setOperation(data.operation); toast(data.created ? t("workspaceStrategyQueued") : t("workspaceStrategyAlreadyQueued"), "info"); } catch (error) { toast(getApiErrorMessage(error, t("workspaceStrategyQueueFailed")), "error"); } finally { setLoading(false); } }; const mutateOperation = async (action: "cancel" | "retry") => { if (!operation) return; try { announced.current = null; const { data } = await api.post(`/operations/${operation.id}/${action}`); setOperation(data); } catch (error) { toast(getApiErrorMessage(error, action === "cancel" ? t("workspaceStrategyCancelFailed") : t("workspaceStrategyRetryFailed")), "error"); } }; const working = !!operation && !terminal(operation.status); const reading = loading || readingPlan; const readErrorTitle = t("workspaceStrategyLoadFailed"); const readErrorDetail = readError ? getApiErrorMessage(readError.cause, readErrorTitle) : ""; return ( {t("workspaceStrategyTitle")} {t("workspaceStrategySubtitle")} {readError ? { headingRef.current?.focus(); void loadPlan(); }}>{t("retry")} : undefined}> {readErrorTitle} {readErrorDetail !== readErrorTitle ? {readErrorDetail} : null} : null} {operation && operation.status !== "succeeded" ? ( {operation.canCancel ? : null} {operation.canRetry ? : null} }> {operation.progressStage || operation.status.replaceAll("_", " ")}{operation.progressPercent != null ? ` ยท ${operation.progressPercent}%` : ""} ) : null} {reading && !plan ? : plan ? ( ) : !readError ? {t("workspaceStrategyEmpty")} : null} ); }