import { useEffect, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { DECISION_MODELS, type DecisionConnectionChoice, type DecisionModelSettings, type DecisionProvider, type DecisionResult, type UpdateDecisionModel } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { decisionModelsApi } from "@/api/decision-models"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { ToggleField } from "@/components/agent-config-primitives"; import { ConnectionChoiceList } from "@/features/connections/ConnectionChoiceList"; import { ConnectionSetupFlow } from "@/features/connections/ConnectionSetupFlow"; import { AppLogo } from "@/pages/apps/AppLogo"; const failureMessages: Record = { not_configured: "Choose and save a connection first.", disabled: "Enable and save decisions before running a test.", connection_unavailable: "The connection is unavailable. Reconnect it and try again.", access_denied: "This connection is not shared with you. Review its permissions.", incompatible_connection: "Choose a supported OpenAI or OpenRouter API connection.", budget_blocked: "The company or task budget prevents this request. Review Costs and Budgets.", provider_auth_failed: "The provider rejected the connection. Reconnect it and try again.", provider_rate_limited: "The provider is rate limited. Try again later.", timeout: "The provider did not respond in time. Billing is unresolved; review Costs.", refused: "The model declined the test. Review the connection and try again.", }; export function DecisionModelSettingsView({ settings, choices, saving, testing, error, result, newlyConnectedId, onSave, onTest, onAdd }: { settings: DecisionModelSettings; choices: DecisionConnectionChoice[]; saving?: boolean; testing?: boolean; error?: string | null; result?: DecisionResult | null; newlyConnectedId?: string | null; onSave: (value: UpdateDecisionModel) => void; onTest: () => void; onAdd: () => void; }) { const [draft, setDraft] = useState>(settings); const appliedConnection = useRef(null); useEffect(() => { setDraft(settings); }, [settings.companyId, settings.connectionId, settings.grantId, settings.enabled, settings.allowBackground]); useEffect(() => { if (!newlyConnectedId) { appliedConnection.current = null; return; } if (appliedConnection.current === newlyConnectedId) return; const connection = choices.find(row => row.id === newlyConnectedId); if (connection) { appliedConnection.current = newlyConnectedId; setDraft(value => ({ ...value, connectionId: connection.id, grantId: connection.grantId, enabled: value.connectionId ? value.enabled : true })); } }, [newlyConnectedId, choices]); const selected = choices.find(row => row.grantId === draft.grantId); const dirty = draft.enabled !== settings.enabled || draft.allowBackground !== settings.allowBackground || draft.grantId !== settings.grantId; const unavailable = result && result.status !== "succeeded" ? failureMessages[result.status === "unavailable" ? result.reason : result.errorCode] ?? "The decision could not be completed. Review its entry in Costs." : null; return

Decision model

View usage

Choose the shared connection Paperclip uses for optional decision features. API usage is charged to this connection.

({ id: row.grantId, name: row.name, description: `${row.decisionModel} · ${row.status === "connected" ? "Connected" : "Needs attention"}`, icon: , }))} onSelect={grantId => { const row = choices.find(choice => choice.grantId === grantId)!; setDraft(value => ({ ...value, connectionId: row.id, grantId, enabled: value.connectionId ? value.enabled : true })); }} /> {choices.length === 0 &&

Add a shared OpenAI or OpenRouter API connection to get started.

} {draft.connectionId && !selected &&

The saved connection is unavailable to you. Disable decisions or choose another connection.

}
{selected && {selected.status === "connected" ? "Manage connection" : "Reconnect connection"}}
{draft.connectionId &&
setDraft(value => ({ ...value, enabled }))} />
setDraft(value => ({ ...value, allowBackground }))} />

Background features can charge this connection without a responsible person. These requests appear as Paperclip services.

} {error &&

{error}

} {!dirty && unavailable &&

{unavailable}

} {!dirty && result?.status === "succeeded" &&

Decision model is working

{Object.entries(result.answers).map(([name, answer]) =>
{name === "billing" ? "Billing problem" : name === "team" ? "Assigned team" : "Disruption score"}
{answer.type === "boolean" ? `${Math.round(answer.probability * 100)}%` : answer.type === "choice" ? answer.choice : `${answer.score.toFixed(2)} / 2`}
)}

Usage was recorded. Test answers are not saved.

}

{dirty ? "Save changes before testing." : "Runs a small sample with an API charge."}

{dirty && }
; } export function DecisionModelSettingsSection({ companyId }: { companyId: string }) { const client = useQueryClient(); const key = ["decision-model", companyId]; const query = useQuery({ queryKey: key, queryFn: () => decisionModelsApi.settings(companyId) }); const [adding, setAdding] = useState(false); const [provider, setProvider] = useState(null); const [newlyConnectedId, setNewlyConnectedId] = useState(null); const settingsRevision = JSON.stringify(query.data?.settings ?? { companyId }); const test = useMutation({ mutationFn: (_revision: string) => decisionModelsApi.test(companyId), onSettled: () => { void client.invalidateQueries({ queryKey: ["decision-history", companyId] }); void client.invalidateQueries({ predicate: q => String(q.queryKey[0]).includes("cost") }); } }); const resetTest = test.reset; useEffect(() => { resetTest(); }, [settingsRevision, resetTest]); const save = useMutation({ mutationFn: (settings: UpdateDecisionModel) => decisionModelsApi.update(companyId, settings), onSuccess: settings => { setNewlyConnectedId(null); test.reset(); client.setQueryData(key, { ...query.data, settings }); } }); if (query.isPending) return

Loading decision model…

; if (query.error) return

Could not load decision settings.

; if (!query.data?.canManage || !query.data.settings) return null; return <> save.mutate(value)} onTest={() => test.mutate(settingsRevision)} onAdd={() => { setProvider(null); setAdding(true); }} /> Add a decision connectionUse a shared API connection for company decisions. {provider ? setAdding(false)} onComplete={async result => { const refreshed = await query.refetch(); if ("connectionId" in result && result.connectionId && refreshed.data?.choices.some(row => row.id === result.connectionId)) setNewlyConnectedId(result.connectionId); setAdding(false); }} /> : ({ id, name: id === "openai" ? "OpenAI" : "OpenRouter", description: DECISION_MODELS[id].name, icon: , }))} onSelect={id => setProvider(id as DecisionProvider)} />} ; }