Commit 8283df16 by feitianbubu Committed by GitHub

feat: add unset price models tab to model pricing settings (#6124)

* feat: add unset price models tab to model pricing settings

* feat: add unset price models tab translations
parent 7a2b9d86
...@@ -110,7 +110,7 @@ const BILLING_SECTIONS = [ ...@@ -110,7 +110,7 @@ const BILLING_SECTIONS = [
modelDefaults={getModelDefaults(settings)} modelDefaults={getModelDefaults(settings)}
groupDefaults={getGroupDefaults(settings)} groupDefaults={getGroupDefaults(settings)}
toolPricesDefault={settings['tool_price_setting.prices']} toolPricesDefault={settings['tool_price_setting.prices']}
visibleTabs={['models', 'tool-prices', 'upstream-sync']} visibleTabs={['models', 'unset-models', 'tool-prices', 'upstream-sync']}
/> />
), ),
}, },
......
...@@ -61,6 +61,12 @@ export type ModelRow = ModelPricingSnapshot & { ...@@ -61,6 +61,12 @@ export type ModelRow = ModelPricingSnapshot & {
export const hasPricingValue = (value?: string) => export const hasPricingValue = (value?: string) =>
value !== undefined && value !== '' value !== undefined && value !== ''
export const isBasePricingUnset = (snapshot?: ModelPricingSnapshot) =>
!snapshot ||
(snapshot.billingMode !== 'tiered_expr' &&
!hasPricingValue(snapshot.price) &&
!hasPricingValue(snapshot.ratio))
const toNumberOrNull = (value?: string) => { const toNumberOrNull = (value?: string) => {
if (!hasPricingValue(value)) return null if (!hasPricingValue(value)) return null
const num = Number(value) const num = Number(value)
......
...@@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useQuery } from '@tanstack/react-query'
import { Code2, Eye, RotateCcw, Save } from 'lucide-react' import { Code2, Eye, RotateCcw, Save } from 'lucide-react'
import { memo, useCallback, useRef, useState } from 'react' import { memo, useCallback, useRef, useState } from 'react'
import { type UseFormReturn } from 'react-hook-form' import { type UseFormReturn } from 'react-hook-form'
...@@ -33,6 +34,7 @@ import { ...@@ -33,6 +34,7 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { getEnabledModels } from '@/features/channels/api'
import { import {
SettingsForm, SettingsForm,
...@@ -65,6 +67,7 @@ type ModelRatioFormProps = { ...@@ -65,6 +67,7 @@ type ModelRatioFormProps = {
onReset: () => void onReset: () => void
isSaving: boolean isSaving: boolean
isResetting: boolean isResetting: boolean
variant?: 'default' | 'unset'
} }
type ModelJsonFieldName = type ModelJsonFieldName =
...@@ -164,11 +167,19 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -164,11 +167,19 @@ export const ModelRatioForm = memo(function ModelRatioForm({
onReset, onReset,
isSaving, isSaving,
isResetting, isResetting,
variant = 'default',
}: ModelRatioFormProps) { }: ModelRatioFormProps) {
const { t } = useTranslation() const { t } = useTranslation()
const isUnsetVariant = variant === 'unset'
const [editMode, setEditMode] = useState<'visual' | 'json'>('visual') const [editMode, setEditMode] = useState<'visual' | 'json'>('visual')
const visualEditorRef = useRef<ModelRatioVisualEditorHandle>(null) const visualEditorRef = useRef<ModelRatioVisualEditorHandle>(null)
const enabledModelsQuery = useQuery({
queryKey: ['enabled-models'],
queryFn: getEnabledModels,
enabled: isUnsetVariant,
})
const handleFieldChange = useCallback( const handleFieldChange = useCallback(
(field: keyof ModelFormValues, value: string) => { (field: keyof ModelFormValues, value: string) => {
form.setValue(field, value, { form.setValue(field, value, {
...@@ -194,6 +205,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -194,6 +205,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
return ( return (
<div className='space-y-6'> <div className='space-y-6'>
{!isUnsetVariant && (
<div className='flex flex-wrap justify-end gap-2'> <div className='flex flex-wrap justify-end gap-2'>
<Button <Button
type='button' type='button'
...@@ -230,6 +242,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -230,6 +242,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
)} )}
</Button> </Button>
</div> </div>
)}
<Form {...form}> <Form {...form}>
{editMode === 'visual' ? ( {editMode === 'visual' ? (
...@@ -256,6 +269,10 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -256,6 +269,10 @@ export const ModelRatioForm = memo(function ModelRatioForm({
audioCompletionRatio={form.watch('AudioCompletionRatio')} audioCompletionRatio={form.watch('AudioCompletionRatio')}
billingMode={form.watch('BillingMode')} billingMode={form.watch('BillingMode')}
billingExpr={form.watch('BillingExpr')} billingExpr={form.watch('BillingExpr')}
candidateModelNames={
isUnsetVariant ? enabledModelsQuery.data?.data : undefined
}
filterMode={isUnsetVariant ? 'unset' : 'all'}
onSave={handleSave} onSave={handleSave}
isSaving={isSaving} isSaving={isSaving}
onChange={(field, value) => { onChange={(field, value) => {
...@@ -269,6 +286,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -269,6 +286,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
}} }}
/> />
{!isUnsetVariant && (
<FormField <FormField
control={form.control} control={form.control}
name='ExposeRatioEnabled' name='ExposeRatioEnabled'
...@@ -291,6 +309,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -291,6 +309,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
</SettingsSwitchItem> </SettingsSwitchItem>
)} )}
/> />
)}
</div> </div>
) : ( ) : (
<SettingsForm onSubmit={form.handleSubmit(onSave)}> <SettingsForm onSubmit={form.handleSubmit(onSave)}>
......
...@@ -42,12 +42,14 @@ const filterBySelectedValues = ( ...@@ -42,12 +42,14 @@ const filterBySelectedValues = (
type BuildModelRatioColumnsOptions = { type BuildModelRatioColumnsOptions = {
onDelete: (name: string) => void onDelete: (name: string) => void
onEdit: (model: ModelRow) => void onEdit: (model: ModelRow) => void
deleteDisabled?: boolean
t: (key: string) => string t: (key: string) => string
} }
export function buildModelRatioColumns({ export function buildModelRatioColumns({
onDelete, onDelete,
onEdit, onEdit,
deleteDisabled,
t, t,
}: BuildModelRatioColumnsOptions): ColumnDef<ModelRow>[] { }: BuildModelRatioColumnsOptions): ColumnDef<ModelRow>[] {
return [ return [
...@@ -151,6 +153,7 @@ export function buildModelRatioColumns({ ...@@ -151,6 +153,7 @@ export function buildModelRatioColumns({
menuLabel={t('Open menu')} menuLabel={t('Open menu')}
onEdit={() => onEdit(row.original)} onEdit={() => onEdit(row.original)}
onDelete={() => onDelete(row.original.name)} onDelete={() => onDelete(row.original.name)}
deleteDisabled={deleteDisabled}
/> />
), ),
enableHiding: false, enableHiding: false,
......
...@@ -60,6 +60,7 @@ import { ...@@ -60,6 +60,7 @@ import {
import { import {
buildModelSnapshots, buildModelSnapshots,
getSnapshotSignature, getSnapshotSignature,
isBasePricingUnset,
type ModelRow, type ModelRow,
} from './model-pricing-snapshots' } from './model-pricing-snapshots'
import { buildModelRatioColumns } from './model-ratio-table-columns' import { buildModelRatioColumns } from './model-ratio-table-columns'
...@@ -85,6 +86,8 @@ type ModelRatioVisualEditorProps = { ...@@ -85,6 +86,8 @@ type ModelRatioVisualEditorProps = {
audioCompletionRatio: string audioCompletionRatio: string
billingMode: string billingMode: string
billingExpr: string billingExpr: string
candidateModelNames?: string[]
filterMode?: 'all' | 'unset'
onChange: (field: string, value: string) => void onChange: (field: string, value: string) => void
onSave: () => void | Promise<void> onSave: () => void | Promise<void>
isSaving: boolean isSaving: boolean
...@@ -121,6 +124,8 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -121,6 +124,8 @@ const ModelRatioVisualEditorComponent = forwardRef<
audioCompletionRatio, audioCompletionRatio,
billingMode, billingMode,
billingExpr, billingExpr,
candidateModelNames,
filterMode = 'all',
onChange, onChange,
onSave, onSave,
isSaving, isSaving,
...@@ -208,18 +213,23 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -208,18 +213,23 @@ const ModelRatioVisualEditorComponent = forwardRef<
const savedByName = new Map(savedRows.map((row) => [row.name, row])) const savedByName = new Map(savedRows.map((row) => [row.name, row]))
const draftByName = new Map(draftRows.map((row) => [row.name, row])) const draftByName = new Map(draftRows.map((row) => [row.name, row]))
const modelNames = new Set([...savedByName.keys(), ...draftByName.keys()]) const modelNames = new Set([
...(candidateModelNames ?? []),
...savedByName.keys(),
...draftByName.keys(),
])
return Array.from(modelNames) return Array.from(modelNames)
.map((name) => { .map((name) => {
const saved = savedByName.get(name) const saved = savedByName.get(name)
const draft = draftByName.get(name) const draft = draftByName.get(name)
const displayed = saved ?? draft const displayed = saved ??
draft ?? { name, billingMode: 'per-token', hasConflict: false }
const savedSignature = getSnapshotSignature(saved) const savedSignature = getSnapshotSignature(saved)
const draftSignature = getSnapshotSignature(draft) const draftSignature = getSnapshotSignature(draft)
return { return {
...displayed!, ...displayed,
saved, saved,
draft, draft,
isDraftChanged: savedSignature !== draftSignature, isDraftChanged: savedSignature !== draftSignature,
...@@ -228,8 +238,11 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -228,8 +238,11 @@ const ModelRatioVisualEditorComponent = forwardRef<
} }
}) })
.filter((row) => !row.isDraftDeleted) .filter((row) => !row.isDraftDeleted)
.filter((row) => filterMode !== 'unset' || isBasePricingUnset(row.saved))
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
}, [ }, [
candidateModelNames,
filterMode,
savedModelPrice, savedModelPrice,
savedModelRatio, savedModelRatio,
savedCacheRatio, savedCacheRatio,
...@@ -423,14 +436,25 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -423,14 +436,25 @@ const ModelRatioVisualEditorComponent = forwardRef<
buildModelRatioColumns({ buildModelRatioColumns({
onDelete: handleDelete, onDelete: handleDelete,
onEdit: handleEdit, onEdit: handleEdit,
deleteDisabled: filterMode === 'unset',
t, t,
}), }),
[handleEdit, handleDelete, t] [handleEdit, handleDelete, filterMode, t]
)
const ensurePageInRange = useCallback((pageCount: number) => {
setPagination((prev) =>
pageCount > 0 && prev.pageIndex >= pageCount
? { ...prev, pageIndex: pageCount - 1 }
: prev
) )
}, [])
const { table } = useDataTable({ const { table } = useDataTable({
data: models, data: models,
columns, columns,
getRowId: (row) => row.name,
ensurePageInRange,
sorting, sorting,
columnFilters, columnFilters,
globalFilter, globalFilter,
...@@ -585,12 +609,20 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -585,12 +609,20 @@ const ModelRatioVisualEditorComponent = forwardRef<
] ]
) )
const handleBatchCopy = useCallback(() => { const handleBatchCopy = useCallback(async () => {
if (!editData) { if (!editData) {
toast.error(t('Open a source model first')) toast.error(t('Open a source model first'))
return return
} }
let sourceData = editData
if (editorOpen && editorPanelRef.current) {
const committed = await editorPanelRef.current.commitDraft()
if (!committed) return
sourceData = committed
setEditData(committed)
}
const targetNames = table const targetNames = table
.getFilteredSelectedRowModel() .getFilteredSelectedRowModel()
.rows.map((row) => row.original.name) .rows.map((row) => row.original.name)
...@@ -600,15 +632,15 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -600,15 +632,15 @@ const ModelRatioVisualEditorComponent = forwardRef<
return return
} }
persistPricingData(editData, targetNames) persistPricingData(sourceData, targetNames)
table.resetRowSelection() table.resetRowSelection()
toast.success( toast.success(
t('Applied {{name}} pricing to {{count}} models', { t('Applied {{name}} pricing to {{count}} models', {
name: editData.name, name: sourceData.name,
count: targetNames.length, count: targetNames.length,
}) })
) )
}, [editData, persistPricingData, t, table]) }, [editData, editorOpen, persistPricingData, t, table])
useImperativeHandle( useImperativeHandle(
ref, ref,
...@@ -627,6 +659,13 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -627,6 +659,13 @@ const ModelRatioVisualEditorComponent = forwardRef<
const hasRows = table.getRowModel().rows.length > 0 const hasRows = table.getRowModel().rows.length > 0
let emptyStateText = t('No models configured. Use Add model to get started.')
if (table.getState().globalFilter) {
emptyStateText = t('No models match your search')
} else if (filterMode === 'unset') {
emptyStateText = t('No models with unset prices')
}
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
<div className='grid h-[clamp(720px,calc(100vh-12rem),900px)] min-h-0 gap-4 md:grid-cols-[minmax(300px,0.72fr)_minmax(520px,1.28fr)] xl:grid-cols-[minmax(320px,0.68fr)_minmax(640px,1.32fr)]'> <div className='grid h-[clamp(720px,calc(100vh-12rem),900px)] min-h-0 gap-4 md:grid-cols-[minmax(300px,0.72fr)_minmax(520px,1.28fr)] xl:grid-cols-[minmax(320px,0.68fr)_minmax(640px,1.32fr)]'>
...@@ -658,18 +697,18 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -658,18 +697,18 @@ const ModelRatioVisualEditorComponent = forwardRef<
}, },
]} ]}
preActions={ preActions={
filterMode === 'unset' ? undefined : (
<Button onClick={handleAdd}> <Button onClick={handleAdd}>
<Plus data-icon='inline-start' /> <Plus data-icon='inline-start' />
{t('Add model')} {t('Add model')}
</Button> </Button>
)
} }
/> />
{!hasRows ? ( {!hasRows ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'> <div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
{table.getState().globalFilter {emptyStateText}
? t('No models match your search')
: t('No models configured. Use Add model to get started.')}
</div> </div>
) : ( ) : (
<DataTableView <DataTableView
...@@ -743,10 +782,12 @@ const ModelRatioVisualEditorComponent = forwardRef< ...@@ -743,10 +782,12 @@ const ModelRatioVisualEditorComponent = forwardRef<
'Use the full-width table to scan prices, then select a row to edit it here.' 'Use the full-width table to scan prices, then select a row to edit it here.'
)} )}
</p> </p>
{filterMode !== 'unset' && (
<Button variant='outline' onClick={handleAdd}> <Button variant='outline' onClick={handleAdd}>
<Plus data-icon='inline-start' /> <Plus data-icon='inline-start' />
{t('Add model')} {t('Add model')}
</Button> </Button>
)}
</div> </div>
)} )}
</div> </div>
...@@ -790,6 +831,8 @@ export const ModelRatioVisualEditor = memo( ...@@ -790,6 +831,8 @@ export const ModelRatioVisualEditor = memo(
prevProps.audioCompletionRatio === nextProps.audioCompletionRatio && prevProps.audioCompletionRatio === nextProps.audioCompletionRatio &&
prevProps.billingMode === nextProps.billingMode && prevProps.billingMode === nextProps.billingMode &&
prevProps.billingExpr === nextProps.billingExpr && prevProps.billingExpr === nextProps.billingExpr &&
prevProps.candidateModelNames === nextProps.candidateModelNames &&
prevProps.filterMode === nextProps.filterMode &&
prevProps.onChange === nextProps.onChange && prevProps.onChange === nextProps.onChange &&
prevProps.onSave === nextProps.onSave && prevProps.onSave === nextProps.onSave &&
prevProps.isSaving === nextProps.isSaving prevProps.isSaving === nextProps.isSaving
......
...@@ -136,7 +136,12 @@ const createGroupSchema = (t: Translate) => ...@@ -136,7 +136,12 @@ const createGroupSchema = (t: Translate) =>
type ModelFormValues = z.infer<ReturnType<typeof createModelSchema>> type ModelFormValues = z.infer<ReturnType<typeof createModelSchema>>
type GroupFormValues = z.infer<ReturnType<typeof createGroupSchema>> type GroupFormValues = z.infer<ReturnType<typeof createGroupSchema>>
type RatioTabId = 'models' | 'groups' | 'tool-prices' | 'upstream-sync' type RatioTabId =
| 'models'
| 'unset-models'
| 'groups'
| 'tool-prices'
| 'upstream-sync'
type RatioSettingsCardProps = { type RatioSettingsCardProps = {
modelDefaults: ModelFormValues modelDefaults: ModelFormValues
...@@ -392,6 +397,7 @@ export function RatioSettingsCard({ ...@@ -392,6 +397,7 @@ export function RatioSettingsCard({
const tabLabels: Record<RatioTabId, string> = { const tabLabels: Record<RatioTabId, string> = {
models: 'Model prices', models: 'Model prices',
'unset-models': 'Unset price models',
groups: 'Group ratios', groups: 'Group ratios',
'tool-prices': 'Tool prices', 'tool-prices': 'Tool prices',
'upstream-sync': 'Upstream price sync', 'upstream-sync': 'Upstream price sync',
...@@ -402,11 +408,12 @@ export function RatioSettingsCard({ ...@@ -402,11 +408,12 @@ export function RatioSettingsCard({
2: 'grid-cols-2', 2: 'grid-cols-2',
3: 'grid-cols-3', 3: 'grid-cols-3',
4: 'grid-cols-4', 4: 'grid-cols-4',
5: 'grid-cols-5',
}[visibleTabs.length] ?? 'grid-cols-4' }[visibleTabs.length] ?? 'grid-cols-4'
const defaultTab = visibleTabs[0] ?? 'models' const defaultTab = visibleTabs[0] ?? 'models'
const renderTabContent = (tab: RatioTabId) => { const renderTabContent = (tab: RatioTabId) => {
if (tab === 'models') { if (tab === 'models' || tab === 'unset-models') {
return ( return (
<ModelRatioForm <ModelRatioForm
form={modelForm} form={modelForm}
...@@ -415,6 +422,7 @@ export function RatioSettingsCard({ ...@@ -415,6 +422,7 @@ export function RatioSettingsCard({
onReset={handleResetRatios} onReset={handleResetRatios}
isSaving={updateOption.isPending} isSaving={updateOption.isPending}
isResetting={resetMutation.isPending} isResetting={resetMutation.isPending}
variant={tab === 'unset-models' ? 'unset' : 'default'}
/> />
) )
} }
......
...@@ -2925,6 +2925,7 @@ ...@@ -2925,6 +2925,7 @@
"No models to add": "No models to add", "No models to add": "No models to add",
"No models to copy": "No models to copy", "No models to copy": "No models to copy",
"No models to remove": "No models to remove", "No models to remove": "No models to remove",
"No models with unset prices": "No models with unset prices",
"No new models to add": "No new models to add", "No new models to add": "No new models to add",
"No new models yet": "No new models yet", "No new models yet": "No new models yet",
"No nodes": "No nodes", "No nodes": "No nodes",
...@@ -4764,6 +4765,7 @@ ...@@ -4764,6 +4765,7 @@
"Unlimited Quota": "Unlimited Quota", "Unlimited Quota": "Unlimited Quota",
"Unsaved changes": "Unsaved changes", "Unsaved changes": "Unsaved changes",
"Unset price": "Unset price", "Unset price": "Unset price",
"Unset price models": "Unset price models",
"Until": "Until", "Until": "Until",
"Untitled": "Untitled", "Untitled": "Untitled",
"Untrusted upstream data:": "Untrusted upstream data:", "Untrusted upstream data:": "Untrusted upstream data:",
......
...@@ -2925,6 +2925,7 @@ ...@@ -2925,6 +2925,7 @@
"No models to add": "Aucun modèle à ajouter", "No models to add": "Aucun modèle à ajouter",
"No models to copy": "Aucun modèle à copier", "No models to copy": "Aucun modèle à copier",
"No models to remove": "Aucun modèle à supprimer", "No models to remove": "Aucun modèle à supprimer",
"No models with unset prices": "Aucun modèle sans prix",
"No new models to add": "Aucun nouveau modèle à ajouter", "No new models to add": "Aucun nouveau modèle à ajouter",
"No new models yet": "Pas encore de nouveaux modèles", "No new models yet": "Pas encore de nouveaux modèles",
"No nodes": "Aucun nœud", "No nodes": "Aucun nœud",
...@@ -4764,6 +4765,7 @@ ...@@ -4764,6 +4765,7 @@
"Unlimited Quota": "Quota illimité", "Unlimited Quota": "Quota illimité",
"Unsaved changes": "Modifications non enregistrées", "Unsaved changes": "Modifications non enregistrées",
"Unset price": "Prix non défini", "Unset price": "Prix non défini",
"Unset price models": "Modèles sans prix",
"Until": "Jusqu'au", "Until": "Jusqu'au",
"Untitled": "Sans titre", "Untitled": "Sans titre",
"Untrusted upstream data:": "Données amont non fiables :", "Untrusted upstream data:": "Données amont non fiables :",
......
...@@ -2925,6 +2925,7 @@ ...@@ -2925,6 +2925,7 @@
"No models to add": "追加するモデルがありません", "No models to add": "追加するモデルがありません",
"No models to copy": "コピーするモデルがありません", "No models to copy": "コピーするモデルがありません",
"No models to remove": "削除するモデルがありません", "No models to remove": "削除するモデルがありません",
"No models with unset prices": "価格未設定のモデルはありません",
"No new models to add": "追加する新しいモデルはありません", "No new models to add": "追加する新しいモデルはありません",
"No new models yet": "新しいモデルはまだありません", "No new models yet": "新しいモデルはまだありません",
"No nodes": "ノードなし", "No nodes": "ノードなし",
...@@ -4764,6 +4765,7 @@ ...@@ -4764,6 +4765,7 @@
"Unlimited Quota": "無制限のクォータ", "Unlimited Quota": "無制限のクォータ",
"Unsaved changes": "未保存の変更", "Unsaved changes": "未保存の変更",
"Unset price": "価格未設定", "Unset price": "価格未設定",
"Unset price models": "価格が未設定のモデル",
"Until": "まで", "Until": "まで",
"Untitled": "無題", "Untitled": "無題",
"Untrusted upstream data:": "信頼されていないアップストリームデータ:", "Untrusted upstream data:": "信頼されていないアップストリームデータ:",
......
...@@ -2925,6 +2925,7 @@ ...@@ -2925,6 +2925,7 @@
"No models to add": "Нет моделей для добавления", "No models to add": "Нет моделей для добавления",
"No models to copy": "Нет моделей для копирования", "No models to copy": "Нет моделей для копирования",
"No models to remove": "Нет моделей для удаления", "No models to remove": "Нет моделей для удаления",
"No models with unset prices": "Нет моделей без цены",
"No new models to add": "Нет новых моделей для добавления", "No new models to add": "Нет новых моделей для добавления",
"No new models yet": "Новых моделей пока нет", "No new models yet": "Новых моделей пока нет",
"No nodes": "Нет узлов", "No nodes": "Нет узлов",
...@@ -4764,6 +4765,7 @@ ...@@ -4764,6 +4765,7 @@
"Unlimited Quota": "Неограниченная квота", "Unlimited Quota": "Неограниченная квота",
"Unsaved changes": "Несохранённые изменения", "Unsaved changes": "Несохранённые изменения",
"Unset price": "Цена не задана", "Unset price": "Цена не задана",
"Unset price models": "Модели с неустановленной ценой",
"Until": "До", "Until": "До",
"Untitled": "Без названия", "Untitled": "Без названия",
"Untrusted upstream data:": "Недоверенные вышестоящие данные:", "Untrusted upstream data:": "Недоверенные вышестоящие данные:",
......
...@@ -2925,6 +2925,7 @@ ...@@ -2925,6 +2925,7 @@
"No models to add": "Không có mô hình để thêm", "No models to add": "Không có mô hình để thêm",
"No models to copy": "Không có mô hình nào để sao chép", "No models to copy": "Không có mô hình nào để sao chép",
"No models to remove": "Không có mô hình để xóa", "No models to remove": "Không có mô hình để xóa",
"No models with unset prices": "Không có mô hình chưa thiết lập giá",
"No new models to add": "Không có mô hình mới để thêm", "No new models to add": "Không có mô hình mới để thêm",
"No new models yet": "Chưa có mô hình mới", "No new models yet": "Chưa có mô hình mới",
"No nodes": "Không có nút", "No nodes": "Không có nút",
...@@ -4764,6 +4765,7 @@ ...@@ -4764,6 +4765,7 @@
"Unlimited Quota": "Hạn mức không giới hạn", "Unlimited Quota": "Hạn mức không giới hạn",
"Unsaved changes": "Thay đổi chưa được lưu", "Unsaved changes": "Thay đổi chưa được lưu",
"Unset price": "Chưa đặt giá", "Unset price": "Chưa đặt giá",
"Unset price models": "Mô hình chưa thiết lập giá",
"Until": "Cho đến", "Until": "Cho đến",
"Untitled": "Không có tiêu đề", "Untitled": "Không có tiêu đề",
"Untrusted upstream data:": "Dữ liệu nguồn không đáng tin cậy:", "Untrusted upstream data:": "Dữ liệu nguồn không đáng tin cậy:",
......
...@@ -2925,6 +2925,7 @@ ...@@ -2925,6 +2925,7 @@
"No models to add": "無待新增模型", "No models to add": "無待新增模型",
"No models to copy": "沒有模型可複製", "No models to copy": "沒有模型可複製",
"No models to remove": "無待刪除模型", "No models to remove": "無待刪除模型",
"No models with unset prices": "沒有未設定定價的模型",
"No new models to add": "沒有新模型可新增", "No new models to add": "沒有新模型可新增",
"No new models yet": "暫無新模型", "No new models yet": "暫無新模型",
"No nodes": "無節點", "No nodes": "無節點",
...@@ -4764,6 +4765,7 @@ ...@@ -4764,6 +4765,7 @@
"Unlimited Quota": "無限配額", "Unlimited Quota": "無限配額",
"Unsaved changes": "未儲存的變更", "Unsaved changes": "未儲存的變更",
"Unset price": "未設定價格", "Unset price": "未設定價格",
"Unset price models": "未設定價格模型",
"Until": "至", "Until": "至",
"Untitled": "未命名", "Untitled": "未命名",
"Untrusted upstream data:": "不受信任的上游數據:", "Untrusted upstream data:": "不受信任的上游數據:",
......
...@@ -2925,6 +2925,7 @@ ...@@ -2925,6 +2925,7 @@
"No models to add": "无待新增模型", "No models to add": "无待新增模型",
"No models to copy": "没有模型可复制", "No models to copy": "没有模型可复制",
"No models to remove": "无待删除模型", "No models to remove": "无待删除模型",
"No models with unset prices": "没有未设置定价的模型",
"No new models to add": "没有新模型可添加", "No new models to add": "没有新模型可添加",
"No new models yet": "暂无新模型", "No new models yet": "暂无新模型",
"No nodes": "无节点", "No nodes": "无节点",
...@@ -4764,6 +4765,7 @@ ...@@ -4764,6 +4765,7 @@
"Unlimited Quota": "无限配额", "Unlimited Quota": "无限配额",
"Unsaved changes": "未保存的更改", "Unsaved changes": "未保存的更改",
"Unset price": "未设置价格", "Unset price": "未设置价格",
"Unset price models": "未设置价格模型",
"Until": "至", "Until": "至",
"Untitled": "未命名", "Untitled": "未命名",
"Untrusted upstream data:": "不受信任的上游数据:", "Untrusted upstream data:": "不受信任的上游数据:",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment