| 1 | import { ErrorMessage } from "./ErrorMessage"; |
| 2 | import { useEffect, useState } from "react"; |
| 3 | import { app } from "../lib/bridge"; |
| 4 | import { useT } from "../lib/i18n"; |
| 5 | import { appliedOnce, type ModelApplicationChoice, type ModelApplicationDetails } from "../lib/modelApplication"; |
| 6 | import { useModelApplicationStatus } from "../lib/useModelApplicationStatus"; |
| 7 | |
| 8 | type RemoteApplication = { key: string; text: string; details: ModelApplicationDetails }; |
| 9 | export function ComposerModelApplicationRecovery({ tabId, local, remote, setRemote, draftKey, text, running, onUseApplied }: { |
| 10 | tabId?: string; |
| 11 | local: { application?: ModelApplicationDetails; setApplication(value: ModelApplicationDetails | undefined): void; blocked: boolean }; |
| 12 | remote?: RemoteApplication; |
| 13 | setRemote(value: RemoteApplication | undefined): void; |
| 14 | draftKey: string; text: string; running: boolean; |
| 15 | onUseApplied(choice: ModelApplicationChoice): void; |
| 16 | }) { |
| 17 | const currentRemote = remote?.key === draftKey && remote.text === text ? remote : undefined; |
| 18 | const details = local.application ?? currentRemote?.details; |
| 19 | if (!tabId || !details) return null; |
| 20 | const change = local.application ? local.setApplication : (value: ModelApplicationDetails | undefined) => setRemote(value && currentRemote ? { ...currentRemote, details: value } : undefined); |
| 21 | return <ModelApplicationRecovery key={details.runtimeIdentity + details.desiredRevision} tabId={tabId} details={details} onChange={change} blocked={local.blocked || running} onUseApplied={onUseApplied} />; |
| 22 | } |
| 23 | |
| 24 | export function ModelApplicationRecovery({ tabId, details, onChange, onUseApplied, blocked }: { |
| 25 | tabId: string; |
| 26 | details: ModelApplicationDetails; |
| 27 | onChange: (value: ModelApplicationDetails | undefined) => void; |
| 28 | onUseApplied: (choice: ModelApplicationChoice) => void; |
| 29 | blocked: boolean; |
| 30 | }) { |
| 31 | const status = useModelApplicationStatus(tabId); |
| 32 | useEffect(() => { |
| 33 | if (status?.application === "applied") onChange(undefined); |
| 34 | }, [status, onChange]); |
| 35 | const t = useT(); |
| 36 | const [busy, setBusy] = useState(false); |
| 37 | const [expanded, setExpanded] = useState(false); |
| 38 | const [selected, setSelected] = useState<string[]>([]); |
| 39 | const [error, setError] = useState(""); |
| 40 | const applying = busy || details.applying; |
| 41 | const allows = (action: string) => !details.availableActions || details.availableActions.includes(action); |
| 42 | const retry = async (stop = false) => { |
| 43 | setBusy(true); |
| 44 | setError(""); |
| 45 | try { |
| 46 | if (stop) await app.CancelModelApplicationBlockers?.(tabId, appliedOnce(details), selected); |
| 47 | const result = await app.RetryModelSettingsApplication(tabId); |
| 48 | const target = result.targets.find(target => target.tabId === tabId); |
| 49 | if (target?.application === "applied") onChange(undefined); |
| 50 | else if (target?.details) onChange(target.details); |
| 51 | else setError(t("modelApply.failed")); |
| 52 | } catch { |
| 53 | setError(t("modelApply.failed")); |
| 54 | } finally { |
| 55 | setBusy(false); |
| 56 | } |
| 57 | }; |
| 58 | return <div role="alert" className="model-application-recovery"> |
| 59 | <span>{t(details.code === "model_settings_pending" ? "modelApply.pending" : "modelApply.failed", { n: details.blockingJobs.length })}</span> |
| 60 | <span>{t("modelApply.current", { model: details.model })}</span> |
| 61 | {details.connectionTarget && <span>{details.connectionTarget}</span>} |
| 62 | {details.blockingJobs.length > 0 && <button type="button" onClick={() => setExpanded(!expanded)}>{t("modelApply.jobs")}</button>} |
| 63 | {allows("retry") && <button type="button" disabled={applying} onClick={() => void retry()}>{t("modelApply.retry")}</button>} |
| 64 | {details.canUseApplied && allows("applied_once") && app.StartTurnWithModelApplication && |
| 65 | <button type="button" disabled={blocked || applying} onClick={() => onUseApplied(appliedOnce(details))}>{t("modelApply.useCurrent")}</button>} |
| 66 | {expanded && <div className="model-application-recovery__jobs"> |
| 67 | {details.blockingJobs.map(job => <label key={job.id}> |
| 68 | <input type="checkbox" checked={selected.includes(job.id)} onChange={event => setSelected(event.target.checked ? [...selected, job.id] : selected.filter(id => id !== job.id))} /> |
| 69 | {job.label || job.id} |
| 70 | </label>)} |
| 71 | {app.CancelModelApplicationBlockers && allows("stop_selected") && |
| 72 | <button type="button" disabled={applying || selected.length === 0} onClick={() => void retry(true)}>{t("modelApply.stopSelected")}</button>} |
| 73 | </div>} |
| 74 | {!details.canUseApplied && details.continuationUnavailable && |
| 75 | <details><summary>{t("modelApply.details")}</summary>{details.continuationUnavailable}</details>} |
| 76 | {error && <span><ErrorMessage error={error} /></span>} |
| 77 | </div>; |
| 78 | } |
| 79 |