mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-12 07:20:43 +02:00
make the budget match the rules
This commit is contained in:
@@ -1657,6 +1657,9 @@
|
|||||||
"aiProviderBudgetSettingsDescription": "Configure how this provider restricts usage based on spending or token limits",
|
"aiProviderBudgetSettingsDescription": "Configure how this provider restricts usage based on spending or token limits",
|
||||||
"aiBudgetAdd": "Add Budget",
|
"aiBudgetAdd": "Add Budget",
|
||||||
"aiBudgetEmpty": "No budgets configured yet. Click Add Budget to set a spending or token limit.",
|
"aiBudgetEmpty": "No budgets configured yet. Click Add Budget to set a spending or token limit.",
|
||||||
|
"aiBudgetUnit": "Spend Type",
|
||||||
|
"aiBudgetPeriod": "Reset Period",
|
||||||
|
"aiBudgetAmount": "Maximum Spend",
|
||||||
"aiBudgetAmountPlaceholder": "Maximum spend",
|
"aiBudgetAmountPlaceholder": "Maximum spend",
|
||||||
"aiBudgetPeriodHourly": "Hourly",
|
"aiBudgetPeriodHourly": "Hourly",
|
||||||
"aiBudgetPeriodDaily": "Daily",
|
"aiBudgetPeriodDaily": "Daily",
|
||||||
|
|||||||
+239
-148
@@ -1,14 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
SettingsFormCell,
|
||||||
|
SettingsFormGrid,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionFooter,
|
SettingsSectionFooter,
|
||||||
|
SettingsSectionForm,
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { DataTableEmptyState } from "@app/components/ui/data-table-empty-state";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -17,6 +21,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from "@app/components/ui/table";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
@@ -247,160 +259,239 @@ export function BudgetsEditor({
|
|||||||
tokens: t("aiBudgetUnitTokens")
|
tokens: t("aiBudgetUnitTokens")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addRowButton = (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={addRow}
|
||||||
|
disabled={saveLoading || budgetsQuery.isLoading}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
{t("aiBudgetAdd")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<SettingsSectionTitle>{title}</SettingsSectionTitle>
|
||||||
<div className="space-y-0.5">
|
<SettingsSectionDescription>
|
||||||
<SettingsSectionTitle>{title}</SettingsSectionTitle>
|
{description}
|
||||||
<SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
{description}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={addRow}
|
|
||||||
disabled={budgetsQuery.isLoading}
|
|
||||||
className="shrink-0"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
|
||||||
{t("aiBudgetAdd")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
{rows.length === 0 ? (
|
<SettingsSectionForm>
|
||||||
<p className="text-sm text-muted-foreground">
|
<SettingsFormGrid>
|
||||||
{t("aiBudgetEmpty")}
|
<SettingsFormCell span="half">
|
||||||
</p>
|
<div className="space-y-4">
|
||||||
) : (
|
<Table>
|
||||||
<div className="space-y-3">
|
<TableHeader>
|
||||||
{rows.map((row) => {
|
<TableRow>
|
||||||
const showConflict = conflictingKeys.has(row.key);
|
<TableHead>
|
||||||
const showInvalidAmount =
|
{t("aiBudgetUnit")}
|
||||||
attemptedSave &&
|
</TableHead>
|
||||||
invalidAmountKeys.has(row.key);
|
<TableHead>
|
||||||
return (
|
{t("aiBudgetPeriod")}
|
||||||
<div key={row.key} className="space-y-1">
|
</TableHead>
|
||||||
<div className="flex items-start gap-2">
|
<TableHead>
|
||||||
<Input
|
{t("aiBudgetAmount")}
|
||||||
type="number"
|
</TableHead>
|
||||||
min="0"
|
<TableHead></TableHead>
|
||||||
step="any"
|
</TableRow>
|
||||||
placeholder={t(
|
</TableHeader>
|
||||||
"aiBudgetAmountPlaceholder"
|
<TableBody>
|
||||||
)}
|
{rows.length === 0 ? (
|
||||||
value={row.amount}
|
<DataTableEmptyState
|
||||||
aria-invalid={showInvalidAmount}
|
colSpan={4}
|
||||||
disabled={
|
message={t("aiBudgetEmpty")}
|
||||||
saveLoading ||
|
action={addRowButton}
|
||||||
budgetsQuery.isLoading
|
compact
|
||||||
}
|
/>
|
||||||
onChange={(e) =>
|
) : (
|
||||||
updateRow(row.key, {
|
rows.map((row) => {
|
||||||
amount: e.target.value
|
const showConflict =
|
||||||
})
|
conflictingKeys.has(
|
||||||
}
|
row.key
|
||||||
className="flex-1"
|
);
|
||||||
/>
|
const showInvalidAmount =
|
||||||
<Select
|
attemptedSave &&
|
||||||
value={row.period}
|
invalidAmountKeys.has(
|
||||||
onValueChange={(value) =>
|
row.key
|
||||||
updateRow(row.key, {
|
);
|
||||||
period: value as AiBudgetPeriod
|
return (
|
||||||
})
|
<TableRow key={row.key}>
|
||||||
}
|
<TableCell>
|
||||||
disabled={
|
<Select
|
||||||
saveLoading ||
|
value={row.unit}
|
||||||
budgetsQuery.isLoading
|
onValueChange={(
|
||||||
}
|
value
|
||||||
>
|
) =>
|
||||||
<SelectTrigger
|
updateRow(
|
||||||
className="w-44 shrink-0"
|
row.key,
|
||||||
aria-invalid={showConflict}
|
{
|
||||||
>
|
unit: value as AiBudgetUnit
|
||||||
<SelectValue />
|
}
|
||||||
</SelectTrigger>
|
)
|
||||||
<SelectContent>
|
}
|
||||||
{AI_BUDGET_PERIODS.map(
|
disabled={
|
||||||
(period) => (
|
saveLoading ||
|
||||||
<SelectItem
|
budgetsQuery.isLoading
|
||||||
key={period}
|
}
|
||||||
value={period}
|
>
|
||||||
>
|
<SelectTrigger
|
||||||
{
|
className="w-full min-w-0"
|
||||||
periodLabels[
|
aria-invalid={
|
||||||
period
|
showConflict
|
||||||
]
|
}
|
||||||
}
|
>
|
||||||
</SelectItem>
|
<SelectValue />
|
||||||
)
|
</SelectTrigger>
|
||||||
)}
|
<SelectContent>
|
||||||
</SelectContent>
|
{AI_BUDGET_UNITS.map(
|
||||||
</Select>
|
(
|
||||||
<Select
|
unit
|
||||||
value={row.unit}
|
) => (
|
||||||
onValueChange={(value) =>
|
<SelectItem
|
||||||
updateRow(row.key, {
|
key={
|
||||||
unit: value as AiBudgetUnit
|
unit
|
||||||
})
|
}
|
||||||
}
|
value={
|
||||||
disabled={
|
unit
|
||||||
saveLoading ||
|
}
|
||||||
budgetsQuery.isLoading
|
>
|
||||||
}
|
{
|
||||||
>
|
unitLabels[
|
||||||
<SelectTrigger
|
unit
|
||||||
className="w-32 shrink-0"
|
]
|
||||||
aria-invalid={showConflict}
|
}
|
||||||
>
|
</SelectItem>
|
||||||
<SelectValue />
|
)
|
||||||
</SelectTrigger>
|
)}
|
||||||
<SelectContent>
|
</SelectContent>
|
||||||
{AI_BUDGET_UNITS.map(
|
</Select>
|
||||||
(unit) => (
|
</TableCell>
|
||||||
<SelectItem
|
<TableCell>
|
||||||
key={unit}
|
<Select
|
||||||
value={unit}
|
value={
|
||||||
>
|
row.period
|
||||||
{unitLabels[unit]}
|
}
|
||||||
</SelectItem>
|
onValueChange={(
|
||||||
)
|
value
|
||||||
)}
|
) =>
|
||||||
</SelectContent>
|
updateRow(
|
||||||
</Select>
|
row.key,
|
||||||
<Button
|
{
|
||||||
type="button"
|
period: value as AiBudgetPeriod
|
||||||
variant="ghost"
|
}
|
||||||
size="icon"
|
)
|
||||||
className="shrink-0"
|
}
|
||||||
disabled={
|
disabled={
|
||||||
saveLoading ||
|
saveLoading ||
|
||||||
budgetsQuery.isLoading
|
budgetsQuery.isLoading
|
||||||
}
|
}
|
||||||
onClick={() => removeRow(row.key)}
|
>
|
||||||
>
|
<SelectTrigger
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
className="w-full min-w-0"
|
||||||
</Button>
|
aria-invalid={
|
||||||
</div>
|
showConflict
|
||||||
{showConflict && (
|
}
|
||||||
<p className="text-xs text-destructive">
|
>
|
||||||
{t("aiBudgetConflictError")}
|
<SelectValue />
|
||||||
</p>
|
</SelectTrigger>
|
||||||
)}
|
<SelectContent>
|
||||||
{showInvalidAmount && (
|
{AI_BUDGET_PERIODS.map(
|
||||||
<p className="text-xs text-destructive">
|
(
|
||||||
{t("aiBudgetInvalidAmountError")}
|
period
|
||||||
</p>
|
) => (
|
||||||
)}
|
<SelectItem
|
||||||
</div>
|
key={
|
||||||
);
|
period
|
||||||
})}
|
}
|
||||||
</div>
|
value={
|
||||||
)}
|
period
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
periodLabels[
|
||||||
|
period
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="any"
|
||||||
|
placeholder={t(
|
||||||
|
"aiBudgetAmountPlaceholder"
|
||||||
|
)}
|
||||||
|
value={
|
||||||
|
row.amount
|
||||||
|
}
|
||||||
|
aria-invalid={
|
||||||
|
showInvalidAmount
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
saveLoading ||
|
||||||
|
budgetsQuery.isLoading
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateRow(
|
||||||
|
row.key,
|
||||||
|
{
|
||||||
|
amount: e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full min-w-0"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
disabled={
|
||||||
|
saveLoading ||
|
||||||
|
budgetsQuery.isLoading
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
removeRow(
|
||||||
|
row.key
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{(conflictingKeys.size > 0 ||
|
||||||
|
(attemptedSave &&
|
||||||
|
invalidAmountKeys.size > 0)) && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{conflictingKeys.size > 0
|
||||||
|
? t("aiBudgetConflictError")
|
||||||
|
: t("aiBudgetInvalidAmountError")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{rows.length > 0 && addRowButton}
|
||||||
|
</div>
|
||||||
|
</SettingsFormCell>
|
||||||
|
</SettingsFormGrid>
|
||||||
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
|
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
|
|||||||
Reference in New Issue
Block a user