Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Pencil, Plus, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { getLobeIcon } from '@/lib/lobe-icon'
import { getVendors } from '../../api'
import { vendorsQueryKeys } from '../../lib'
import { handleDeleteVendor } from '../../lib/vendor-actions'
import type { Vendor } from '../../types'
import { useModels } from '../models-provider'

type VendorManageDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
}

export function VendorManageDialog({
open,
onOpenChange,
}: VendorManageDialogProps) {
const { t } = useTranslation()
const { setOpen, setCurrentVendor } = useModels()
const queryClient = useQueryClient()
const [deleteTarget, setDeleteTarget] = useState<Vendor | null>(null)

const { data, isLoading } = useQuery({
queryKey: vendorsQueryKeys.list(),
queryFn: () => getVendors({ page_size: 1000 }),
enabled: open,
})

const vendors = data?.data?.items ?? []

const handleEdit = (vendor: Vendor) => {
setCurrentVendor(vendor)
setOpen('update-vendor')
}

const handleCreate = () => {
setCurrentVendor(null)
setOpen('create-vendor')
}

return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='max-w-lg'>
<DialogHeader>
<DialogTitle>{t('Manage Vendors')}</DialogTitle>
<DialogDescription>
{t('Create, edit, or delete vendors')}
</DialogDescription>
</DialogHeader>

<div className='max-h-80 space-y-2 overflow-y-auto'>
{!isLoading && vendors.length === 0 && (
<p className='text-muted-foreground py-4 text-center text-sm'>
{t('No vendors yet')}
</p>
)}
{vendors.map((vendor) => {
const icon = vendor.icon
? getLobeIcon(vendor.icon, 20)
: null
return (
<div
key={vendor.id}
className='flex items-center justify-between rounded-lg border px-3 py-2'
>
<div className='flex items-center gap-2'>
{icon}
<div>
<div className='text-sm font-medium'>
{vendor.name}
</div>
{vendor.description && (
<div className='text-muted-foreground text-xs'>
{vendor.description}
</div>
)}
</div>
</div>
<div className='flex items-center gap-1'>
<Button
variant='ghost'
size='icon'
className='h-8 w-8'
aria-label={t('Edit Vendor')}
onClick={() => handleEdit(vendor)}
>
<Pencil className='h-4 w-4' />
</Button>
<Button
variant='ghost'
size='icon'
className='text-destructive hover:text-destructive h-8 w-8'
aria-label={t('Delete Vendor')}
onClick={() => setDeleteTarget(vendor)}
>
<Trash2 className='h-4 w-4' />
</Button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</div>
)
})}
</div>

<Button onClick={handleCreate} className='w-full'>
<Plus className='mr-2 h-4 w-4' />
{t('Create Vendor')}
</Button>
</DialogContent>
</Dialog>

<ConfirmDialog
open={!!deleteTarget}
onOpenChange={(v) => !v && setDeleteTarget(null)}
title={t('Delete Vendor')}
desc={t('Are you sure you want to delete "{{name}}"?', {
name: deleteTarget?.name,
})}
confirmText={t('Delete')}
destructive
handleConfirm={() => {
if (deleteTarget) {
handleDeleteVendor(deleteTarget.id, queryClient)
setDeleteTarget(null)
}
}}
/>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ export function VendorMutateDialog({

if (response.success) {
toast.success(
isEdit ? 'Vendor updated successfully' : 'Vendor created successfully'
isEdit ? t('Vendor updated successfully') : t('Vendor created successfully')
)
queryClient.invalidateQueries({ queryKey: vendorsQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: modelsQueryKeys.lists() })
onOpenChange(false)
} else {
toast.error(response.message || 'Operation failed')
toast.error(response.message || t('Operation failed'))
}
} catch (error: unknown) {
toast.error((error as Error)?.message || 'Operation failed')
Expand Down Expand Up @@ -186,7 +186,7 @@ export function VendorMutateDialog({
</Button>
<Button type='submit' disabled={isSaving}>
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isSaving ? 'Saving...' : isEdit ? 'Update' : 'Create'}
{isSaving ? t('Saving...') : isEdit ? t('Update') : t('Create')}
</Button>
</DialogFooter>
</form>
Expand Down
37 changes: 25 additions & 12 deletions web/default/src/features/models/components/models-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ import { parseModelTags, formatEndpointsDisplay } from '../lib'
import type { Model, Vendor } from '../types'
import { DataTableRowActions } from './data-table-row-actions'
import { DescriptionCell } from './description-cell'
import { useModels } from './models-provider'

function VendorCell({ vendor }: { vendor: Vendor }) {
const { setOpen, setCurrentVendor } = useModels()
const icon = vendor.icon ? getLobeIcon(vendor.icon, 14) : null

return (
<button
type='button'
className='flex cursor-pointer items-center gap-1.5 hover:opacity-80'
onClick={() => {
setCurrentVendor(vendor)
setOpen('update-vendor')
}}
>
{icon}
<StatusBadge
label={vendor.name}
autoColor={vendor.name}
size='sm'
/>
</button>
)
}

/**
* Render limited items with "and X more" indicator
Expand Down Expand Up @@ -266,18 +290,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
return <span className='text-muted-foreground text-xs'>-</span>
}

const icon = vendor.icon ? getLobeIcon(vendor.icon, 14) : null

return (
<div className='flex items-center gap-1.5'>
{icon}
<StatusBadge
label={vendor.name}
autoColor={vendor.name}
size='sm'
/>
</div>
)
return <VendorCell vendor={vendor} />
},
filterFn: (row, id, value) => {
if (!value || value.length === 0 || value.includes('all')) return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MissingModelsDialog } from './dialogs/missing-models-dialog'
import { PrefillGroupManagement } from './dialogs/prefill-group-management'
import { SyncWizardDialog } from './dialogs/sync-wizard-dialog'
import { UpstreamConflictDialog } from './dialogs/upstream-conflict-dialog'
import { VendorManageDialog } from './dialogs/vendor-manage-dialog'
import { VendorMutateDialog } from './dialogs/vendor-mutate-dialog'
import { ModelMutateDrawer } from './drawers/model-mutate-drawer'
import { useModels } from './models-provider'
Expand All @@ -26,10 +27,16 @@ export function ModelsDialogs() {
currentRow={currentRow}
/>

{/* Vendor Management Dialog */}
<VendorManageDialog
open={open === 'manage-vendors'}
onOpenChange={(v) => !v && setOpen(null)}
/>

{/* Vendor Create/Update Dialog */}
<VendorMutateDialog
open={open === 'create-vendor' || open === 'update-vendor'}
onOpenChange={(v) => !v && setOpen(null)}
onOpenChange={(v) => !v && setOpen('manage-vendors')}
currentVendor={open === 'update-vendor' ? currentVendor : null}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function ModelsPrimaryButtons() {
}

const handleManageVendors = () => {
setOpen('create-vendor') // Will be a separate vendors management dialog
setOpen('manage-vendors')
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type DialogType =
| 'update-model'
| 'create-vendor'
| 'update-vendor'
| 'manage-vendors'
| 'missing-models'
| 'sync-wizard'
| 'upstream-conflict'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import { DataTableColumnHeader } from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge'
import { formatQuota } from '@/lib/format'
import { formatDuration, formatResetPeriod } from '../lib'
import type { PlanRecord } from '../types'
import { DataTableRowActions } from './data-table-row-actions'
Expand Down Expand Up @@ -159,7 +160,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
const total = Number(row.original.plan.total_amount || 0)
return (
<span className='text-muted-foreground'>
{total > 0 ? total : t('Unlimited')}
{total > 0 ? formatQuota(total) : t('Unlimited')}
</span>
)
},
Expand Down
5 changes: 3 additions & 2 deletions web/default/src/features/subscriptions/lib/plan-form.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import type { TFunction } from 'i18next'
import { parseQuotaFromDollars, quotaUnitsToDollars } from '@/lib/format'
import type { SubscriptionPlan, PlanPayload } from '../types'

export function getPlanFormSchema(t: TFunction) {
Expand Down Expand Up @@ -61,7 +62,7 @@ export function planToFormValues(plan: SubscriptionPlan): PlanFormValues {
enabled: plan.enabled !== false,
sort_order: Number(plan.sort_order || 0),
max_purchase_per_user: Number(plan.max_purchase_per_user || 0),
total_amount: Number(plan.total_amount || 0),
total_amount: Number(quotaUnitsToDollars(plan.total_amount || 0).toFixed(2)),
upgrade_group: plan.upgrade_group || '',
stripe_price_id: plan.stripe_price_id || '',
creem_product_id: plan.creem_product_id || '',
Expand All @@ -83,7 +84,7 @@ export function formValuesToPlanPayload(values: PlanFormValues): PlanPayload {
: 0,
sort_order: Number(values.sort_order || 0),
max_purchase_per_user: Number(values.max_purchase_per_user || 0),
total_amount: Number(values.total_amount || 0),
total_amount: parseQuotaFromDollars(Number(values.total_amount || 0)),
upgrade_group: values.upgrade_group || '',
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ const quotaSchema = z.object({
PreConsumedQuota: z.coerce.number().min(0),
QuotaForInviter: z.coerce.number().min(0),
QuotaForInvitee: z.coerce.number().min(0),
TopUpLink: z.string().url().optional().or(z.literal('')),
'general_setting.docs_link': z.string().url().optional().or(z.literal('')),
'quota_setting.enable_free_model_pre_consume': z.boolean(),
TopUpLink: z.string().default(''),
general_setting: z.object({
docs_link: z.string().default(''),
}),
quota_setting: z.object({
enable_free_model_pre_consume: z.boolean(),
}),
})

type QuotaFormValues = z.infer<typeof quotaSchema>
Expand Down Expand Up @@ -80,7 +84,7 @@ export function QuotaSettingsSection({
<Input
type='number'
value={field.value as number}
onChange={(e) => field.onChange(e.target.valueAsNumber)}
onChange={(e) => field.onChange(Number.isNaN(e.target.valueAsNumber) ? 0 : e.target.valueAsNumber)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
Expand All @@ -104,7 +108,7 @@ export function QuotaSettingsSection({
<Input
type='number'
value={field.value as number}
onChange={(e) => field.onChange(e.target.valueAsNumber)}
onChange={(e) => field.onChange(Number.isNaN(e.target.valueAsNumber) ? 0 : e.target.valueAsNumber)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
Expand All @@ -128,7 +132,7 @@ export function QuotaSettingsSection({
<Input
type='number'
value={field.value as number}
onChange={(e) => field.onChange(e.target.valueAsNumber)}
onChange={(e) => field.onChange(Number.isNaN(e.target.valueAsNumber) ? 0 : e.target.valueAsNumber)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
Expand All @@ -152,7 +156,7 @@ export function QuotaSettingsSection({
<Input
type='number'
value={field.value as number}
onChange={(e) => field.onChange(e.target.valueAsNumber)}
onChange={(e) => field.onChange(Number.isNaN(e.target.valueAsNumber) ? 0 : e.target.valueAsNumber)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
Expand Down
8 changes: 8 additions & 0 deletions web/default/src/i18n/locales/_extras/fr.extras.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Are you sure you want to delete \"{{name}}\"?": "Êtes-vous sûr de vouloir supprimer \"{{name}}\" ?",
"Create, edit, or delete vendors": "Créer, modifier ou supprimer des fournisseurs",
"Delete Vendor": "Supprimer le fournisseur",
"No vendors yet": "Aucun fournisseur",
"Vendor created successfully": "Fournisseur créé avec succès",
"Vendor updated successfully": "Fournisseur mis à jour avec succès"
}
8 changes: 8 additions & 0 deletions web/default/src/i18n/locales/_extras/ja.extras.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Are you sure you want to delete \"{{name}}\"?": "\"{{name}}\" を削除してもよろしいですか?",
"Create, edit, or delete vendors": "ベンダーの作成、編集、削除",
"Delete Vendor": "ベンダーを削除",
"No vendors yet": "ベンダーはまだありません",
"Vendor created successfully": "ベンダーを作成しました",
"Vendor updated successfully": "ベンダーを更新しました"
Comment on lines +2 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fill in the remaining vendor-dialog translations.

This file only adds a small subset of the keys used by VendorMutateDialog and VendorManageDialog, so the dialogs will still render a mix of Japanese and English. Please sync the rest of the vendor-facing strings here as well, or confirm that the fallback behavior is intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/i18n/locales/_extras/ja.extras.json` around lines 2 - 7, The
ja.extras.json file only contains a subset of strings used by VendorMutateDialog
and VendorManageDialog; add translations for all remaining vendor-facing keys
referenced by those components (labels, buttons like "Create vendor", "Update
vendor", "Cancel", "Confirm", form field labels, validation/error messages, list
headers, and any pluralization keys) so the dialogs don't mix English/Japanese;
preserve interpolation tokens (e.g., "{{name}}") and escaping/quoting exactly as
in existing entries and mirror the English keys' keys/names used by
VendorMutateDialog and VendorManageDialog when adding the Japanese values.

}
8 changes: 8 additions & 0 deletions web/default/src/i18n/locales/_extras/ru.extras.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Are you sure you want to delete \"{{name}}\"?": "Вы уверены, что хотите удалить \"{{name}}\"?",
"Create, edit, or delete vendors": "Создание, редактирование или удаление поставщиков",
"Delete Vendor": "Удалить поставщика",
"No vendors yet": "Поставщиков пока нет",
"Vendor created successfully": "Поставщик успешно создан",
"Vendor updated successfully": "Поставщик успешно обновлён"
}
8 changes: 8 additions & 0 deletions web/default/src/i18n/locales/_extras/vi.extras.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Are you sure you want to delete \"{{name}}\"?": "Bạn có chắc muốn xóa \"{{name}}\" không?",
"Create, edit, or delete vendors": "Tạo, chỉnh sửa hoặc xóa nhà cung cấp",
"Delete Vendor": "Xóa nhà cung cấp",
"No vendors yet": "Chưa có nhà cung cấp",
"Vendor created successfully": "Tạo nhà cung cấp thành công",
"Vendor updated successfully": "Cập nhật nhà cung cấp thành công"
}
Loading