mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
feat(automations): implement model eligibility checks for automation creation
- Added model eligibility checks to ensure automations can only use billable models (premium or BYOK). - Introduced new API endpoint to report model eligibility status for search spaces. - Updated frontend components to display eligibility alerts and disable creation options when models are not billable. - Enhanced automation creation forms to reflect model eligibility, preventing users from submitting invalid configurations. - Implemented server-side logic to capture and preserve model preferences across automation edits, ensuring consistent behavior during execution.
This commit is contained in:
parent
5d90fbe99f
commit
409fec94c3
32 changed files with 1451 additions and 67 deletions
|
|
@ -1,6 +1,8 @@
|
|||
"use client";
|
||||
import { ShieldAlert } from "lucide-react";
|
||||
import { useAutomationModelEligibility } from "@/hooks/use-automation-model-eligibility";
|
||||
import { useAutomations } from "@/hooks/use-automations";
|
||||
import { AutomationModelGateAlert } from "./components/automation-model-gate-alert";
|
||||
import { AutomationsEmptyState } from "./components/automations-empty-state";
|
||||
import { AutomationsHeader } from "./components/automations-header";
|
||||
import { AutomationsTable } from "./components/automations-table";
|
||||
|
|
@ -22,6 +24,18 @@ interface AutomationsContentProps {
|
|||
export function AutomationsContent({ searchSpaceId }: AutomationsContentProps) {
|
||||
const { automations, total, loading, error } = useAutomations();
|
||||
const perms = useAutomationPermissions();
|
||||
// Gate creation on billable models (premium/BYOK). Only meaningful for
|
||||
// users who can create; the eligibility query loads in parallel.
|
||||
const { data: eligibility, isLoading: eligibilityLoading } = useAutomationModelEligibility(
|
||||
perms.canCreate ? searchSpaceId : undefined
|
||||
);
|
||||
const modelViolations = eligibility?.violations ?? [];
|
||||
// Disable create CTAs while loading (avoid a flash of enabled buttons) and
|
||||
// when the resolved models aren't billable.
|
||||
const createDisabled = perms.canCreate && (eligibilityLoading || modelViolations.length > 0);
|
||||
const disabledReason = eligibilityLoading
|
||||
? "Checking model eligibility…"
|
||||
: modelViolations[0]?.reason;
|
||||
|
||||
if (perms.loading) {
|
||||
// Permissions gate the entire page; defer everything until we know.
|
||||
|
|
@ -77,7 +91,11 @@ export function AutomationsContent({ searchSpaceId }: AutomationsContentProps) {
|
|||
canCreate={perms.canCreate}
|
||||
showCreateCta={false}
|
||||
/>
|
||||
<AutomationsEmptyState searchSpaceId={searchSpaceId} canCreate={perms.canCreate} />
|
||||
<AutomationsEmptyState
|
||||
searchSpaceId={searchSpaceId}
|
||||
canCreate={perms.canCreate}
|
||||
modelViolations={modelViolations}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -89,7 +107,12 @@ export function AutomationsContent({ searchSpaceId }: AutomationsContentProps) {
|
|||
total={total}
|
||||
loading={loading}
|
||||
canCreate={perms.canCreate}
|
||||
createDisabled={createDisabled}
|
||||
disabledReason={disabledReason}
|
||||
/>
|
||||
{modelViolations.length > 0 && (
|
||||
<AutomationModelGateAlert searchSpaceId={searchSpaceId} violations={modelViolations} />
|
||||
)}
|
||||
<AutomationsTable
|
||||
automations={automations}
|
||||
searchSpaceId={searchSpaceId}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
"use client";
|
||||
import { Lock } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import type { ModelEligibilityViolation } from "@/contracts/types/automation.types";
|
||||
|
||||
interface AutomationModelGateAlertProps {
|
||||
searchSpaceId: number;
|
||||
violations: ModelEligibilityViolation[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Model selection for automations is managed under the roles tab, so every
|
||||
// blocked slot deep-links there (the label still names which slot to fix).
|
||||
const SETTINGS_TAB = "roles";
|
||||
const KIND_LABEL: Record<ModelEligibilityViolation["kind"], string> = {
|
||||
llm: "Agent model",
|
||||
image: "Image model",
|
||||
vision: "Vision model",
|
||||
};
|
||||
|
||||
/**
|
||||
* Warns that the search space's models aren't billable for automations.
|
||||
*
|
||||
* Automations may only use premium global models or your own (BYOK) models —
|
||||
* free models and Auto mode are blocked so every run is metered in premium
|
||||
* credits. Surfaced wherever a user can start creating an automation.
|
||||
*/
|
||||
export function AutomationModelGateAlert({
|
||||
searchSpaceId,
|
||||
violations,
|
||||
className,
|
||||
}: AutomationModelGateAlertProps) {
|
||||
if (violations.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Alert variant="warning" className={className}>
|
||||
<Lock aria-hidden />
|
||||
<AlertTitle>Automations need a premium or your own model</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
Automations run unattended, so every run must use a premium model or your own (BYOK)
|
||||
model. Update these in your model settings, then create your automation.
|
||||
</p>
|
||||
<ul className="mt-1 list-inside list-disc">
|
||||
{violations.map((violation) => (
|
||||
<li key={violation.kind}>
|
||||
<Link
|
||||
href={`/dashboard/${searchSpaceId}/search-space-settings/${SETTINGS_TAB}`}
|
||||
className="font-medium text-foreground underline underline-offset-2 hover:no-underline"
|
||||
>
|
||||
{KIND_LABEL[violation.kind]}
|
||||
</Link>
|
||||
<span className="text-muted-foreground"> — {violation.reason}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,10 +2,14 @@
|
|||
import { MessageSquarePlus, SquarePen, Workflow } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { ModelEligibilityViolation } from "@/contracts/types/automation.types";
|
||||
import { AutomationModelGateAlert } from "./automation-model-gate-alert";
|
||||
|
||||
interface AutomationsEmptyStateProps {
|
||||
searchSpaceId: number;
|
||||
canCreate: boolean;
|
||||
/** Model slots that block creation (free/Auto). Empty when eligible. */
|
||||
modelViolations?: ModelEligibilityViolation[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -14,7 +18,13 @@ interface AutomationsEmptyStateProps {
|
|||
* "new automation" form. We surface the chat path explicitly so users
|
||||
* don't go hunting for an "add" button that doesn't exist.
|
||||
*/
|
||||
export function AutomationsEmptyState({ searchSpaceId, canCreate }: AutomationsEmptyStateProps) {
|
||||
export function AutomationsEmptyState({
|
||||
searchSpaceId,
|
||||
canCreate,
|
||||
modelViolations = [],
|
||||
}: AutomationsEmptyStateProps) {
|
||||
const modelsBlocked = modelViolations.length > 0;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-border/60 bg-muted/20 px-6 py-12 text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
|
|
@ -26,20 +36,26 @@ export function AutomationsEmptyState({ searchSpaceId, canCreate }: AutomationsE
|
|||
SurfSense drafts the automation for your approval.
|
||||
</p>
|
||||
{canCreate ? (
|
||||
<div className="mt-6 flex items-center justify-center gap-2 flex-wrap">
|
||||
<Button asChild>
|
||||
<Link href={`/dashboard/${searchSpaceId}/new-chat`}>
|
||||
<MessageSquarePlus className="mr-2 h-4 w-4" />
|
||||
Create via chat
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/dashboard/${searchSpaceId}/automations/new`}>
|
||||
<SquarePen className="mr-2 h-4 w-4" />
|
||||
Create manually
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
modelsBlocked ? (
|
||||
<div className="mt-6 mx-auto max-w-md text-left">
|
||||
<AutomationModelGateAlert searchSpaceId={searchSpaceId} violations={modelViolations} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 flex items-center justify-center gap-2 flex-wrap">
|
||||
<Button asChild>
|
||||
<Link href={`/dashboard/${searchSpaceId}/new-chat`}>
|
||||
<MessageSquarePlus className="mr-2 h-4 w-4" />
|
||||
Create via chat
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/dashboard/${searchSpaceId}/automations/new`}>
|
||||
<SquarePen className="mr-2 h-4 w-4" />
|
||||
Create manually
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<p className="mt-6 text-xs text-muted-foreground">
|
||||
You don't have permission to create automations in this search space.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"use client";
|
||||
import { MessageSquarePlus, SquarePen } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
interface AutomationsHeaderProps {
|
||||
searchSpaceId: number;
|
||||
|
|
@ -14,8 +16,18 @@ interface AutomationsHeaderProps {
|
|||
* there to avoid a duplicate button.
|
||||
*/
|
||||
showCreateCta?: boolean;
|
||||
/**
|
||||
* Disable the create CTAs when the search space's models aren't billable
|
||||
* for automations (free/Auto). When set, a tooltip explains why and the
|
||||
* buttons render disabled rather than as links.
|
||||
*/
|
||||
createDisabled?: boolean;
|
||||
disabledReason?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_DISABLED_REASON =
|
||||
"Automations need a premium or your own (BYOK) model. Update your model settings to enable.";
|
||||
|
||||
/**
|
||||
* Page header: title + count + "Create via chat" CTA. Creation is intent-driven
|
||||
* (the create_automation tool runs inside chat with a HITL approval card), so
|
||||
|
|
@ -27,6 +39,8 @@ export function AutomationsHeader({
|
|||
loading,
|
||||
canCreate,
|
||||
showCreateCta = true,
|
||||
createDisabled = false,
|
||||
disabledReason,
|
||||
}: AutomationsHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
|
|
@ -40,20 +54,70 @@ export function AutomationsHeader({
|
|||
</div>
|
||||
{canCreate && showCreateCta && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link href={`/dashboard/${searchSpaceId}/automations/new`}>
|
||||
<SquarePen className="mr-2 h-4 w-4" />
|
||||
Create manually
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm">
|
||||
<Link href={`/dashboard/${searchSpaceId}/new-chat`}>
|
||||
<MessageSquarePlus className="mr-2 h-4 w-4" />
|
||||
Create via chat
|
||||
</Link>
|
||||
</Button>
|
||||
{createDisabled ? (
|
||||
<>
|
||||
<DisabledCta
|
||||
variant="outline"
|
||||
icon={<SquarePen className="mr-2 h-4 w-4" />}
|
||||
label="Create manually"
|
||||
reason={disabledReason ?? DEFAULT_DISABLED_REASON}
|
||||
/>
|
||||
<DisabledCta
|
||||
icon={<MessageSquarePlus className="mr-2 h-4 w-4" />}
|
||||
label="Create via chat"
|
||||
reason={disabledReason ?? DEFAULT_DISABLED_REASON}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link href={`/dashboard/${searchSpaceId}/automations/new`}>
|
||||
<SquarePen className="mr-2 h-4 w-4" />
|
||||
Create manually
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm">
|
||||
<Link href={`/dashboard/${searchSpaceId}/new-chat`}>
|
||||
<MessageSquarePlus className="mr-2 h-4 w-4" />
|
||||
Create via chat
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DisabledCta({
|
||||
icon,
|
||||
label,
|
||||
reason,
|
||||
variant,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
reason: string;
|
||||
variant?: "outline";
|
||||
}) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{/* aria-disabled (not `disabled`) keeps the button focusable so the
|
||||
tooltip is reachable by hover and keyboard; onClick is a no-op. */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant={variant}
|
||||
aria-disabled
|
||||
className="cursor-not-allowed opacity-50"
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">{reason}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import {
|
||||
type Automation,
|
||||
automationCreateRequest,
|
||||
|
|
@ -45,6 +46,12 @@ interface AutomationBuilderFormProps {
|
|||
searchSpaceId: number;
|
||||
/** Required in edit mode; seeds the form and trigger reconciliation. */
|
||||
automation?: Automation;
|
||||
/**
|
||||
* When set (create mode only), the search space's models aren't billable
|
||||
* for automations. Submit is disabled with this reason as the tooltip; the
|
||||
* orchestrator also renders the full gate alert above the form.
|
||||
*/
|
||||
submitDisabledReason?: string;
|
||||
}
|
||||
|
||||
type Mode = "form" | "json";
|
||||
|
|
@ -66,6 +73,7 @@ export function AutomationBuilderForm({
|
|||
mode,
|
||||
searchSpaceId,
|
||||
automation,
|
||||
submitDisabledReason,
|
||||
}: AutomationBuilderFormProps) {
|
||||
const router = useRouter();
|
||||
const { mutateAsync: createAutomation } = useAtomValue(createAutomationMutationAtom);
|
||||
|
|
@ -273,6 +281,8 @@ export function AutomationBuilderForm({
|
|||
}
|
||||
|
||||
const submitLabel = mode === "edit" ? "Save changes" : "Create automation";
|
||||
// Only gate creation; editing an existing automation isn't blocked here.
|
||||
const submitBlocked = mode === "create" && !!submitDisabledReason;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -390,15 +400,39 @@ export function AutomationBuilderForm({
|
|||
<Button asChild type="button" variant="ghost" size="sm">
|
||||
<Link href={cancelHref}>Cancel</Link>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={submitting}
|
||||
onClick={() => (activeMode === "json" ? submitJson() : submitForm())}
|
||||
>
|
||||
{submitting ? <Spinner size="xs" className="mr-2" /> : <Save className="mr-2 h-4 w-4" />}
|
||||
{submitLabel}
|
||||
</Button>
|
||||
{submitBlocked ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{/* aria-disabled keeps the button focusable so the tooltip is
|
||||
reachable by hover and keyboard; onClick is a no-op. */}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
aria-disabled
|
||||
className="cursor-not-allowed opacity-50"
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">{submitDisabledReason}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={submitting}
|
||||
onClick={() => (activeMode === "json" ? submitJson() : submitForm())}
|
||||
>
|
||||
{submitting ? (
|
||||
<Spinner size="xs" className="mr-2" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{submitLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
import { ShieldAlert } from "lucide-react";
|
||||
import { useAutomationModelEligibility } from "@/hooks/use-automation-model-eligibility";
|
||||
import { AutomationModelGateAlert } from "../components/automation-model-gate-alert";
|
||||
import { AutomationBuilderForm } from "../components/builder/automation-builder-form";
|
||||
import { useAutomationPermissions } from "../hooks/use-automation-permissions";
|
||||
import { AutomationNewHeader } from "./components/automation-new-header";
|
||||
|
|
@ -16,6 +18,9 @@ interface AutomationNewContentProps {
|
|||
*/
|
||||
export function AutomationNewContent({ searchSpaceId }: AutomationNewContentProps) {
|
||||
const perms = useAutomationPermissions();
|
||||
const { data: eligibility, isLoading: eligibilityLoading } = useAutomationModelEligibility(
|
||||
perms.canCreate ? searchSpaceId : undefined
|
||||
);
|
||||
|
||||
if (perms.loading) {
|
||||
return <div className="h-32 rounded-md border border-border/60 bg-muted/10 animate-pulse" />;
|
||||
|
|
@ -33,10 +38,22 @@ export function AutomationNewContent({ searchSpaceId }: AutomationNewContentProp
|
|||
);
|
||||
}
|
||||
|
||||
const modelViolations = eligibility?.violations ?? [];
|
||||
const submitDisabledReason = eligibilityLoading
|
||||
? "Checking model eligibility…"
|
||||
: modelViolations[0]?.reason;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutomationNewHeader searchSpaceId={searchSpaceId} />
|
||||
<AutomationBuilderForm mode="create" searchSpaceId={searchSpaceId} />
|
||||
{modelViolations.length > 0 && (
|
||||
<AutomationModelGateAlert searchSpaceId={searchSpaceId} violations={modelViolations} />
|
||||
)}
|
||||
<AutomationBuilderForm
|
||||
mode="create"
|
||||
searchSpaceId={searchSpaceId}
|
||||
submitDisabledReason={submitDisabledReason}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,12 @@ export const updateLLMPreferencesMutationAtom = atomWithMutation((get) => {
|
|||
cacheKeys.newLLMConfigs.preferences(Number(searchSpaceId)),
|
||||
(old: Record<string, unknown> | undefined) => ({ ...old, ...request.data })
|
||||
);
|
||||
// Automation eligibility is derived from these model preferences
|
||||
// (agent/image/vision). Invalidate it so the automations gate alert
|
||||
// reflects the new selection without a manual refresh.
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.automations.modelEligibility(Number(searchSpaceId)),
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to update LLM preferences");
|
||||
|
|
|
|||
|
|
@ -60,6 +60,15 @@ export const inputs = z.object({
|
|||
});
|
||||
export type Inputs = z.infer<typeof inputs>;
|
||||
|
||||
// Captured model snapshot (server-managed). Set at create time and preserved
|
||||
// across edits so runs are insulated from later chat/search-space model changes.
|
||||
export const automationModels = z.object({
|
||||
agent_llm_id: z.number().int().default(0),
|
||||
image_generation_config_id: z.number().int().default(0),
|
||||
vision_llm_config_id: z.number().int().default(0),
|
||||
});
|
||||
export type AutomationModels = z.infer<typeof automationModels>;
|
||||
|
||||
export const automationDefinition = z.object({
|
||||
schema_version: z.string().default("1.0"),
|
||||
name: z.string().min(1).max(200),
|
||||
|
|
@ -69,6 +78,7 @@ export const automationDefinition = z.object({
|
|||
plan: z.array(planStep).min(1),
|
||||
execution: execution.default(execution.parse({})),
|
||||
metadata: metadata.default(metadata.parse({})),
|
||||
models: automationModels.nullable().optional(),
|
||||
});
|
||||
export type AutomationDefinition = z.infer<typeof automationDefinition>;
|
||||
|
||||
|
|
@ -191,3 +201,23 @@ export const runListParams = z.object({
|
|||
offset: z.number().int().min(0).default(0),
|
||||
});
|
||||
export type RunListParams = z.infer<typeof runListParams>;
|
||||
|
||||
// =============================================================================
|
||||
// Model eligibility — mirror app/automations/api/automation.py (ModelEligibility)
|
||||
// =============================================================================
|
||||
|
||||
export const modelEligibilityKind = z.enum(["llm", "image", "vision"]);
|
||||
export type ModelEligibilityKind = z.infer<typeof modelEligibilityKind>;
|
||||
|
||||
export const modelEligibilityViolation = z.object({
|
||||
kind: modelEligibilityKind,
|
||||
config_id: z.number().nullable(),
|
||||
reason: z.string(),
|
||||
});
|
||||
export type ModelEligibilityViolation = z.infer<typeof modelEligibilityViolation>;
|
||||
|
||||
export const modelEligibility = z.object({
|
||||
allowed: z.boolean(),
|
||||
violations: z.array(modelEligibilityViolation),
|
||||
});
|
||||
export type ModelEligibility = z.infer<typeof modelEligibility>;
|
||||
|
|
|
|||
25
surfsense_web/hooks/use-automation-model-eligibility.ts
Normal file
25
surfsense_web/hooks/use-automation-model-eligibility.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ModelEligibility } from "@/contracts/types/automation.types";
|
||||
import { automationsApiService } from "@/lib/apis/automations-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
/**
|
||||
* Whether the search space's configured models are billable for automations.
|
||||
*
|
||||
* Automations may only run on premium global models or user-provided (BYOK)
|
||||
* models; free global models and Auto mode are blocked so every run is metered
|
||||
* in premium credits. Creation surfaces use this to gate their CTAs before the
|
||||
* user invests effort drafting an automation that can't be saved.
|
||||
*
|
||||
* Keyed by search space id (not the jotai "current scope" atom) so it can be
|
||||
* used on the create route as well as the list page.
|
||||
*/
|
||||
export function useAutomationModelEligibility(searchSpaceId: number | undefined) {
|
||||
return useQuery<ModelEligibility, Error>({
|
||||
queryKey: cacheKeys.automations.modelEligibility(searchSpaceId ?? 0),
|
||||
queryFn: () => automationsApiService.getModelEligibility(searchSpaceId as number),
|
||||
enabled: !!searchSpaceId,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import {
|
|||
automationCreateRequest,
|
||||
automationListResponse,
|
||||
automationUpdateRequest,
|
||||
modelEligibility,
|
||||
type RunListParams,
|
||||
run,
|
||||
runListResponse,
|
||||
|
|
@ -62,6 +63,13 @@ class AutomationsApiService {
|
|||
return baseApiService.delete(`${BASE}/${automationId}`);
|
||||
};
|
||||
|
||||
// Whether the search space's models are billable for automations (premium
|
||||
// global or BYOK). Used to gate creation surfaces before submit.
|
||||
getModelEligibility = async (searchSpaceId: number) => {
|
||||
const qs = new URLSearchParams({ search_space_id: String(searchSpaceId) });
|
||||
return baseApiService.get(`${BASE}/model-eligibility?${qs.toString()}`, modelEligibility);
|
||||
};
|
||||
|
||||
// ---- Triggers (sub-resource) --------------------------------------------
|
||||
|
||||
addTrigger = async (automationId: number, request: TriggerCreateRequest) => {
|
||||
|
|
|
|||
|
|
@ -134,5 +134,7 @@ export const cacheKeys = {
|
|||
["automations", "runs", automationId, limit, offset] as const,
|
||||
run: (automationId: number, runId: number) =>
|
||||
["automations", "runs", automationId, runId] as const,
|
||||
modelEligibility: (searchSpaceId: number) =>
|
||||
["automations", "model-eligibility", searchSpaceId] as const,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue