feat(ui): migrate budgets to shadcn

- budget_panel.tsx: @tremor/react Table / Card / TabGroup / TabList /
  TabPanel / Button / Text \u2192 shadcn Table / Card / Tabs / Button +
  plain Tailwind for text. Moved action buttons into a proper TableCell
  (antd Table accepted dangling children but shadcn's semantic
  <table> element does not).
- budget_modal.tsx: antd Modal / Form / InputNumber / Select +
  @tremor/react TextInput / Accordion \u2192 shadcn Dialog /
  react-hook-form / Input / Select / Accordion. Introduces a small
  LabeledRow helper for the label-on-left-value-on-right layout that
  antd's Form.Item with labelCol/wrapperCol used to provide.
- edit_budget_modal.tsx: same treatment; preserves the disabled
  Budget ID field and the Accordion-wrapped optional settings.

Gates: TS \u2713 | Lint \u2713 (pre-existing unused-imports errors in
budget_panel.test.tsx unchanged) | Vitest 6/6 \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 07:08:32 +00:00
parent b8d48fecca
commit 01cc55886e
No known key found for this signature in database
4 changed files with 499 additions and 252 deletions

View file

@ -36,3 +36,10 @@ Layer abbreviations: **TS** (tsc --noEmit), **Lint** (eslint), **Vitest**
(Table + @tanstack/react-table, Popover, Tooltip, Badge variants, Skeleton) covered
every antd / @tremor / @heroicons import encountered.
## 3. Budgets (experimental/budgets)
- Cycles used: 1 / 7
- Layer outcomes per cycle:
- cycle 1: TS ✓ | Lint ✓ (pre-existing unused-imports in test excluded) | Vitest ✓ (6/6) | Parity ⏭ | Snap ⏭
- Final status: **done**

View file

@ -1,95 +1,212 @@
import React from "react";
import { TextInput, Accordion, AccordionHeader, AccordionBody } from "@tremor/react";
import { Button as Button2, Modal, Form, InputNumber, Select } from "antd";
import { useCreateBudget } from "@/app/(dashboard)/hooks/budgets/useBudgets";
import NotificationsManager from "../molecules/notifications_manager";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Controller, FormProvider, useForm } from "react-hook-form";
interface BudgetModalProps {
isModalVisible: boolean;
setIsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
const BudgetModal: React.FC<BudgetModalProps> = ({ isModalVisible, setIsModalVisible }) => {
const [form] = Form.useForm();
const createBudget = useCreateBudget();
const handleOk = () => {
setIsModalVisible(false);
form.resetFields();
};
type BudgetFormValues = {
budget_id: string;
tpm_limit: number | null;
rpm_limit: number | null;
max_budget: number | null;
budget_duration: string | null;
};
const defaultValues: BudgetFormValues = {
budget_id: "",
tpm_limit: null,
rpm_limit: null,
max_budget: null,
budget_duration: null,
};
function LabeledRow({
id,
label,
help,
children,
required,
error,
}: {
id?: string;
label: string;
help?: string;
children: React.ReactNode;
required?: boolean;
error?: string;
}) {
return (
<div className="grid grid-cols-3 gap-3 items-start mb-4">
<Label htmlFor={id} className="pt-2">
{label}
{required && <span className="text-destructive">&nbsp;*</span>}
</Label>
<div className="col-span-2 space-y-1">
{children}
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
</div>
);
}
const BudgetModal: React.FC<BudgetModalProps> = ({
isModalVisible,
setIsModalVisible,
}) => {
const form = useForm<BudgetFormValues>({ defaultValues, mode: "onSubmit" });
const createBudget = useCreateBudget();
const handleCancel = () => {
setIsModalVisible(false);
form.resetFields();
form.reset(defaultValues);
};
const handleCreate = async (formValues: Record<string, any>) => {
const handleCreate = form.handleSubmit(async (values) => {
try {
NotificationsManager.info("Making API Call");
await createBudget.mutateAsync(formValues);
await createBudget.mutateAsync(values);
NotificationsManager.success("Budget Created");
form.resetFields();
form.reset(defaultValues);
setIsModalVisible(false);
} catch (error) {
console.error("Error creating the budget:", error);
NotificationsManager.fromBackend(`Error creating the budget: ${error}`);
}
};
});
return (
<Modal
title="Create Budget"
<Dialog
open={isModalVisible}
width={800}
footer={null}
onOk={handleOk}
onCancel={handleCancel}
onOpenChange={(o) => (!o ? handleCancel() : undefined)}
>
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
<>
<Form.Item
label="Budget ID"
name="budget_id"
rules={[
{
required: true,
message: "Please input a human-friendly name for the budget",
},
]}
help="A human-friendly name for the budget"
>
<TextInput placeholder="" />
</Form.Item>
<Form.Item label="Max Tokens per minute" name="tpm_limit" help="Default is model limit.">
<InputNumber step={1} precision={2} width={200} />
</Form.Item>
<Form.Item label="Max Requests per minute" name="rpm_limit" help="Default is model limit.">
<InputNumber step={1} precision={2} width={200} />
</Form.Item>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Create Budget</DialogTitle>
<DialogDescription className="sr-only">
Create a new spend / rate-limit budget.
</DialogDescription>
</DialogHeader>
<FormProvider {...form}>
<form onSubmit={handleCreate}>
<LabeledRow
id="budget_id"
label="Budget ID"
help="A human-friendly name for the budget"
required
error={form.formState.errors.budget_id?.message as string | undefined}
>
<Input
id="budget_id"
{...form.register("budget_id", {
required: "Please input a human-friendly name for the budget",
})}
/>
</LabeledRow>
<LabeledRow
id="tpm_limit"
label="Max Tokens per minute"
help="Default is model limit."
>
<Input
id="tpm_limit"
type="number"
step={1}
{...form.register("tpm_limit", { valueAsNumber: true })}
/>
</LabeledRow>
<LabeledRow
id="rpm_limit"
label="Max Requests per minute"
help="Default is model limit."
>
<Input
id="rpm_limit"
type="number"
step={1}
{...form.register("rpm_limit", { valueAsNumber: true })}
/>
</LabeledRow>
<Accordion className="mt-20 mb-8">
<AccordionHeader>
<b>Optional Settings</b>
</AccordionHeader>
<AccordionBody>
<Form.Item label="Max Budget (USD)" name="max_budget">
<InputNumber step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item className="mt-8" label="Reset Budget" name="budget_duration">
<Select defaultValue={null} placeholder="n/a">
<Select.Option value="24h">daily</Select.Option>
<Select.Option value="7d">weekly</Select.Option>
<Select.Option value="30d">monthly</Select.Option>
</Select>
</Form.Item>
</AccordionBody>
</Accordion>
</>
<Accordion type="single" collapsible className="mt-8 mb-4">
<AccordionItem value="optional">
<AccordionTrigger className="font-semibold">
Optional Settings
</AccordionTrigger>
<AccordionContent>
<LabeledRow id="max_budget" label="Max Budget (USD)">
<Input
id="max_budget"
type="number"
step={0.01}
{...form.register("max_budget", { valueAsNumber: true })}
/>
</LabeledRow>
<LabeledRow label="Reset Budget">
<Controller
control={form.control}
name="budget_duration"
render={({ field }) => (
<Select
value={field.value ?? ""}
onValueChange={(v) => field.onChange(v || null)}
>
<SelectTrigger>
<SelectValue placeholder="n/a" />
</SelectTrigger>
<SelectContent>
<SelectItem value="24h">daily</SelectItem>
<SelectItem value="7d">weekly</SelectItem>
<SelectItem value="30d">monthly</SelectItem>
</SelectContent>
</Select>
)}
/>
</LabeledRow>
</AccordionContent>
</AccordionItem>
</Accordion>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Create Budget</Button2>
</div>
</Form>
</Modal>
<DialogFooter className="mt-2">
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" disabled={createBudget.isPending}>
{createBudget.isPending ? "Creating…" : "Create Budget"}
</Button>
</DialogFooter>
</form>
</FormProvider>
</DialogContent>
</Dialog>
);
};

View file

@ -1,24 +1,19 @@
/**
* The parent pane, showing list of budgets
*
* The parent pane, showing list of budgets.
* Migrated to shadcn in phase 1. See docs/BLUEPRINT.md.
*/
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Button,
Card,
Tab,
TabGroup,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableHeader,
TableRow,
TabList,
TabPanel,
TabPanels,
Text,
} from "@tremor/react";
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import React, { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
@ -27,7 +22,11 @@ import NotificationsManager from "../molecules/notifications_manager";
import { useBudgets, useDeleteBudget } from "@/app/(dashboard)/hooks/budgets/useBudgets";
import BudgetModal from "./budget_modal";
import EditBudgetModal from "./edit_budget_modal";
import { CREATE_END_USER_CURL_COMMAND, CHAT_COMPLETIONS_CURL_COMMAND, OPENAI_SDK_PYTHON_CODE } from "./constants";
import {
CREATE_END_USER_CURL_COMMAND,
CHAT_COMPLETIONS_CURL_COMMAND,
OPENAI_SDK_PYTHON_CODE,
} from "./constants";
interface BudgetSettingsPageProps {
accessToken: string | null;
@ -50,10 +49,8 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
const { data: budgetList = [] } = useBudgets();
const deleteBudget = useDeleteBudget();
const handleEditCall = async (budget: budgetItem) => {
if (accessToken == null) {
return;
}
const handleEditCall = (budget: budgetItem) => {
if (accessToken == null) return;
setSelectedBudget(budget);
setIsEditModalVisible(true);
};
@ -64,9 +61,7 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
};
const handleDeleteConfirm = async () => {
if (!selectedBudget || accessToken == null) {
return;
}
if (!selectedBudget || accessToken == null) return;
try {
await deleteBudget.mutateAsync(selectedBudget.budget_id);
NotificationsManager.success("Budget deleted.");
@ -83,115 +78,132 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
}
};
const handleDeleteCancel = () => {
setIsDeleteModalVisible(false);
};
const handleDeleteCancel = () => setIsDeleteModalVisible(false);
return (
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
<Button size="sm" variant="primary" className="mb-2" onClick={() => setIsCreateModelVisible(true)}>
<Button size="sm" className="mb-2" onClick={() => setIsCreateModelVisible(true)}>
+ Create Budget
</Button>
<TabGroup>
<TabList>
<Tab>Budgets</Tab>
<Tab>Examples</Tab>
</TabList>
<TabPanels>
<TabPanel>
<div className="mt-6">
<BudgetModal
isModalVisible={isCreateModelVisible}
setIsModalVisible={setIsCreateModelVisible}
<Tabs defaultValue="budgets">
<TabsList>
<TabsTrigger value="budgets">Budgets</TabsTrigger>
<TabsTrigger value="examples">Examples</TabsTrigger>
</TabsList>
<TabsContent value="budgets">
<div className="mt-6">
<BudgetModal
isModalVisible={isCreateModelVisible}
setIsModalVisible={setIsCreateModelVisible}
/>
{selectedBudget && (
<EditBudgetModal
isModalVisible={isEditModalVisible}
setIsModalVisible={setIsEditModalVisible}
existingBudget={selectedBudget}
/>
{selectedBudget && (
<EditBudgetModal
isModalVisible={isEditModalVisible}
setIsModalVisible={setIsEditModalVisible}
existingBudget={selectedBudget}
/>
)}
<Card>
<Text>Create a budget to assign to customers.</Text>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Budget ID</TableHeaderCell>
<TableHeaderCell>Max Budget</TableHeaderCell>
<TableHeaderCell>TPM</TableHeaderCell>
<TableHeaderCell>RPM</TableHeaderCell>
</TableRow>
</TableHead>
)}
<Card className="p-6">
<p className="text-sm text-muted-foreground mb-3">
Create a budget to assign to customers.
</p>
<Table>
<TableHeader>
<TableRow>
<TableHead>Budget ID</TableHead>
<TableHead>Max Budget</TableHead>
<TableHead>TPM</TableHead>
<TableHead>RPM</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{budgetList
.slice()
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
.map((value: budgetItem) => (
<TableRow key={value.budget_id}>
<TableCell>{value.budget_id}</TableCell>
<TableCell>{value.max_budget ? value.max_budget : "n/a"}</TableCell>
<TableCell>{value.tpm_limit ? value.tpm_limit : "n/a"}</TableCell>
<TableCell>{value.rpm_limit ? value.rpm_limit : "n/a"}</TableCell>
<TableIconActionButton
variant="Edit"
tooltipText="Edit budget"
onClick={() => handleEditCall(value)}
dataTestId="edit-budget-button"
/>
<TableIconActionButton
variant="Delete"
tooltipText="Delete budget"
onClick={() => handleDeleteClick(value)}
dataTestId="delete-budget-button"
/>
</TableRow>
))}
</TableBody>
</Table>
</Card>
<DeleteResourceModal
isOpen={isDeleteModalVisible}
title="Delete Budget?"
message="Are you sure you want to delete this budget? This action cannot be undone."
resourceInformationTitle="Budget Information"
resourceInformation={[
{ label: "Budget ID", value: selectedBudget?.budget_id, code: true },
{ label: "Max Budget", value: selectedBudget?.max_budget },
{ label: "TPM", value: selectedBudget?.tpm_limit },
{ label: "RPM", value: selectedBudget?.rpm_limit },
]}
onCancel={handleDeleteCancel}
onOk={handleDeleteConfirm}
confirmLoading={deleteBudget.isPending}
/>
</div>
</TabPanel>
<TabPanel>
<div className="mt-6">
<Text className="text-base">How to use budget id</Text>
<TabGroup>
<TabList>
<Tab>Assign Budget to Customer</Tab>
<Tab>Test it (Curl)</Tab>
<Tab>Test it (OpenAI SDK)</Tab>
</TabList>
<TabPanels>
<TabPanel>
<SyntaxHighlighter language="bash">{CREATE_END_USER_CURL_COMMAND}</SyntaxHighlighter>
</TabPanel>
<TabPanel>
<SyntaxHighlighter language="bash">{CHAT_COMPLETIONS_CURL_COMMAND}</SyntaxHighlighter>
</TabPanel>
<TabPanel>
<SyntaxHighlighter language="python">{OPENAI_SDK_PYTHON_CODE}</SyntaxHighlighter>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
</TabPanel>
</TabPanels>
</TabGroup>
<TableBody>
{budgetList
.slice()
.sort(
(a, b) =>
new Date(b.updated_at).getTime() -
new Date(a.updated_at).getTime(),
)
.map((value: budgetItem) => (
<TableRow key={value.budget_id}>
<TableCell>{value.budget_id}</TableCell>
<TableCell>
{value.max_budget ? value.max_budget : "n/a"}
</TableCell>
<TableCell>
{value.tpm_limit ? value.tpm_limit : "n/a"}
</TableCell>
<TableCell>
{value.rpm_limit ? value.rpm_limit : "n/a"}
</TableCell>
<TableCell>
<div className="flex gap-1">
<TableIconActionButton
variant="Edit"
tooltipText="Edit budget"
onClick={() => handleEditCall(value)}
dataTestId="edit-budget-button"
/>
<TableIconActionButton
variant="Delete"
tooltipText="Delete budget"
onClick={() => handleDeleteClick(value)}
dataTestId="delete-budget-button"
/>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
<DeleteResourceModal
isOpen={isDeleteModalVisible}
title="Delete Budget?"
message="Are you sure you want to delete this budget? This action cannot be undone."
resourceInformationTitle="Budget Information"
resourceInformation={[
{ label: "Budget ID", value: selectedBudget?.budget_id, code: true },
{ label: "Max Budget", value: selectedBudget?.max_budget },
{ label: "TPM", value: selectedBudget?.tpm_limit },
{ label: "RPM", value: selectedBudget?.rpm_limit },
]}
onCancel={handleDeleteCancel}
onOk={handleDeleteConfirm}
confirmLoading={deleteBudget.isPending}
/>
</div>
</TabsContent>
<TabsContent value="examples">
<div className="mt-6">
<p className="text-base font-medium">How to use budget id</p>
<Tabs defaultValue="assign">
<TabsList>
<TabsTrigger value="assign">Assign Budget to Customer</TabsTrigger>
<TabsTrigger value="curl">Test it (Curl)</TabsTrigger>
<TabsTrigger value="openai">Test it (OpenAI SDK)</TabsTrigger>
</TabsList>
<TabsContent value="assign">
<SyntaxHighlighter language="bash">
{CREATE_END_USER_CURL_COMMAND}
</SyntaxHighlighter>
</TabsContent>
<TabsContent value="curl">
<SyntaxHighlighter language="bash">
{CHAT_COMPLETIONS_CURL_COMMAND}
</SyntaxHighlighter>
</TabsContent>
<TabsContent value="openai">
<SyntaxHighlighter language="python">
{OPENAI_SDK_PYTHON_CODE}
</SyntaxHighlighter>
</TabsContent>
</Tabs>
</div>
</TabsContent>
</Tabs>
</div>
);
};

View file

@ -1,106 +1,217 @@
import React, { useEffect } from "react";
import { TextInput, Accordion, AccordionHeader, AccordionBody } from "@tremor/react";
import { Button as Button2, Modal, Form, InputNumber, Select } from "antd";
import { useUpdateBudget } from "@/app/(dashboard)/hooks/budgets/useBudgets";
import { budgetItem } from "./budget_panel";
import NotificationsManager from "../molecules/notifications_manager";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Controller, FormProvider, useForm } from "react-hook-form";
interface EditBudgetModalProps {
isModalVisible: boolean;
setIsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
existingBudget: budgetItem;
}
type EditBudgetFormValues = {
budget_id: string;
tpm_limit: number | null;
rpm_limit: number | null;
max_budget: number | null;
budget_duration: string | null;
};
function LabeledRow({
id,
label,
help,
children,
disabledHint,
}: {
id?: string;
label: string;
help?: string;
children: React.ReactNode;
disabledHint?: string;
}) {
return (
<div className="grid grid-cols-3 gap-3 items-start mb-4">
<Label htmlFor={id} className="pt-2">
{label}
</Label>
<div className="col-span-2 space-y-1">
{children}
{help && <p className="text-xs text-muted-foreground">{help}</p>}
{disabledHint && (
<p className="text-xs text-muted-foreground">{disabledHint}</p>
)}
</div>
</div>
);
}
const EditBudgetModal: React.FC<EditBudgetModalProps> = ({
isModalVisible,
setIsModalVisible,
existingBudget,
}) => {
const [form] = Form.useForm();
const form = useForm<EditBudgetFormValues>({
defaultValues: {
budget_id: existingBudget.budget_id,
tpm_limit: existingBudget.tpm_limit,
rpm_limit: existingBudget.rpm_limit,
max_budget: existingBudget.max_budget,
budget_duration: null,
},
mode: "onSubmit",
});
const updateBudget = useUpdateBudget();
useEffect(() => {
form.setFieldsValue(existingBudget);
}, [existingBudget, form]);
const handleOk = () => {
setIsModalVisible(false);
form.resetFields();
};
form.reset({
budget_id: existingBudget.budget_id,
tpm_limit: existingBudget.tpm_limit,
rpm_limit: existingBudget.rpm_limit,
max_budget: existingBudget.max_budget,
budget_duration: null,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [existingBudget]);
const handleCancel = () => {
setIsModalVisible(false);
form.resetFields();
};
const handleUpdate = async (formValues: Record<string, any>) => {
const handleUpdate = form.handleSubmit(async (values) => {
try {
NotificationsManager.info("Making API Call");
await updateBudget.mutateAsync(formValues);
await updateBudget.mutateAsync(values);
NotificationsManager.success("Budget Updated");
form.resetFields();
setIsModalVisible(false);
} catch (error) {
console.error("Error updating the budget:", error);
NotificationsManager.fromBackend(`Error updating the budget: ${error}`);
}
};
});
return (
<Modal
title="Edit Budget"
<Dialog
open={isModalVisible}
width={800}
footer={null}
onOk={handleOk}
onCancel={handleCancel}
onOpenChange={(o) => (!o ? handleCancel() : undefined)}
>
<Form
form={form}
onFinish={handleUpdate}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
initialValues={existingBudget}
>
<>
<Form.Item
label="Budget ID"
name="budget_id"
help="Budget ID cannot be changed after creation"
>
<TextInput placeholder="" disabled={true} />
</Form.Item>
<Form.Item label="Max Tokens per minute" name="tpm_limit" help="Default is model limit.">
<InputNumber step={1} precision={2} width={200} />
</Form.Item>
<Form.Item label="Max Requests per minute" name="rpm_limit" help="Default is model limit.">
<InputNumber step={1} precision={2} width={200} />
</Form.Item>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Edit Budget</DialogTitle>
<DialogDescription className="sr-only">
Update an existing budget. Budget ID cannot be changed after creation.
</DialogDescription>
</DialogHeader>
<FormProvider {...form}>
<form onSubmit={handleUpdate}>
<LabeledRow
id="budget_id"
label="Budget ID"
help="Budget ID cannot be changed after creation"
>
<Input id="budget_id" {...form.register("budget_id")} disabled />
</LabeledRow>
<LabeledRow
id="tpm_limit"
label="Max Tokens per minute"
help="Default is model limit."
>
<Input
id="tpm_limit"
type="number"
step={1}
{...form.register("tpm_limit", { valueAsNumber: true })}
/>
</LabeledRow>
<LabeledRow
id="rpm_limit"
label="Max Requests per minute"
help="Default is model limit."
>
<Input
id="rpm_limit"
type="number"
step={1}
{...form.register("rpm_limit", { valueAsNumber: true })}
/>
</LabeledRow>
<Accordion className="mt-20 mb-8">
<AccordionHeader>
<b>Optional Settings</b>
</AccordionHeader>
<AccordionBody>
<Form.Item label="Max Budget (USD)" name="max_budget">
<InputNumber step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item className="mt-8" label="Reset Budget" name="budget_duration">
<Select defaultValue={null} placeholder="n/a">
<Select.Option value="24h">daily</Select.Option>
<Select.Option value="7d">weekly</Select.Option>
<Select.Option value="30d">monthly</Select.Option>
</Select>
</Form.Item>
</AccordionBody>
</Accordion>
</>
<Accordion type="single" collapsible className="mt-8 mb-4">
<AccordionItem value="optional">
<AccordionTrigger className="font-semibold">
Optional Settings
</AccordionTrigger>
<AccordionContent>
<LabeledRow id="max_budget" label="Max Budget (USD)">
<Input
id="max_budget"
type="number"
step={0.01}
{...form.register("max_budget", { valueAsNumber: true })}
/>
</LabeledRow>
<LabeledRow label="Reset Budget">
<Controller
control={form.control}
name="budget_duration"
render={({ field }) => (
<Select
value={field.value ?? ""}
onValueChange={(v) => field.onChange(v || null)}
>
<SelectTrigger>
<SelectValue placeholder="n/a" />
</SelectTrigger>
<SelectContent>
<SelectItem value="24h">daily</SelectItem>
<SelectItem value="7d">weekly</SelectItem>
<SelectItem value="30d">monthly</SelectItem>
</SelectContent>
</Select>
)}
/>
</LabeledRow>
</AccordionContent>
</AccordionItem>
</Accordion>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Save</Button2>
</div>
</Form>
</Modal>
<DialogFooter className="mt-2">
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" disabled={updateBudget.isPending}>
{updateBudget.isPending ? "Saving…" : "Save"}
</Button>
</DialogFooter>
</form>
</FormProvider>
</DialogContent>
</Dialog>
);
};