Improve admin LLM registry UX and error handling

Adds user feedback and error handling to LLM registry modals (add/edit creator, model, provider) in the admin UI, including loading states and error messages. Ensures atomic updates for model costs in the backend using transactions. Improves display of creator website URLs and handles the case where no LLM models are available in analytics config. Updates icon usage and removes unnecessary 'use server' directive.
This commit is contained in:
Bentlybro
2026-01-07 14:17:37 +00:00
parent 23e37fd163
commit 6dc767aafa
10 changed files with 187 additions and 71 deletions

View File

@@ -201,6 +201,21 @@ async def get_execution_analytics_config(
# Sort models by provider and name for better UX
available_models.sort(key=lambda x: (x.provider, x.label))
# Handle case where no models are available
if not available_models:
logger.warning(
"No enabled LLM models found in registry. "
"Ensure models are configured and enabled in the LLM Registry."
)
# Provide a placeholder entry so admins see meaningful feedback
available_models.append(
ModelInfo(
value="",
label="No models available - configure in LLM Registry",
provider="none",
)
)
return ExecutionAnalyticsConfig(
available_models=available_models,
default_system_prompt=DEFAULT_SYSTEM_PROMPT,

View File

@@ -27,12 +27,7 @@ async def initialize_registry_for_executor() -> None:
await db.connect()
logger.info("[GraphExecutor] Connected to database for registry refresh")
# Refresh LLM registry before initializing blocks
await llm_registry.refresh_llm_registry()
refresh_llm_costs()
logger.info("[GraphExecutor] LLM registry refreshed")
# Initialize blocks (also refreshes registry, but we do it explicitly above)
# Initialize blocks (internally refreshes LLM registry and costs)
await initialize_blocks()
logger.info("[GraphExecutor] Blocks initialized")
except Exception as exc:

View File

@@ -252,24 +252,22 @@ async def update_model(
# If we have costs to update, we need to handle them separately
# because nested writes have different constraints
if request.costs is not None:
# First update scalar fields
if scalar_data:
await prisma.models.LlmModel.prisma().update(
where={"id": model_id},
data=scalar_data,
)
# Then handle costs: delete existing and create new
await prisma.models.LlmModelCost.prisma().delete_many(
where={"llmModelId": model_id}
)
if request.costs:
cost_payload = _cost_create_payload(request.costs)
for cost_item in cost_payload["create"]:
cost_item["llmModelId"] = model_id
await prisma.models.LlmModelCost.prisma().create(
data=cast(Any, cost_item)
# Wrap cost replacement in a transaction for atomicity
async with transaction() as tx:
# First update scalar fields
if scalar_data:
await tx.llmmodel.update(
where={"id": model_id},
data=scalar_data,
)
# Fetch the updated record
# Then handle costs: delete existing and create new
await tx.llmmodelcost.delete_many(where={"llmModelId": model_id})
if request.costs:
cost_payload = _cost_create_payload(request.costs)
for cost_item in cost_payload["create"]:
cost_item["llmModelId"] = model_id
await tx.llmmodelcost.create(data=cast(Any, cost_item))
# Fetch the updated record (outside transaction)
record = await prisma.models.LlmModel.prisma().find_unique(
where={"id": model_id},
include={"Costs": True, "Creator": True},

View File

@@ -1,5 +1,8 @@
"use client";
import { Sidebar } from "@/components/__legacy__/Sidebar";
import { Users, DollarSign, UserSearch, FileText, Cpu } from "lucide-react";
import { Users, DollarSign, UserSearch, FileText } from "lucide-react";
import { Cpu } from "@phosphor-icons/react";
import { IconSliders } from "@/components/__legacy__/ui/icons";
@@ -29,7 +32,7 @@ const sidebarLinkGroups = [
{
text: "LLM Registry",
href: "/admin/llms",
icon: <Cpu className="h-6 w-6" />,
icon: <Cpu size={24} />,
},
{
text: "Admin User Management",

View File

@@ -8,8 +8,24 @@ import { useRouter } from "next/navigation";
export function AddCreatorModal() {
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setError(null);
try {
await createLlmCreatorAction(formData);
setOpen(false);
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create creator");
} finally {
setIsSubmitting(false);
}
}
return (
<Dialog
title="Add Creator"
@@ -27,14 +43,7 @@ export function AddCreatorModal() {
model).
</div>
<form
action={async (formData) => {
await createLlmCreatorAction(formData);
setOpen(false);
router.refresh();
}}
className="space-y-4"
>
<form action={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<label
@@ -103,17 +112,32 @@ export function AddCreatorModal() {
/>
</div>
{error && (
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<Dialog.Footer>
<Button
variant="ghost"
size="small"
onClick={() => setOpen(false)}
type="button"
onClick={() => {
setOpen(false);
setError(null);
}}
disabled={isSubmitting}
>
Cancel
</Button>
<Button variant="primary" size="small" type="submit">
Add Creator
<Button
variant="primary"
size="small"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? "Creating..." : "Add Creator"}
</Button>
</Dialog.Footer>
</form>

View File

@@ -16,8 +16,24 @@ interface Props {
export function AddModelModal({ providers, creators }: Props) {
const [open, setOpen] = useState(false);
const [selectedCreatorId, setSelectedCreatorId] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setError(null);
try {
await createLlmModelAction(formData);
setOpen(false);
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create model");
} finally {
setIsSubmitting(false);
}
}
// When provider changes, auto-select matching creator if one exists
function handleProviderChange(providerId: string) {
const provider = providers.find((p) => p.id === providerId);
@@ -49,14 +65,7 @@ export function AddModelModal({ providers, creators }: Props) {
Register a new model slug, metadata, and pricing.
</div>
<form
action={async (formData) => {
await createLlmModelAction(formData);
setOpen(false);
router.refresh();
}}
className="space-y-6"
>
<form action={handleSubmit} className="space-y-6">
{/* Basic Information */}
<div className="space-y-4">
<div className="space-y-1">
@@ -239,6 +248,7 @@ export function AddModelModal({ providers, creators }: Props) {
required
type="number"
name="credit_cost"
step="1"
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
placeholder="5"
min={0}
@@ -269,17 +279,32 @@ export function AddModelModal({ providers, creators }: Props) {
</label>
</div>
{error && (
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<Dialog.Footer>
<Button
variant="ghost"
size="small"
onClick={() => setOpen(false)}
type="button"
onClick={() => {
setOpen(false);
setError(null);
}}
disabled={isSubmitting}
>
Cancel
</Button>
<Button variant="primary" size="small" type="submit">
Save Model
<Button
variant="primary"
size="small"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? "Creating..." : "Save Model"}
</Button>
</Dialog.Footer>
</form>

View File

@@ -8,8 +8,26 @@ import { useRouter } from "next/navigation";
export function AddProviderModal() {
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setError(null);
try {
await createLlmProviderAction(formData);
setOpen(false);
router.refresh();
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to create provider",
);
} finally {
setIsSubmitting(false);
}
}
return (
<Dialog
title="Add Provider"
@@ -66,14 +84,7 @@ export function AddProviderModal() {
</div>
</div>
<form
action={async (formData) => {
await createLlmProviderAction(formData);
setOpen(false);
router.refresh();
}}
className="space-y-6"
>
<form action={handleSubmit} className="space-y-6">
{/* Basic Information */}
<div className="space-y-4">
<div className="space-y-1">
@@ -222,17 +233,32 @@ export function AddProviderModal() {
</div>
</div>
{error && (
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<Dialog.Footer>
<Button
variant="ghost"
size="small"
onClick={() => setOpen(false)}
type="button"
onClick={() => {
setOpen(false);
setError(null);
}}
disabled={isSubmitting}
>
Cancel
</Button>
<Button variant="primary" size="small" type="submit">
Save Provider
<Button
variant="primary"
size="small"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? "Creating..." : "Save Provider"}
</Button>
</Dialog.Footer>
</form>

View File

@@ -58,7 +58,13 @@ export function CreatorsTable({ creators }: { creators: LlmModelCreator[] }) {
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
>
{new URL(creator.website_url).hostname}
{(() => {
try {
return new URL(creator.website_url).hostname;
} catch {
return creator.website_url;
}
})()}
</a>
) : (
<span className="text-muted-foreground"></span>
@@ -80,8 +86,24 @@ export function CreatorsTable({ creators }: { creators: LlmModelCreator[] }) {
function EditCreatorModal({ creator }: { creator: LlmModelCreator }) {
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setError(null);
try {
await updateLlmCreatorAction(formData);
setOpen(false);
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to update creator");
} finally {
setIsSubmitting(false);
}
}
return (
<Dialog
title="Edit Creator"
@@ -94,14 +116,7 @@ function EditCreatorModal({ creator }: { creator: LlmModelCreator }) {
</Button>
</Dialog.Trigger>
<Dialog.Content>
<form
action={async (formData) => {
await updateLlmCreatorAction(formData);
setOpen(false);
router.refresh();
}}
className="space-y-4"
>
<form action={handleSubmit} className="space-y-4">
<input type="hidden" name="creator_id" value={creator.id} />
<div className="grid gap-4 sm:grid-cols-2">
@@ -145,17 +160,32 @@ function EditCreatorModal({ creator }: { creator: LlmModelCreator }) {
/>
</div>
{error && (
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<Dialog.Footer>
<Button
variant="ghost"
size="small"
onClick={() => setOpen(false)}
type="button"
onClick={() => {
setOpen(false);
setError(null);
}}
disabled={isSubmitting}
>
Cancel
</Button>
<Button variant="primary" size="small" type="submit">
Update
<Button
variant="primary"
size="small"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? "Updating..." : "Update"}
</Button>
</Dialog.Footer>
</form>

View File

@@ -147,6 +147,7 @@ export function DeleteModelModal({
<Button
variant="ghost"
size="small"
type="button"
onClick={() => {
setOpen(false);
setSelectedReplacement("");

View File

@@ -8,7 +8,6 @@ async function LlmRegistryPage() {
}
export default async function AdminLlmRegistryPage() {
"use server";
const withAdminAccess = await withRoleAccess(["admin"]);
const ProtectedLlmRegistryPage = await withAdminAccess(LlmRegistryPage);
return <ProtectedLlmRegistryPage />;