feat: implement AI gateway phase one runtime
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Calculator } from 'lucide-react';
|
||||
|
||||
export function PricingInlineField(props: {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="pricingInlineField">
|
||||
<span>{props.label}</span>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PricingReadonlyField(props: {
|
||||
label: string;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="pricingInlineField pricingInlineReadonly">
|
||||
<span>{props.label}</span>
|
||||
<strong>{props.value}</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function unitLabel(unit: string | undefined) {
|
||||
const labels: Record<string, string> = {
|
||||
image: '每张图片',
|
||||
'5s': '每 5 秒',
|
||||
second: '每秒',
|
||||
item: '每次',
|
||||
'1k_tokens': '每 1K Token',
|
||||
character_1k: '每 1K 字符',
|
||||
};
|
||||
return labels[unit ?? ''] ?? unit ?? '-';
|
||||
}
|
||||
|
||||
export function calculatorLabel(calculatorType: string | undefined) {
|
||||
const labels: Record<string, string> = {
|
||||
token_usage: '按 Token 用量',
|
||||
unit_weight: '按数量和参数',
|
||||
duration_weight: '按时长和参数',
|
||||
formula: '按公式',
|
||||
};
|
||||
return labels[calculatorType ?? ''] ?? calculatorType ?? '-';
|
||||
}
|
||||
|
||||
export function PricingFormulaNote(props: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="pricingFormulaBox">
|
||||
<Calculator size={16} />
|
||||
<div>
|
||||
<strong>计费公式</strong>
|
||||
<p>{props.children}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Calculator, Plus, Trash2 } from 'lucide-react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import type { PricingRuleInput } from '@easyai-ai-gateway/contracts';
|
||||
import { Badge, Button, Input, Label, Select } from '../../components/ui';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { calculatorLabel, PricingFormulaNote, PricingInlineField, PricingReadonlyField, unitLabel } from './PricingEditorControls';
|
||||
|
||||
type RecordValue = Record<string, unknown>;
|
||||
type ModeKey = 'text' | 'image' | 'video' | 'digitalHuman' | 'speech' | 'music';
|
||||
type ModeKey = 'text' | 'image' | 'video';
|
||||
type ParameterGroup = {
|
||||
key: string;
|
||||
title: string;
|
||||
defaults: RecordValue;
|
||||
labels?: Record<string, string>;
|
||||
locked?: boolean;
|
||||
};
|
||||
|
||||
type ModeDefinition = {
|
||||
key: ModeKey;
|
||||
@@ -13,88 +21,52 @@ type ModeDefinition = {
|
||||
formula: string;
|
||||
match: (rule: PricingRuleInput) => boolean;
|
||||
templates: (currency: string) => PricingRuleInput[];
|
||||
weightGroups: Array<{ key: string; title: string; defaults: RecordValue }>;
|
||||
parameterGroups: ParameterGroup[];
|
||||
};
|
||||
|
||||
const currencies = ['resource', 'credit', 'cny', 'usd'];
|
||||
const calculators = ['token_usage', 'unit_weight', 'duration_weight', 'formula'];
|
||||
const statuses = ['active', 'deprecated', 'hidden'];
|
||||
|
||||
const modeDefinitions: ModeDefinition[] = [
|
||||
{
|
||||
key: 'text',
|
||||
label: '大语言模型',
|
||||
formula: '扣费 = 输入 Token 单价 × 输入 Token 数 / 1000 + 输出 Token 单价 × 输出 Token 数 / 1000。',
|
||||
label: '文本',
|
||||
formula: '一次对话扣费 = 输入 Token / 1000 × 输入扣费单价 + 输出 Token / 1000 × 输出扣费单价。',
|
||||
match: (rule) => rule.resourceType.startsWith('text_'),
|
||||
templates: (currency) => [
|
||||
createRule('text_input_tokens', '文本输入 Token', 'text_input', '1k_tokens', 0.01, currency, 'token_usage', 'ceil(input_tokens / 1000) * base_price', { meter: 'input_tokens' }, {}, { metrics: ['input_tokens'], unitScale: 1000 }),
|
||||
createRule('text_output_tokens', '文本输出 Token', 'text_output', '1k_tokens', 0.03, currency, 'token_usage', 'ceil(output_tokens / 1000) * base_price', { meter: 'output_tokens' }, {}, { metrics: ['output_tokens'], unitScale: 1000 }),
|
||||
],
|
||||
weightGroups: [],
|
||||
templates: (currency) => [createTextRule(currency, 0.01, 0.03)],
|
||||
parameterGroups: [],
|
||||
},
|
||||
{
|
||||
key: 'image',
|
||||
label: '图像生成',
|
||||
formula: '扣费 = 基础单价 × 基础权重 × 动态参数权重,例如 2K 或高清质量会按对应倍数计费。',
|
||||
label: '图像',
|
||||
formula: '扣费 = 基础单价 × 数量 × 分辨率参数 × 图像质量参数。图像质量按 OpenAI GPT Image 的 low / medium / high 三档配置。',
|
||||
match: (rule) => rule.resourceType === 'image' || rule.resourceType === 'image_edit',
|
||||
templates: (currency) => [
|
||||
createRule('image_generation', '图像生成', 'image', 'image', 10, currency, 'unit_weight', 'count * base_price * resolution_weight * quality_weight * mode_weight', { meter: 'count', base: 1 }, {
|
||||
resolutionWeights: { '512x512': 0.5, '1024x1024': 1, '2K': 1.5, '4K': 2 },
|
||||
qualityWeights: { standard: 1, hd: 1.5 },
|
||||
modeWeights: { generation: 1 },
|
||||
}, { dimensions: ['count', 'resolution', 'quality', 'mode'], defaults: { count: 1, resolution: '1024x1024', quality: 'standard', mode: 'generation' } }),
|
||||
],
|
||||
weightGroups: [
|
||||
{ key: 'resolutionWeights', title: '分辨率', defaults: { '512x512': 0.5, '1024x1024': 1, '2K': 1.5, '4K': 2 } },
|
||||
{ key: 'qualityWeights', title: '图像质量', defaults: { standard: 1, hd: 1.5 } },
|
||||
{ key: 'referenceImageWeights', title: '参考图数量', defaults: { single: 1, multiple: 1.3 } },
|
||||
templates: (currency) => [createRule('image', '图像', 'image', 'image', 10, currency, 'unit_weight', 'count * base_price * resolution_factor * quality_factor', {
|
||||
resolutionFactors: { '1K': 1, '2K': 1.5, '3K': 1.75, '4K': 2, '8K': 4 },
|
||||
qualityFactors: { low: 0.5, medium: 1, high: 1.5 },
|
||||
}, { dimensions: ['count', 'resolution', 'quality'], defaults: { count: 1, resolution: '1K', quality: 'medium' } })],
|
||||
parameterGroups: [
|
||||
{ key: 'resolutionFactors', title: '分辨率', defaults: { '1K': 1, '2K': 1.5, '3K': 1.75, '4K': 2, '8K': 4 }, locked: true },
|
||||
{ key: 'qualityFactors', title: '图像质量', defaults: { low: 0.5, medium: 1, high: 1.5 }, labels: { low: '低质量 (low)', medium: '标准质量 (medium)', high: '高质量 (high)' }, locked: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
label: '视频生成',
|
||||
formula: '扣费 = 基础单价 × 基础权重 × 动态参数权重。例如生成 1080p 且带音频时,会叠加分辨率和音频权重。',
|
||||
label: '视频',
|
||||
formula: '扣费 = 基础单价 × 生成时长单位 × 数量 × 分辨率、音频、参考视频、音色等计费参数。',
|
||||
match: (rule) => rule.resourceType === 'video',
|
||||
templates: (currency) => [
|
||||
createRule('video_generation', '视频生成', 'video', '5s', 100, currency, 'duration_weight', 'count * ceil(duration_seconds / 5) * base_price * resolution_weight * audio_weight * reference_video_weight * voice_specified_weight', { meter: 'duration', unitSeconds: 5, base: 1 }, {
|
||||
createRule('video', '视频', 'video', '5s', 100, currency, 'duration_weight', 'count * ceil(duration_seconds / 5) * base_price * resolution_factor * audio_factor * reference_video_factor * voice_specified_factor', {
|
||||
resolutionWeights: { '480p': 0.75, '720p': 1, '1080p': 1.5, '2160p': 2 },
|
||||
audioWeights: { true: 2, false: 1 },
|
||||
referenceVideoWeights: { true: 1.5, false: 1 },
|
||||
voiceSpecifiedWeights: { true: 1.2, false: 1 },
|
||||
}, { dimensions: ['count', 'duration_seconds', 'resolution', 'audio', 'reference_video', 'voice_specified'], defaults: { count: 1, duration_seconds: 5, resolution: '720p', audio: false, reference_video: false, voice_specified: false } }),
|
||||
],
|
||||
weightGroups: [
|
||||
{ key: 'resolutionWeights', title: '分辨率', defaults: { '480p': 0.75, '720p': 1, '1080p': 1.5, '2160p': 2 } },
|
||||
{ key: 'audioWeights', title: '是否生成音频', defaults: { true: 2, false: 1 } },
|
||||
{ key: 'referenceVideoWeights', title: '是否含有参考视频', defaults: { true: 1.5, false: 1 } },
|
||||
{ key: 'voiceSpecifiedWeights', title: '是否指定音色', defaults: { true: 1.2, false: 1 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'digitalHuman',
|
||||
label: '数字人生成',
|
||||
formula: '扣费 = 基础单价 × 生成时长 × 分辨率、音频驱动等动态参数权重。',
|
||||
match: (rule) => rule.resourceType === 'digital_human',
|
||||
templates: (currency) => [createRule('digital_human_generation', '数字人生成', 'digital_human', 'second', 2, currency, 'duration_weight', 'duration_seconds * base_price * resolution_weight', { meter: 'duration_seconds', base: 1 }, { resolutionWeights: { '720p': 1, '1080p': 1.5 } }, { dimensions: ['duration_seconds', 'resolution'], defaults: { duration_seconds: 10, resolution: '720p' } })],
|
||||
weightGroups: [{ key: 'resolutionWeights', title: '分辨率', defaults: { '720p': 1, '1080p': 1.5 } }],
|
||||
},
|
||||
{
|
||||
key: 'speech',
|
||||
label: '语音合成',
|
||||
formula: '扣费 = 基础单价 × 字符数 / 1000 × 声音或质量权重。',
|
||||
match: (rule) => rule.resourceType === 'audio',
|
||||
templates: (currency) => [createRule('speech_generation', '语音合成', 'audio', 'character_1k', 1, currency, 'unit_weight', 'ceil(characters / 1000) * base_price * voice_weight', { meter: 'characters', base: 1 }, { voiceWeights: { standard: 1, premium: 1.5 } }, { dimensions: ['characters', 'voice'], defaults: { voice: 'standard' } })],
|
||||
weightGroups: [{ key: 'voiceWeights', title: '音色', defaults: { standard: 1, premium: 1.5 } }],
|
||||
},
|
||||
{
|
||||
key: 'music',
|
||||
label: '音乐生成',
|
||||
formula: '扣费 = 基础单价 × 生成数量 × 时长或质量权重。',
|
||||
match: (rule) => rule.resourceType === 'music',
|
||||
templates: (currency) => [createRule('music_generation', '音乐生成', 'music', 'item', 20, currency, 'unit_weight', 'count * base_price * duration_weight * quality_weight', { meter: 'count', base: 1 }, { durationWeights: { short: 1, long: 1.8 }, qualityWeights: { standard: 1, high: 1.5 } }, { dimensions: ['count', 'duration', 'quality'], defaults: { count: 1, duration: 'short', quality: 'standard' } })],
|
||||
weightGroups: [
|
||||
{ key: 'durationWeights', title: '时长', defaults: { short: 1, long: 1.8 } },
|
||||
{ key: 'qualityWeights', title: '质量', defaults: { standard: 1, high: 1.5 } },
|
||||
parameterGroups: [
|
||||
{ key: 'resolutionWeights', title: '分辨率', defaults: { '480p': 0.75, '720p': 1, '1080p': 1.5, '2160p': 2 }, locked: true },
|
||||
{ key: 'audioWeights', title: '生成声音', defaults: { true: 2, false: 1 }, labels: { true: '生成声音', false: '不生成声音' }, locked: true },
|
||||
{ key: 'referenceVideoWeights', title: '参考视频', defaults: { true: 1.5, false: 1 }, labels: { true: '有参考视频', false: '无参考视频' }, locked: true },
|
||||
{ key: 'voiceSpecifiedWeights', title: '指定音色', defaults: { true: 1.2, false: 1 }, labels: { true: '指定音色', false: '不指定音色' }, locked: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -104,27 +76,26 @@ export function PricingRuleVisualEditor(props: {
|
||||
rules: PricingRuleInput[];
|
||||
onChange: (rules: PricingRuleInput[]) => void;
|
||||
}) {
|
||||
const [activeMode, setActiveMode] = useState<ModeKey>('video');
|
||||
const [activeMode, setActiveMode] = useState<ModeKey>('text');
|
||||
const mode = modeDefinitions.find((item) => item.key === activeMode) ?? modeDefinitions[0];
|
||||
const activeRules = useMemo(() => props.rules.filter(mode.match), [mode, props.rules]);
|
||||
const rawActiveRules = useMemo(() => props.rules.filter(mode.match), [mode, props.rules]);
|
||||
const activeRules = useMemo(() => {
|
||||
if (mode.key === 'text' && rawActiveRules.length) return [completeRule(mergeTextRules(rawActiveRules, props.currency), mode)];
|
||||
if (mode.key === 'image' && rawActiveRules.length) return [completeRule(mergeTemplateRules(mode.templates(props.currency), rawActiveRules)[0], mode)];
|
||||
return rawActiveRules.map((rule) => completeRule(rule, mode));
|
||||
}, [mode.key, props.currency, rawActiveRules]);
|
||||
|
||||
function replaceModeRules(nextRules: PricingRuleInput[]) {
|
||||
props.onChange([...props.rules.filter((rule) => !mode.match(rule)), ...nextRules]);
|
||||
}
|
||||
|
||||
function completeMissing() {
|
||||
const existing = activeRules.length ? activeRules : mode.templates(props.currency);
|
||||
replaceModeRules(existing.map((rule) => completeRule(rule, mode)));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="pricingModeEditor spanTwo">
|
||||
<div className="pricingModeHeader">
|
||||
<div>
|
||||
<strong>计费模式</strong>
|
||||
<span>选择能力后维护基础价格、计费公式和动态权重。</span>
|
||||
<span>选择能力后维护基础价格、计费公式和计费参数。</span>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={completeMissing}>一键补全缺失</Button>
|
||||
</div>
|
||||
<div className="pricingModeTabs">
|
||||
{modeDefinitions.map((item) => (
|
||||
@@ -152,6 +123,10 @@ export function PricingRuleVisualEditor(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function createDefaultPricingRules(currency: string): PricingRuleInput[] {
|
||||
return modeDefinitions.flatMap((mode) => mode.templates(currency).map((rule) => completeRule(rule, mode)));
|
||||
}
|
||||
|
||||
function ModeRuleEditor(props: {
|
||||
mode: ModeDefinition;
|
||||
rule: PricingRuleInput;
|
||||
@@ -159,19 +134,17 @@ function ModeRuleEditor(props: {
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const rule = props.rule;
|
||||
const baseWeight = Number((rule.baseWeight ?? {}).base ?? 1);
|
||||
const formula = String((rule.formulaConfig ?? {}).formula ?? props.mode.formula);
|
||||
|
||||
function patch(patchValue: Partial<PricingRuleInput>) {
|
||||
props.onChange({ ...rule, ...patchValue });
|
||||
props.onChange(completeRule({ ...rule, ...patchValue }, props.mode));
|
||||
}
|
||||
|
||||
function patchDynamicWeight(key: string, value: RecordValue) {
|
||||
patch({ dynamicWeight: { ...(rule.dynamicWeight ?? {}), [key]: value } });
|
||||
}
|
||||
|
||||
function addCustomGroup() {
|
||||
patchDynamicWeight(nextKey(rule.dynamicWeight ?? {}, 'customWeights'), {});
|
||||
if (props.mode.key === 'text') {
|
||||
return <TextRuleEditor formula={props.mode.formula} rule={rule} onChange={props.onChange} onDelete={props.onDelete} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -185,40 +158,33 @@ function ModeRuleEditor(props: {
|
||||
<Button type="button" variant="ghost" size="sm" onClick={props.onDelete}><Trash2 size={14} />删除</Button>
|
||||
</header>
|
||||
|
||||
<div className="pricingModeBaseRows">
|
||||
<Label>规则 Key<Input value={rule.ruleKey ?? ''} onChange={(event) => patch({ ruleKey: event.target.value })} /></Label>
|
||||
<div className="pricingModeBaseRows compact">
|
||||
<Label>展示名称<Input value={rule.displayName ?? ''} onChange={(event) => patch({ displayName: event.target.value })} /></Label>
|
||||
<Label>资源类型<Input value={rule.resourceType} onChange={(event) => patch({ resourceType: event.target.value })} /></Label>
|
||||
<Label>计算方式<Select value={rule.calculatorType ?? 'unit_weight'} onChange={(event) => patch({ calculatorType: event.target.value })}>{calculators.map((item) => <option key={item} value={item}>{item}</option>)}</Select></Label>
|
||||
<Label>货币<Select value={rule.currency ?? 'resource'} onChange={(event) => patch({ currency: event.target.value })}>{currencies.map((item) => <option key={item} value={item}>{item}</option>)}</Select></Label>
|
||||
<Label>状态<Select value={rule.status ?? 'active'} onChange={(event) => patch({ status: event.target.value })}>{statuses.map((item) => <option key={item} value={item}>{item}</option>)}</Select></Label>
|
||||
</div>
|
||||
|
||||
<div className="pricingModeInlineRows">
|
||||
<div className="pricingModeInlineInput"><span>基础单价/{rule.unit}</span><Input min="0" step="0.0001" type="number" value={rule.basePrice} onChange={(event) => patch({ basePrice: Number(event.target.value) })} /></div>
|
||||
<div className="pricingModeInlineInput"><span>基础权重</span><Input min="0" step="0.01" type="number" value={baseWeight} onChange={(event) => patch({ baseWeight: { ...(rule.baseWeight ?? {}), base: Number(event.target.value) } })} /></div>
|
||||
<div className="pricingModeInlineInput"><span>单位</span><Input value={rule.unit} onChange={(event) => patch({ unit: event.target.value })} /></div>
|
||||
<PricingInlineField label={`基础单价/${unitLabel(rule.unit)}`}>
|
||||
<Input min="0" step="0.0001" type="number" value={rule.basePrice} onChange={(event) => patch({ basePrice: Number(event.target.value) })} />
|
||||
</PricingInlineField>
|
||||
<PricingReadonlyField label="计价单位" value={unitLabel(rule.unit)} />
|
||||
<PricingReadonlyField label="计算方式" value={calculatorLabel(rule.calculatorType)} />
|
||||
</div>
|
||||
|
||||
<div className="pricingFormulaBox">
|
||||
<Calculator size={16} />
|
||||
<div>
|
||||
<strong>计费公式</strong>
|
||||
<Input value={formula} onChange={(event) => patch({ formulaConfig: { ...(rule.formulaConfig ?? {}), formula: event.target.value } })} />
|
||||
<p>{props.mode.formula}</p>
|
||||
</div>
|
||||
</div>
|
||||
<PricingFormulaNote>{props.mode.formula}</PricingFormulaNote>
|
||||
|
||||
<div className="pricingWeightStack">
|
||||
{props.mode.weightGroups.map((group) => (
|
||||
{props.mode.parameterGroups.map((group) => (
|
||||
<WeightTable
|
||||
key={group.key}
|
||||
title={group.title}
|
||||
value={readGroup(rule.dynamicWeight, group)}
|
||||
labels={group.labels}
|
||||
locked={group.locked}
|
||||
onChange={(value) => patchDynamicWeight(group.key, value)}
|
||||
/>
|
||||
))}
|
||||
{Object.entries(rule.dynamicWeight ?? {}).filter(([key]) => !props.mode.weightGroups.some((group) => group.key === key)).map(([key, value]) => (
|
||||
{Object.entries(rule.dynamicWeight ?? {}).filter(([key]) => !props.mode.parameterGroups.some((group) => group.key === key)).map(([key, value]) => (
|
||||
<WeightTable key={key} editableTitle title={key} value={isPlainObject(value) ? value as RecordValue : { value }} onChange={(nextValue, nextTitle) => {
|
||||
const dynamicWeight = { ...(rule.dynamicWeight ?? {}) };
|
||||
delete dynamicWeight[key];
|
||||
@@ -227,14 +193,74 @@ function ModeRuleEditor(props: {
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
<Button className="pricingAddWeightButton" type="button" variant="outline" onClick={addCustomGroup}><Plus size={15} />添加自定义计费权重</Button>
|
||||
function TextRuleEditor(props: {
|
||||
formula: string;
|
||||
rule: PricingRuleInput;
|
||||
onChange: (rule: PricingRuleInput) => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const prices = textPrices(props.rule);
|
||||
|
||||
function updatePrices(inputTokenPrice: number, outputTokenPrice: number) {
|
||||
props.onChange({
|
||||
...props.rule,
|
||||
basePrice: inputTokenPrice,
|
||||
displayName: props.rule.displayName || '文本',
|
||||
resourceType: 'text_total',
|
||||
ruleKey: 'text',
|
||||
unit: '1k_tokens',
|
||||
formulaConfig: {
|
||||
...(props.rule.formulaConfig ?? {}),
|
||||
formula: textFormula,
|
||||
inputTokenPrice,
|
||||
outputTokenPrice,
|
||||
},
|
||||
dimensionSchema: {
|
||||
...(props.rule.dimensionSchema ?? {}),
|
||||
metrics: ['input_tokens', 'output_tokens'],
|
||||
unitScale: 1000,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="pricingModeRule pricingTextRule">
|
||||
<header>
|
||||
<div>
|
||||
<Badge variant="secondary">text_total</Badge>
|
||||
<strong>{props.rule.displayName || '文本'}</strong>
|
||||
<span>text</span>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={props.onDelete}><Trash2 size={14} />删除</Button>
|
||||
</header>
|
||||
|
||||
<div className="pricingModeBaseRows compact">
|
||||
<Label>展示名称<Input value={props.rule.displayName ?? '文本'} onChange={(event) => props.onChange({ ...props.rule, displayName: event.target.value, ruleKey: 'text' })} /></Label>
|
||||
<Label>状态<Select value={props.rule.status ?? 'active'} onChange={(event) => props.onChange({ ...props.rule, status: event.target.value })}>{statuses.map((item) => <option key={item} value={item}>{item}</option>)}</Select></Label>
|
||||
</div>
|
||||
|
||||
<div className="pricingTextPriceGrid">
|
||||
<PricingInlineField label="输入单价/1K tokens">
|
||||
<Input min="0" step="0.0001" type="number" value={prices.inputTokenPrice} onChange={(event) => updatePrices(Number(event.target.value), prices.outputTokenPrice)} />
|
||||
</PricingInlineField>
|
||||
<PricingInlineField label="输出单价/1K tokens">
|
||||
<Input min="0" step="0.0001" type="number" value={prices.outputTokenPrice} onChange={(event) => updatePrices(prices.inputTokenPrice, Number(event.target.value))} />
|
||||
</PricingInlineField>
|
||||
</div>
|
||||
|
||||
<PricingFormulaNote>{props.formula}</PricingFormulaNote>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function WeightTable(props: {
|
||||
editableTitle?: boolean;
|
||||
labels?: Record<string, string>;
|
||||
locked?: boolean;
|
||||
title: string;
|
||||
value: RecordValue;
|
||||
onChange: (value: RecordValue, title?: string) => void;
|
||||
@@ -256,13 +282,17 @@ function WeightTable(props: {
|
||||
</header>
|
||||
<div className="pricingWeightRows">
|
||||
{rows.map(([key, value], index) => (
|
||||
<div className="pricingWeightRow" key={`${key}-${index}`}>
|
||||
<Input value={key} onChange={(event) => updateRows(rows.map((row, rowIndex) => (rowIndex === index ? [event.target.value, value] : row)))} />
|
||||
<div className={cn('pricingWeightRow', props.locked && 'locked')} key={`${key}-${index}`}>
|
||||
{props.locked ? (
|
||||
<span className="pricingFactorLabel">{props.labels?.[key] ?? key}</span>
|
||||
) : (
|
||||
<Input value={key} onChange={(event) => updateRows(rows.map((row, rowIndex) => (rowIndex === index ? [event.target.value, value] : row)))} />
|
||||
)}
|
||||
<Input value={scalarToString(value)} onChange={(event) => updateRows(rows.map((row, rowIndex) => (rowIndex === index ? [key, parseScalar(event.target.value)] : row)))} />
|
||||
<Button type="button" variant="ghost" size="icon" onClick={() => updateRows(rows.filter((_, rowIndex) => rowIndex !== index))}><Trash2 size={14} /></Button>
|
||||
{!props.locked && <Button type="button" variant="ghost" size="icon" onClick={() => updateRows(rows.filter((_, rowIndex) => rowIndex !== index))}><Trash2 size={14} /></Button>}
|
||||
</div>
|
||||
))}
|
||||
<Button className="pricingInlineAdd" type="button" variant="outline" size="sm" onClick={() => updateRows([...rows, [nextKey(props.value, 'option'), 1]])}><Plus size={14} />添加</Button>
|
||||
{!props.locked && <Button className="pricingInlineAdd" type="button" variant="outline" size="sm" onClick={() => updateRows([...rows, [nextKey(props.value, 'option'), 1]])}><Plus size={14} />添加</Button>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -283,20 +313,130 @@ export function KeyValueEditor(props: {
|
||||
}
|
||||
|
||||
function completeRule(rule: PricingRuleInput, mode: ModeDefinition): PricingRuleInput {
|
||||
const template = mode.templates(rule.currency ?? 'resource')[0] ?? rule;
|
||||
return {
|
||||
...template,
|
||||
...rule,
|
||||
dynamicWeight: {
|
||||
...Object.fromEntries(mode.weightGroups.map((group) => [group.key, { ...group.defaults, ...(isPlainObject(rule.dynamicWeight?.[group.key]) ? rule.dynamicWeight?.[group.key] as RecordValue : {}) }])),
|
||||
...(rule.dynamicWeight ?? {}),
|
||||
ruleKey: mode.key,
|
||||
displayName: normalizeDisplayName(rule.displayName || template.displayName, mode),
|
||||
resourceType: template.resourceType,
|
||||
dimensionSchema: template.dimensionSchema,
|
||||
formulaConfig: {
|
||||
...(rule.formulaConfig ?? {}),
|
||||
formula: template.formulaConfig?.formula,
|
||||
},
|
||||
dynamicWeight: normalizeDynamicWeight(rule.dynamicWeight, mode),
|
||||
};
|
||||
}
|
||||
|
||||
function readGroup(value: RecordValue | undefined, group: ModeDefinition['weightGroups'][number]) {
|
||||
function normalizeDisplayName(value: string | undefined, mode: ModeDefinition) {
|
||||
const legacyNames: Record<ModeKey, string[]> = {
|
||||
text: ['文本输入 Token', '文本输出 Token', '文本生成 Token', '文本生成'],
|
||||
image: ['图像生成', '图像编辑'],
|
||||
video: ['视频生成'],
|
||||
};
|
||||
if (!value || legacyNames[mode.key].includes(value)) return mode.label;
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeDynamicWeight(value: PricingRuleInput['dynamicWeight'], mode: ModeDefinition): RecordValue {
|
||||
const source = isPlainObject(value) ? { ...(value as RecordValue) } : {};
|
||||
if (mode.key === 'image') {
|
||||
if (!isPlainObject(source.resolutionFactors) && isPlainObject(source.resolutionWeights)) {
|
||||
source.resolutionFactors = source.resolutionWeights;
|
||||
}
|
||||
if (!isPlainObject(source.qualityFactors) && isPlainObject(source.qualityWeights)) {
|
||||
source.qualityFactors = source.qualityWeights;
|
||||
}
|
||||
delete source.modeFactors;
|
||||
delete source.modeWeights;
|
||||
delete source.referenceImageWeights;
|
||||
delete source.resolutionWeights;
|
||||
delete source.qualityWeights;
|
||||
}
|
||||
|
||||
const groupKeys = new Set(mode.parameterGroups.map((group) => group.key));
|
||||
const defaults = Object.fromEntries(mode.parameterGroups.map((group) => [
|
||||
group.key,
|
||||
mergeGroupDefaults(group, isPlainObject(source[group.key]) ? source[group.key] as RecordValue : {}),
|
||||
]));
|
||||
const custom = Object.fromEntries(Object.entries(source).filter(([key]) => !groupKeys.has(key)));
|
||||
return { ...defaults, ...custom };
|
||||
}
|
||||
|
||||
function mergeGroupDefaults(group: ParameterGroup, source: RecordValue): RecordValue {
|
||||
if (!group.locked) return { ...group.defaults, ...source };
|
||||
return Object.fromEntries(Object.entries(group.defaults).map(([key, defaultValue]) => [
|
||||
key,
|
||||
key in source ? source[key] : defaultValue,
|
||||
]));
|
||||
}
|
||||
|
||||
function mergeTemplateRules(templates: PricingRuleInput[], rules: PricingRuleInput[]) {
|
||||
if (templates.length === 1) return rules.length ? [{ ...templates[0], ...rules[0] }] : templates;
|
||||
const byKey = new Map(templates.map((rule) => [rule.ruleKey ?? rule.resourceType, rule]));
|
||||
for (const rule of rules) {
|
||||
byKey.set(rule.ruleKey ?? rule.resourceType, rule);
|
||||
}
|
||||
return Array.from(byKey.values());
|
||||
}
|
||||
|
||||
function mergeTextRules(rules: PricingRuleInput[], currency: string): PricingRuleInput {
|
||||
const totalRule = rules.find((rule) => rule.resourceType === 'text_total');
|
||||
const inputRule = rules.find((rule) => rule.resourceType === 'text_input');
|
||||
const outputRule = rules.find((rule) => rule.resourceType === 'text_output');
|
||||
const source = totalRule ?? inputRule ?? outputRule ?? createTextRule(currency, 0.01, 0.03);
|
||||
return createTextRule(
|
||||
source.currency ?? currency,
|
||||
Number(source.formulaConfig?.inputTokenPrice ?? inputRule?.basePrice ?? source.basePrice ?? 0.01),
|
||||
Number(source.formulaConfig?.outputTokenPrice ?? outputRule?.basePrice ?? 0.03),
|
||||
source,
|
||||
);
|
||||
}
|
||||
|
||||
function readGroup(value: RecordValue | undefined, group: ModeDefinition['parameterGroups'][number]) {
|
||||
return isPlainObject(value?.[group.key]) ? value?.[group.key] as RecordValue : group.defaults;
|
||||
}
|
||||
|
||||
function createRule(ruleKey: string, displayName: string, resourceType: string, unit: string, basePrice: number, currency: string, calculatorType: string, formula: string, baseWeight: RecordValue, dynamicWeight: RecordValue, dimensionSchema: RecordValue): PricingRuleInput {
|
||||
const textFormula = 'input_tokens / 1000 * input_token_price + output_tokens / 1000 * output_token_price';
|
||||
|
||||
function createTextRule(currency: string, inputTokenPrice: number, outputTokenPrice: number, source?: PricingRuleInput): PricingRuleInput {
|
||||
return {
|
||||
...(source ?? {}),
|
||||
ruleKey: 'text',
|
||||
displayName: normalizeDisplayName(source?.displayName, modeDefinitions[0]),
|
||||
resourceType: 'text_total',
|
||||
unit: '1k_tokens',
|
||||
basePrice: inputTokenPrice,
|
||||
currency,
|
||||
calculatorType: 'token_usage',
|
||||
baseWeight: {},
|
||||
dynamicWeight: {},
|
||||
dimensionSchema: {
|
||||
metrics: ['input_tokens', 'output_tokens'],
|
||||
unitScale: 1000,
|
||||
...(source?.dimensionSchema ?? {}),
|
||||
},
|
||||
formulaConfig: {
|
||||
...(source?.formulaConfig ?? {}),
|
||||
formula: textFormula,
|
||||
inputTokenPrice,
|
||||
outputTokenPrice,
|
||||
},
|
||||
priority: source?.priority ?? 100,
|
||||
status: source?.status ?? 'active',
|
||||
metadata: source?.metadata ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
function textPrices(rule: PricingRuleInput) {
|
||||
return {
|
||||
inputTokenPrice: Number(rule.formulaConfig?.inputTokenPrice ?? rule.basePrice ?? 0.01),
|
||||
outputTokenPrice: Number(rule.formulaConfig?.outputTokenPrice ?? 0.03),
|
||||
};
|
||||
}
|
||||
|
||||
function createRule(ruleKey: string, displayName: string, resourceType: string, unit: string, basePrice: number, currency: string, calculatorType: string, formula: string, dynamicWeight: RecordValue, dimensionSchema: RecordValue): PricingRuleInput {
|
||||
return {
|
||||
ruleKey,
|
||||
displayName,
|
||||
@@ -305,7 +445,7 @@ function createRule(ruleKey: string, displayName: string, resourceType: string,
|
||||
basePrice,
|
||||
currency,
|
||||
calculatorType,
|
||||
baseWeight,
|
||||
baseWeight: {},
|
||||
dynamicWeight,
|
||||
dimensionSchema,
|
||||
formulaConfig: { formula },
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMemo, useState, type FormEvent } from 'react';
|
||||
import { Pencil, Plus, RotateCcw, Trash2 } from 'lucide-react';
|
||||
import { FileText, Image, Pencil, Plus, RotateCcw, Trash2, Video } from 'lucide-react';
|
||||
import type { PricingRule, PricingRuleInput, PricingRuleSet, PricingRuleSetUpsertRequest } from '@easyai-ai-gateway/contracts';
|
||||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, FormDialog, Input, Label, Select } from '../../components/ui';
|
||||
import type { LoadState } from '../../types';
|
||||
import { KeyValueEditor, PricingRuleVisualEditor } from './PricingRuleVisualEditor';
|
||||
import { unitLabel } from './PricingEditorControls';
|
||||
import { createDefaultPricingRules, KeyValueEditor, PricingRuleVisualEditor } from './PricingRuleVisualEditor';
|
||||
|
||||
type PricingForm = {
|
||||
ruleSetKey: string;
|
||||
@@ -16,17 +17,6 @@ type PricingForm = {
|
||||
rules: PricingRuleInput[];
|
||||
};
|
||||
|
||||
const emptyForm: PricingForm = {
|
||||
ruleSetKey: '',
|
||||
name: '',
|
||||
description: '',
|
||||
category: 'custom',
|
||||
currency: 'resource',
|
||||
status: 'active',
|
||||
metadata: {},
|
||||
rules: [],
|
||||
};
|
||||
|
||||
const categories = ['default', 'custom', 'provider', 'model'];
|
||||
const statuses = ['active', 'deprecated', 'hidden'];
|
||||
const currencies = ['resource', 'credit', 'cny', 'usd'];
|
||||
@@ -40,7 +30,7 @@ export function PricingRulesPanel(props: {
|
||||
}) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState('');
|
||||
const [form, setForm] = useState<PricingForm>(emptyForm);
|
||||
const [form, setForm] = useState<PricingForm>(() => createEmptyForm());
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [query, setQuery] = useState('');
|
||||
const filtered = useMemo(() => {
|
||||
@@ -62,7 +52,7 @@ export function PricingRulesPanel(props: {
|
||||
|
||||
function openCreateDialog() {
|
||||
setEditingId('');
|
||||
setForm(emptyForm);
|
||||
setForm(createEmptyForm());
|
||||
setLocalError('');
|
||||
setDialogOpen(true);
|
||||
}
|
||||
@@ -76,11 +66,15 @@ export function PricingRulesPanel(props: {
|
||||
|
||||
function closeDialog() {
|
||||
setEditingId('');
|
||||
setForm(emptyForm);
|
||||
setForm(createEmptyForm());
|
||||
setLocalError('');
|
||||
setDialogOpen(false);
|
||||
}
|
||||
|
||||
function updateCurrency(currency: string) {
|
||||
setForm({ ...form, currency, rules: ensureRules(form.rules, currency) });
|
||||
}
|
||||
|
||||
async function deleteRuleSet(ruleSet: PricingRuleSet) {
|
||||
const confirmed = window.confirm(`确认删除定价规则 ${ruleSet.name}?已绑定的平台或模型会清空规则绑定。`);
|
||||
if (!confirmed) return;
|
||||
@@ -125,11 +119,14 @@ export function PricingRulesPanel(props: {
|
||||
<div className="providerCatalogMeta">
|
||||
<span>{ruleSet.category}</span>
|
||||
<span>{ruleSet.currency}</span>
|
||||
<span>{ruleSet.rules?.length ?? 0} 条计价规则</span>
|
||||
<span>{pricingRuleSummaries(ruleSet).length} 条计价规则</span>
|
||||
</div>
|
||||
<div className="pricingRuleItems">
|
||||
{(ruleSet.rules ?? []).slice(0, 5).map((rule) => (
|
||||
<span key={rule.id || rule.ruleKey}>{rule.displayName || rule.resourceType}: {rule.basePrice}/{rule.unit}</span>
|
||||
{pricingRuleSummaries(ruleSet).map((item) => (
|
||||
<span key={item.label}>
|
||||
<strong>{pricingRuleSummaryIcon(item.kind)}{item.label}</strong>
|
||||
<em>{item.value}</em>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="providerCatalogActions">
|
||||
@@ -163,7 +160,7 @@ export function PricingRulesPanel(props: {
|
||||
<Label>名称<Input value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} /></Label>
|
||||
<Label className="spanTwo">说明<Input value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /></Label>
|
||||
<Label>类型<Select value={form.category} onChange={(event) => setForm({ ...form, category: event.target.value })}>{categories.map((item) => <option value={item} key={item}>{item}</option>)}</Select></Label>
|
||||
<Label>货币<Select value={form.currency} onChange={(event) => setForm({ ...form, currency: event.target.value })}>{currencies.map((item) => <option value={item} key={item}>{item}</option>)}</Select></Label>
|
||||
<Label>货币<Select value={form.currency} onChange={(event) => updateCurrency(event.target.value)}>{currencies.map((item) => <option value={item} key={item}>{item}</option>)}</Select></Label>
|
||||
<Label>状态<Select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>{statuses.map((item) => <option value={item} key={item}>{item}</option>)}</Select></Label>
|
||||
<PricingRuleVisualEditor currency={form.currency} rules={form.rules} onChange={(rules) => setForm({ ...form, rules })} />
|
||||
<KeyValueEditor className="spanTwo" title="规则集元数据" value={form.metadata} onChange={(metadata) => setForm({ ...form, metadata })} />
|
||||
@@ -172,6 +169,19 @@ export function PricingRulesPanel(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function createEmptyForm(currency = 'resource'): PricingForm {
|
||||
return {
|
||||
ruleSetKey: '',
|
||||
name: '',
|
||||
description: '',
|
||||
category: 'custom',
|
||||
currency,
|
||||
status: 'active',
|
||||
metadata: {},
|
||||
rules: createDefaultPricingRules(currency),
|
||||
};
|
||||
}
|
||||
|
||||
function ruleSetToForm(ruleSet: PricingRuleSet): PricingForm {
|
||||
return {
|
||||
ruleSetKey: ruleSet.ruleSetKey,
|
||||
@@ -205,13 +215,12 @@ function ruleToInput(rule: PricingRule): PricingRuleInput {
|
||||
}
|
||||
|
||||
function formToPayload(form: PricingForm): PricingRuleSetUpsertRequest {
|
||||
const rules = form.rules.map((rule, index) => ({
|
||||
const rules = ensureRules(form.rules, form.currency).map((rule, index) => ({
|
||||
...rule,
|
||||
displayName: rule.displayName?.trim() || `${rule.resourceType} 计价`,
|
||||
priority: rule.priority ?? (index + 1) * 10,
|
||||
ruleKey: rule.ruleKey?.trim() || `${rule.resourceType}_${index + 1}`,
|
||||
}));
|
||||
if (!rules.length) throw new Error('计价规则至少需要一条');
|
||||
return {
|
||||
ruleSetKey: form.ruleSetKey.trim(),
|
||||
name: form.name.trim(),
|
||||
@@ -223,3 +232,48 @@ function formToPayload(form: PricingForm): PricingRuleSetUpsertRequest {
|
||||
rules,
|
||||
};
|
||||
}
|
||||
|
||||
type PricingRuleSummary = {
|
||||
kind: 'text' | 'image' | 'video' | 'custom';
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
function pricingRuleSummaries(ruleSet: PricingRuleSet): PricingRuleSummary[] {
|
||||
const rules = ruleSet.rules ?? [];
|
||||
const textTotal = rules.find((rule) => rule.resourceType === 'text_total' || rule.ruleKey === 'text');
|
||||
const textInput = rules.find((rule) => rule.resourceType === 'text_input');
|
||||
const textOutput = rules.find((rule) => rule.resourceType === 'text_output');
|
||||
const image = rules.find((rule) => rule.resourceType === 'image');
|
||||
const video = rules.find((rule) => rule.resourceType === 'video');
|
||||
const items: PricingRuleSummary[] = [];
|
||||
if (textTotal || textInput || textOutput) {
|
||||
const inputPrice = Number(textTotal?.formulaConfig?.inputTokenPrice ?? textInput?.basePrice ?? textTotal?.basePrice ?? 0);
|
||||
const outputPrice = Number(textTotal?.formulaConfig?.outputTokenPrice ?? textOutput?.basePrice ?? 0);
|
||||
items.push({ kind: 'text', label: '文本', value: `输入 ${formatPrice(inputPrice)} / 输出 ${formatPrice(outputPrice)} / 1K Token` });
|
||||
}
|
||||
if (image) items.push({ kind: 'image', label: '图像', value: `${formatPrice(image.basePrice)} / ${unitLabel(image.unit)}` });
|
||||
if (video) items.push({ kind: 'video', label: '视频', value: `${formatPrice(video.basePrice)} / ${unitLabel(video.unit)}` });
|
||||
if (items.length) return items;
|
||||
return rules.slice(0, 3).map((rule) => ({
|
||||
kind: 'custom',
|
||||
label: rule.displayName || rule.resourceType,
|
||||
value: `${formatPrice(rule.basePrice)} / ${unitLabel(rule.unit)}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function pricingRuleSummaryIcon(kind: PricingRuleSummary['kind']) {
|
||||
if (kind === 'image') return <Image size={14} />;
|
||||
if (kind === 'video') return <Video size={14} />;
|
||||
return <FileText size={14} />;
|
||||
}
|
||||
|
||||
function formatPrice(value: number) {
|
||||
if (!Number.isFinite(value)) return '0';
|
||||
return Number(value.toFixed(4)).toString();
|
||||
}
|
||||
|
||||
function ensureRules(rules: PricingRuleInput[], currency: string): PricingRuleInput[] {
|
||||
const sourceRules = rules.length ? rules : createDefaultPricingRules(currency);
|
||||
return sourceRules.map((rule) => ({ ...rule, currency }));
|
||||
}
|
||||
|
||||
+37
-36
@@ -1,22 +1,24 @@
|
||||
@import './styles/ui.css';
|
||||
@import './styles/pages.css';
|
||||
@import './styles/pricing.css';
|
||||
@import './styles/api-docs.css';
|
||||
@import './styles/landing.css';
|
||||
|
||||
:root {
|
||||
--background: #f6f7f9;
|
||||
--foreground: #111827;
|
||||
--background: #f7f8fa;
|
||||
--foreground: #101318;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #111827;
|
||||
--muted: #eef2f6;
|
||||
--muted-foreground: #667085;
|
||||
--border: #dde3ea;
|
||||
--input: #cfd8e3;
|
||||
--primary: #145388;
|
||||
--card-foreground: #101318;
|
||||
--muted: #f2f4f7;
|
||||
--muted-foreground: #6b7280;
|
||||
--border: #e4e7ec;
|
||||
--input: #d8dee6;
|
||||
--primary: #111827;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #edf2f7;
|
||||
--secondary-foreground: #1f2937;
|
||||
--secondary: #f4f6f8;
|
||||
--secondary-foreground: #202734;
|
||||
--destructive: #b42318;
|
||||
--ring: #2f80c1;
|
||||
--ring: #64748b;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
@@ -90,8 +92,8 @@ h1 {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: #145388;
|
||||
border-radius: 10px;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
@@ -153,10 +155,9 @@ h1 {
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid #e7ebf0;
|
||||
border-radius: 999px;
|
||||
background: #f5f7fa;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.topNavItem {
|
||||
@@ -168,7 +169,7 @@ h1 {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #475467;
|
||||
color: #5d6675;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
@@ -177,8 +178,8 @@ h1 {
|
||||
.topNavItem:hover,
|
||||
.topNavItem[data-active="true"] {
|
||||
background: #ffffff;
|
||||
color: #145388;
|
||||
box-shadow: 0 1px 4px rgba(16, 24, 40, 0.12);
|
||||
color: #111827;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.08);
|
||||
}
|
||||
|
||||
.workspaceShell {
|
||||
@@ -196,8 +197,8 @@ h1 {
|
||||
gap: 16px;
|
||||
padding: 0 28px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 1px 10px rgba(16, 24, 40, 0.04);
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
box-shadow: 0 1px 0 rgba(16, 24, 40, 0.03);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
@@ -262,10 +263,10 @@ h1 {
|
||||
.notice,
|
||||
.inlineNotice {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #f0b8b8;
|
||||
border: 1px solid #f0d4d4;
|
||||
border-radius: 8px;
|
||||
background: #fff1f1;
|
||||
color: #9b2c2c;
|
||||
background: #fff7f7;
|
||||
color: #9f2f2f;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -301,17 +302,16 @@ h1 {
|
||||
min-height: 92px;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border);
|
||||
border-top: 3px solid #64748b;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
.statCard[data-tone="blue"] { border-top-color: #145388; }
|
||||
.statCard[data-tone="green"] { border-top-color: #14805e; }
|
||||
.statCard[data-tone="violet"] { border-top-color: #7048b8; }
|
||||
.statCard[data-tone="amber"] { border-top-color: #b7791f; }
|
||||
.statCard[data-tone="cyan"] { border-top-color: #087f8c; }
|
||||
.statCard[data-tone="rose"] { border-top-color: #b8325f; }
|
||||
.statCard[data-tone="blue"] { border-color: #d8e0ea; }
|
||||
.statCard[data-tone="green"] { border-color: #d7e7df; }
|
||||
.statCard[data-tone="violet"] { border-color: #e0dce9; }
|
||||
.statCard[data-tone="amber"] { border-color: #ebe0cc; }
|
||||
.statCard[data-tone="cyan"] { border-color: #d5e4e8; }
|
||||
.statCard[data-tone="rose"] { border-color: #ead8df; }
|
||||
|
||||
.statCard span {
|
||||
color: var(--muted-foreground);
|
||||
@@ -336,9 +336,10 @@ h1 {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: #eaf2f8;
|
||||
color: #145388;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.infoItem,
|
||||
@@ -348,7 +349,7 @@ h1 {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fbfcfe;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
.apiDocsShell {
|
||||
display: grid;
|
||||
min-height: calc(100vh - 120px);
|
||||
grid-template-columns: 280px minmax(420px, 1fr) 360px;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsSidebar,
|
||||
.docsArticle,
|
||||
.docsRunner {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.docsSidebar {
|
||||
padding: 18px;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.docsBrand,
|
||||
.docsSearch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.docsBrand {
|
||||
margin-bottom: 18px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.docsSearch {
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.docsSearch input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.docsGroup {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.docsGroup h3 {
|
||||
margin-bottom: 4px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.docsGroup button {
|
||||
display: flex;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: #344054;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.docsGroup button[data-active="true"] {
|
||||
background: #f5f6f8;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.docsArticle {
|
||||
padding: 28px 34px 60px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsArticle h1 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.endpointBar,
|
||||
.runnerEndpoint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsLead {
|
||||
margin: 18px 0 24px;
|
||||
color: #475467;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.paramCard {
|
||||
margin-top: 18px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.paramCard header,
|
||||
.docsRunner header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.paramRow {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 110px minmax(0, 1fr) 50px;
|
||||
gap: 12px;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #475467;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.paramRow em {
|
||||
color: #a16207;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.docsRunner {
|
||||
border-left: 1px solid var(--border);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsRunner form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.runnerResult {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.runnerResult pre {
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #f7f8fa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.apiDocsShell {
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.docsRunner {
|
||||
grid-column: 1 / -1;
|
||||
border-top: 1px solid var(--border);
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.apiDocsShell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.paramRow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,10 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #d7eadf;
|
||||
border-radius: 10px;
|
||||
background: #f1fbf5;
|
||||
color: #155e3d;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: #202734;
|
||||
}
|
||||
|
||||
.releaseNotice strong {
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
.releaseNotice span {
|
||||
color: #43735b;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -32,10 +32,8 @@
|
||||
align-items: center;
|
||||
padding: 44px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background:
|
||||
linear-gradient(120deg, rgba(245, 250, 255, 0.96), rgba(255, 255, 255, 0.9)),
|
||||
#ffffff;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.landingCopy {
|
||||
@@ -66,10 +64,10 @@
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid #dbe5ef;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
box-shadow: 0 18px 45px rgba(16, 24, 40, 0.10);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: #fafbfc;
|
||||
box-shadow: 0 20px 60px rgba(16, 24, 40, 0.08);
|
||||
}
|
||||
|
||||
.previewHeader,
|
||||
@@ -98,7 +96,7 @@
|
||||
min-height: 92px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -114,8 +112,8 @@
|
||||
|
||||
.previewFlow {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: #102033;
|
||||
border-radius: 10px;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
|
||||
+26
-379
@@ -15,7 +15,9 @@
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.sideTabs .shTab {
|
||||
@@ -58,7 +60,7 @@
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
@@ -66,8 +68,8 @@
|
||||
}
|
||||
|
||||
.filterChip[data-active="true"] {
|
||||
border-color: #2f6fe4;
|
||||
background: #2f6fe4;
|
||||
border-color: #111827;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -109,9 +111,9 @@
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
border: 1px solid #ebe5fb;
|
||||
border-radius: 8px;
|
||||
background: #f6f1ff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.providerHero strong {
|
||||
@@ -130,7 +132,7 @@
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font-weight: 900;
|
||||
@@ -193,9 +195,10 @@
|
||||
|
||||
.modelTags span {
|
||||
padding: 4px 7px;
|
||||
border-radius: 5px;
|
||||
background: #fff5e7;
|
||||
color: #9a5b12;
|
||||
border: 1px solid #eceff3;
|
||||
border-radius: 999px;
|
||||
background: #fafbfc;
|
||||
color: #5d6675;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -208,7 +211,7 @@
|
||||
}
|
||||
|
||||
.modelCardFooter a {
|
||||
color: #2563eb;
|
||||
color: #111827;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
@@ -247,7 +250,7 @@
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -257,9 +260,9 @@
|
||||
height: 54px;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border: 1px solid #edf1f5;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef1f4;
|
||||
border-radius: 10px;
|
||||
background: #fafbfc;
|
||||
color: #111827;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -323,7 +326,7 @@
|
||||
min-height: 184px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -359,9 +362,10 @@
|
||||
|
||||
.modelAbilityChips span {
|
||||
padding: 4px 7px;
|
||||
border-radius: 5px;
|
||||
background: #eef7ff;
|
||||
color: #0f5d8f;
|
||||
border: 1px solid #e8ecf1;
|
||||
border-radius: 999px;
|
||||
background: #fafbfc;
|
||||
color: #4b5563;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -376,368 +380,17 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.pricingRuleCard {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pricingRuleCard header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleCard strong,
|
||||
.pricingRuleCard span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pricingRuleCard header span,
|
||||
.pricingRuleCard p {
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleItems {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pricingRuleItems span {
|
||||
padding: 5px 7px;
|
||||
border-radius: 5px;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pricingRuleDialog {
|
||||
width: min(1120px, 100%);
|
||||
}
|
||||
|
||||
.pricingRuleFormBody {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pricingRuleEditor {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleEditorHeader,
|
||||
.pricingRuleEditorCard header,
|
||||
.pricingRuleSubHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleEditorHeader strong,
|
||||
.pricingRuleEditorCard strong,
|
||||
.pricingRuleSubHeader strong {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pricingRuleEditorHeader span,
|
||||
.pricingRuleEditorCard header span,
|
||||
.pricingRuleSubHeader span,
|
||||
.pricingRuleEmpty span,
|
||||
.mutedLine {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleQuickActions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pricingRuleEditorCard,
|
||||
.pricingRuleSubPanel,
|
||||
.pricingRuleEmpty {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.pricingRuleEditorCard {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pricingRuleEditorCard header > div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pricingRuleEditorCard header span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pricingRuleFields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dimensionChips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.dimensionChips button {
|
||||
min-height: 30px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dimensionChips button.active {
|
||||
border-color: #145388;
|
||||
background: #eaf3fb;
|
||||
color: #145388;
|
||||
}
|
||||
|
||||
.weightGroup {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.keyValueRows {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.keyValueRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 0.55fr) minmax(140px, 1fr) 34px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.apiDocsShell {
|
||||
display: grid;
|
||||
min-height: calc(100vh - 120px);
|
||||
grid-template-columns: 280px minmax(420px, 1fr) 360px;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsSidebar,
|
||||
.docsArticle,
|
||||
.docsRunner {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.docsSidebar {
|
||||
padding: 18px;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.docsBrand,
|
||||
.docsSearch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.docsBrand {
|
||||
margin-bottom: 18px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.docsSearch {
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.docsSearch input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.docsGroup {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.docsGroup h3 {
|
||||
margin-bottom: 4px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.docsGroup button {
|
||||
display: flex;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: #344054;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.docsGroup button[data-active="true"] {
|
||||
background: #f0e9ff;
|
||||
color: #7048e8;
|
||||
}
|
||||
|
||||
.docsArticle {
|
||||
padding: 28px 34px 60px;
|
||||
background: linear-gradient(90deg, #fff 0%, #fbf8ff 100%);
|
||||
}
|
||||
|
||||
.docsArticle h1 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.endpointBar,
|
||||
.runnerEndpoint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsLead {
|
||||
margin: 18px 0 24px;
|
||||
color: #475467;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.paramCard {
|
||||
margin-top: 18px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.paramCard header,
|
||||
.docsRunner header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.paramRow {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 110px minmax(0, 1fr) 50px;
|
||||
gap: 12px;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
color: #475467;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.paramRow em {
|
||||
color: #f97316;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.docsRunner {
|
||||
border-left: 1px solid var(--border);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsRunner form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.runnerResult {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.runnerResult pre {
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.modelCards,
|
||||
.providerCatalogGrid,
|
||||
.baseModelGrid,
|
||||
.pricingRuleGrid {
|
||||
.baseModelGrid {
|
||||
grid-template-columns: repeat(2, minmax(240px, 1fr));
|
||||
}
|
||||
|
||||
.apiDocsShell {
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.docsRunner {
|
||||
grid-column: 1 / -1;
|
||||
border-top: 1px solid var(--border);
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.subPageLayout,
|
||||
.modelsPage,
|
||||
.apiDocsShell {
|
||||
.modelsPage {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -749,9 +402,6 @@
|
||||
.modelCards,
|
||||
.providerCatalogGrid,
|
||||
.baseModelGrid,
|
||||
.pricingRuleGrid,
|
||||
.pricingRuleFields,
|
||||
.pricingRuleFormBody,
|
||||
.modelCatalogFilters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -770,7 +420,4 @@
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.paramRow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
.pricingRuleGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.pricingRuleCard {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pricingRuleCard header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleCard strong,
|
||||
.pricingRuleCard span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pricingRuleCard header span,
|
||||
.pricingRuleCard p {
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleItems {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pricingRuleItems span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-height: 56px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8ecf1;
|
||||
background: #fafbfc;
|
||||
color: #4b5563;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pricingRuleItems strong {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pricingRuleItems svg {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.pricingRuleItems em {
|
||||
color: var(--muted-foreground);
|
||||
font-style: normal;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.pricingRuleDialog {
|
||||
width: min(1120px, 100%);
|
||||
}
|
||||
|
||||
.pricingRuleFormBody {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pricingModeEditor,
|
||||
.pricingModeRule,
|
||||
.pricingWeightStack {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.pricingModeHeader,
|
||||
.pricingModeRule header,
|
||||
.pricingWeightTable header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pricingModeHeader strong,
|
||||
.pricingModeRule strong,
|
||||
.pricingWeightTable strong {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pricingModeHeader span,
|
||||
.pricingModeRule header span {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pricingModeTabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pricingModeTabs button {
|
||||
min-height: 40px;
|
||||
padding: 0 2px;
|
||||
border: 0;
|
||||
border-bottom: 3px solid transparent;
|
||||
background: transparent;
|
||||
color: #475467;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pricingModeTabs button.active {
|
||||
border-bottom-color: #111827;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.pricingModeRule,
|
||||
.pricingModeEmpty {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pricingModeEmpty {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pricingModeRule header > div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pricingModeRule header span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pricingModeBaseRows {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pricingModeBaseRows.compact {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pricingModeInlineRows {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pricingTextPriceGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pricingModeInlineInput,
|
||||
.pricingInlineField {
|
||||
display: grid;
|
||||
grid-template-columns: 140px minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pricingModeInlineInput span,
|
||||
.pricingInlineField span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
border-right: 1px solid var(--border);
|
||||
background: #fafbfc;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.pricingModeInlineInput input,
|
||||
.pricingModeInlineInput select,
|
||||
.pricingInlineField input,
|
||||
.pricingInlineField select {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.pricingInlineReadonly strong {
|
||||
display: flex;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pricingFormulaBox {
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: #f6f7f9;
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
.pricingFormulaBox p {
|
||||
margin-top: 8px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pricingFormulaBox input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pricingWeightTable {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pricingWeightTable header {
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
background: #f6f7f9;
|
||||
}
|
||||
|
||||
.pricingWeightRows {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.pricingWeightRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(100px, 150px) minmax(82px, 120px) 32px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pricingWeightRow.locked {
|
||||
grid-template-columns: minmax(112px, 150px) minmax(82px, 120px);
|
||||
}
|
||||
|
||||
.pricingFactorLabel {
|
||||
display: flex;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fafbfc;
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pricingInlineAdd {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.pricingRuleGrid,
|
||||
.pricingModeBaseRows,
|
||||
.pricingRuleFormBody,
|
||||
.pricingModeInlineRows,
|
||||
.pricingModeInlineInput,
|
||||
.pricingInlineField,
|
||||
.pricingTextPriceGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
+35
-20
@@ -1,7 +1,7 @@
|
||||
.shCard {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--card);
|
||||
color: var(--card-foreground);
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
border-bottom: 1px solid #eef1f4;
|
||||
}
|
||||
|
||||
.shCardTitle {
|
||||
@@ -39,9 +39,10 @@
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
border-radius: 7px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
transition: background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, color 0.16s ease;
|
||||
}
|
||||
|
||||
.shButtonDefaultSize {
|
||||
@@ -65,6 +66,10 @@
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.shButtonDefault:hover {
|
||||
background: #202734;
|
||||
}
|
||||
|
||||
.shButtonSecondary {
|
||||
background: var(--secondary);
|
||||
color: var(--secondary-foreground);
|
||||
@@ -76,11 +81,21 @@
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
.shButtonOutline:hover,
|
||||
.shButtonSecondary:hover {
|
||||
border-color: #d5dbe3;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.shButtonGhost {
|
||||
background: transparent;
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
.shButtonGhost:hover {
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.shButtonDestructive {
|
||||
background: var(--destructive);
|
||||
color: #fff;
|
||||
@@ -93,7 +108,7 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background: rgba(15, 23, 42, 0.42);
|
||||
background: rgba(16, 19, 24, 0.36);
|
||||
}
|
||||
|
||||
.formDialog {
|
||||
@@ -103,9 +118,9 @@
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.22);
|
||||
box-shadow: 0 22px 60px rgba(16, 24, 40, 0.16);
|
||||
}
|
||||
|
||||
.formDialogHeader {
|
||||
@@ -154,14 +169,14 @@
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: #fff;
|
||||
box-shadow: 0 -10px 24px rgba(15, 23, 42, 0.06);
|
||||
box-shadow: 0 -1px 0 rgba(16, 24, 40, 0.02);
|
||||
}
|
||||
|
||||
.shInput,
|
||||
.shTextarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--input);
|
||||
border-radius: 8px;
|
||||
border-radius: 7px;
|
||||
background: #fff;
|
||||
color: var(--foreground);
|
||||
outline: none;
|
||||
@@ -182,7 +197,7 @@
|
||||
.shTextarea:focus,
|
||||
.tokenInline input:focus {
|
||||
border-color: var(--ring);
|
||||
box-shadow: 0 0 0 3px rgba(47, 128, 193, 0.16);
|
||||
box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.14);
|
||||
}
|
||||
|
||||
.shLabel,
|
||||
@@ -212,7 +227,7 @@
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
background: #f6f7f9;
|
||||
}
|
||||
|
||||
.shTab {
|
||||
@@ -230,8 +245,8 @@
|
||||
|
||||
.shTab[data-active="true"] {
|
||||
background: #fff;
|
||||
color: #145388;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.08);
|
||||
color: #111827;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
|
||||
}
|
||||
|
||||
.shBadge {
|
||||
@@ -246,12 +261,12 @@
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.shBadgeDefault { background: #eaf2f8; color: #145388; }
|
||||
.shBadgeSecondary { background: #eef2f6; color: #475467; }
|
||||
.shBadgeDefault { background: #eef1f5; color: #202734; }
|
||||
.shBadgeSecondary { background: #f3f5f7; color: #5d6675; }
|
||||
.shBadgeOutline { border-color: var(--border); background: #fff; color: #344054; }
|
||||
.shBadgeSuccess { background: #e8f5ef; color: #14805e; }
|
||||
.shBadgeWarning { background: #fff6df; color: #98690c; }
|
||||
.shBadgeDestructive { background: #fff1f1; color: #b42318; }
|
||||
.shBadgeSuccess { background: #edf7f1; color: #157a57; }
|
||||
.shBadgeWarning { background: #fbf4e8; color: #8a6116; }
|
||||
.shBadgeDestructive { background: #fff3f3; color: #b42318; }
|
||||
|
||||
.shTable {
|
||||
overflow: auto;
|
||||
@@ -261,11 +276,11 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
|
||||
min-width: 520px;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
border-bottom: 1px solid #eef1f4;
|
||||
}
|
||||
|
||||
.shTableHeader {
|
||||
background: #f8fafc;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.shTableHead,
|
||||
@@ -305,7 +320,7 @@
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
background: #f7f8fa;
|
||||
color: #1f2937;
|
||||
font-family: "SFMono-Regular", Consolas, monospace;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user