|
|
@@ -1,6 +1,7 @@
|
|
|
import i18next from 'i18next';
|
|
|
import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
|
|
|
-import { copy, isMobile, showSuccess } from './utils.js';
|
|
|
+import { copy, isMobile, showSuccess } from './index.js';
|
|
|
+import { visit } from 'unist-util-visit';
|
|
|
|
|
|
export function renderText(text, limit) {
|
|
|
if (text.length > limit) {
|
|
|
@@ -419,11 +420,25 @@ export function renderModelPrice(
|
|
|
<p>
|
|
|
{cacheTokens > 0 && !image && !webSearch && !fileSearch
|
|
|
? i18next.t(
|
|
|
- '输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
+ '输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
+ {
|
|
|
+ nonCacheInput: inputTokens - cacheTokens,
|
|
|
+ cacheInput: cacheTokens,
|
|
|
+ cachePrice: inputRatioPrice * cacheRatio,
|
|
|
+ price: inputRatioPrice,
|
|
|
+ completion: completionTokens,
|
|
|
+ compPrice: completionRatioPrice,
|
|
|
+ ratio: groupRatio,
|
|
|
+ total: price.toFixed(6),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ : image && imageOutputTokens > 0 && !webSearch && !fileSearch
|
|
|
+ ? i18next.t(
|
|
|
+ '输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
{
|
|
|
- nonCacheInput: inputTokens - cacheTokens,
|
|
|
- cacheInput: cacheTokens,
|
|
|
- cachePrice: inputRatioPrice * cacheRatio,
|
|
|
+ nonImageInput: inputTokens - imageOutputTokens,
|
|
|
+ imageInput: imageOutputTokens,
|
|
|
+ imageRatio: imageRatio,
|
|
|
price: inputRatioPrice,
|
|
|
completion: completionTokens,
|
|
|
compPrice: completionRatioPrice,
|
|
|
@@ -431,82 +446,68 @@ export function renderModelPrice(
|
|
|
total: price.toFixed(6),
|
|
|
},
|
|
|
)
|
|
|
- : image && imageOutputTokens > 0 && !webSearch && !fileSearch
|
|
|
- ? i18next.t(
|
|
|
- '输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
+ : webSearch && webSearchCallCount > 0 && !image && !fileSearch
|
|
|
+ ? i18next.t(
|
|
|
+ '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
|
|
|
{
|
|
|
- nonImageInput: inputTokens - imageOutputTokens,
|
|
|
- imageInput: imageOutputTokens,
|
|
|
- imageRatio: imageRatio,
|
|
|
+ input: inputTokens,
|
|
|
price: inputRatioPrice,
|
|
|
completion: completionTokens,
|
|
|
compPrice: completionRatioPrice,
|
|
|
ratio: groupRatio,
|
|
|
+ webSearchCallCount,
|
|
|
+ webSearchPrice,
|
|
|
total: price.toFixed(6),
|
|
|
},
|
|
|
)
|
|
|
- : webSearch && webSearchCallCount > 0 && !image && !fileSearch
|
|
|
- ? i18next.t(
|
|
|
- '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
|
|
|
+ : fileSearch &&
|
|
|
+ fileSearchCallCount > 0 &&
|
|
|
+ !image &&
|
|
|
+ !webSearch
|
|
|
+ ? i18next.t(
|
|
|
+ '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
|
|
{
|
|
|
input: inputTokens,
|
|
|
price: inputRatioPrice,
|
|
|
completion: completionTokens,
|
|
|
compPrice: completionRatioPrice,
|
|
|
ratio: groupRatio,
|
|
|
- webSearchCallCount,
|
|
|
- webSearchPrice,
|
|
|
+ fileSearchCallCount,
|
|
|
+ fileSearchPrice,
|
|
|
total: price.toFixed(6),
|
|
|
},
|
|
|
)
|
|
|
- : fileSearch &&
|
|
|
+ : webSearch &&
|
|
|
+ webSearchCallCount > 0 &&
|
|
|
+ fileSearch &&
|
|
|
fileSearchCallCount > 0 &&
|
|
|
- !image &&
|
|
|
- !webSearch
|
|
|
- ? i18next.t(
|
|
|
- '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
|
|
+ !image
|
|
|
+ ? i18next.t(
|
|
|
+ '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
|
|
{
|
|
|
input: inputTokens,
|
|
|
price: inputRatioPrice,
|
|
|
completion: completionTokens,
|
|
|
compPrice: completionRatioPrice,
|
|
|
ratio: groupRatio,
|
|
|
+ webSearchCallCount,
|
|
|
+ webSearchPrice,
|
|
|
fileSearchCallCount,
|
|
|
fileSearchPrice,
|
|
|
total: price.toFixed(6),
|
|
|
},
|
|
|
)
|
|
|
- : webSearch &&
|
|
|
- webSearchCallCount > 0 &&
|
|
|
- fileSearch &&
|
|
|
- fileSearchCallCount > 0 &&
|
|
|
- !image
|
|
|
- ? i18next.t(
|
|
|
- '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
|
|
- {
|
|
|
- input: inputTokens,
|
|
|
- price: inputRatioPrice,
|
|
|
- completion: completionTokens,
|
|
|
- compPrice: completionRatioPrice,
|
|
|
- ratio: groupRatio,
|
|
|
- webSearchCallCount,
|
|
|
- webSearchPrice,
|
|
|
- fileSearchCallCount,
|
|
|
- fileSearchPrice,
|
|
|
- total: price.toFixed(6),
|
|
|
- },
|
|
|
- )
|
|
|
: i18next.t(
|
|
|
- '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
- {
|
|
|
- input: inputTokens,
|
|
|
- price: inputRatioPrice,
|
|
|
- completion: completionTokens,
|
|
|
- compPrice: completionRatioPrice,
|
|
|
- ratio: groupRatio,
|
|
|
- total: price.toFixed(6),
|
|
|
- },
|
|
|
- )}
|
|
|
+ '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
+ {
|
|
|
+ input: inputTokens,
|
|
|
+ price: inputRatioPrice,
|
|
|
+ completion: completionTokens,
|
|
|
+ compPrice: completionRatioPrice,
|
|
|
+ ratio: groupRatio,
|
|
|
+ total: price.toFixed(6),
|
|
|
+ },
|
|
|
+ )}
|
|
|
</p>
|
|
|
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
|
|
</article>
|
|
|
@@ -677,10 +678,10 @@ export function renderAudioModelPrice(
|
|
|
let audioPrice =
|
|
|
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
|
|
|
(audioCompletionTokens / 1000000) *
|
|
|
- inputRatioPrice *
|
|
|
- audioRatio *
|
|
|
- audioCompletionRatio *
|
|
|
- groupRatio;
|
|
|
+ inputRatioPrice *
|
|
|
+ audioRatio *
|
|
|
+ audioCompletionRatio *
|
|
|
+ groupRatio;
|
|
|
let price = textPrice + audioPrice;
|
|
|
return (
|
|
|
<>
|
|
|
@@ -736,27 +737,27 @@ export function renderAudioModelPrice(
|
|
|
<p>
|
|
|
{cacheTokens > 0
|
|
|
? i18next.t(
|
|
|
- '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
|
|
- {
|
|
|
- nonCacheInput: inputTokens - cacheTokens,
|
|
|
- cacheInput: cacheTokens,
|
|
|
- cachePrice: inputRatioPrice * cacheRatio,
|
|
|
- price: inputRatioPrice,
|
|
|
- completion: completionTokens,
|
|
|
- compPrice: completionRatioPrice,
|
|
|
- total: textPrice.toFixed(6),
|
|
|
- },
|
|
|
- )
|
|
|
+ '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
|
|
+ {
|
|
|
+ nonCacheInput: inputTokens - cacheTokens,
|
|
|
+ cacheInput: cacheTokens,
|
|
|
+ cachePrice: inputRatioPrice * cacheRatio,
|
|
|
+ price: inputRatioPrice,
|
|
|
+ completion: completionTokens,
|
|
|
+ compPrice: completionRatioPrice,
|
|
|
+ total: textPrice.toFixed(6),
|
|
|
+ },
|
|
|
+ )
|
|
|
: i18next.t(
|
|
|
- '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
|
|
- {
|
|
|
- input: inputTokens,
|
|
|
- price: inputRatioPrice,
|
|
|
- completion: completionTokens,
|
|
|
- compPrice: completionRatioPrice,
|
|
|
- total: textPrice.toFixed(6),
|
|
|
- },
|
|
|
- )}
|
|
|
+ '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
|
|
+ {
|
|
|
+ input: inputTokens,
|
|
|
+ price: inputRatioPrice,
|
|
|
+ completion: completionTokens,
|
|
|
+ compPrice: completionRatioPrice,
|
|
|
+ total: textPrice.toFixed(6),
|
|
|
+ },
|
|
|
+ )}
|
|
|
</p>
|
|
|
<p>
|
|
|
{i18next.t(
|
|
|
@@ -1024,33 +1025,33 @@ export function renderClaudeModelPrice(
|
|
|
<p>
|
|
|
{cacheTokens > 0 || cacheCreationTokens > 0
|
|
|
? i18next.t(
|
|
|
- '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
- {
|
|
|
- nonCacheInput: nonCachedTokens,
|
|
|
- cacheInput: cacheTokens,
|
|
|
- cacheRatio: cacheRatio,
|
|
|
- cacheCreationInput: cacheCreationTokens,
|
|
|
- cacheCreationRatio: cacheCreationRatio,
|
|
|
- cachePrice: cacheRatioPrice,
|
|
|
- cacheCreationPrice: cacheCreationRatioPrice,
|
|
|
- price: inputRatioPrice,
|
|
|
- completion: completionTokens,
|
|
|
- compPrice: completionRatioPrice,
|
|
|
- ratio: groupRatio,
|
|
|
- total: price.toFixed(6),
|
|
|
- },
|
|
|
- )
|
|
|
+ '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
+ {
|
|
|
+ nonCacheInput: nonCachedTokens,
|
|
|
+ cacheInput: cacheTokens,
|
|
|
+ cacheRatio: cacheRatio,
|
|
|
+ cacheCreationInput: cacheCreationTokens,
|
|
|
+ cacheCreationRatio: cacheCreationRatio,
|
|
|
+ cachePrice: cacheRatioPrice,
|
|
|
+ cacheCreationPrice: cacheCreationRatioPrice,
|
|
|
+ price: inputRatioPrice,
|
|
|
+ completion: completionTokens,
|
|
|
+ compPrice: completionRatioPrice,
|
|
|
+ ratio: groupRatio,
|
|
|
+ total: price.toFixed(6),
|
|
|
+ },
|
|
|
+ )
|
|
|
: i18next.t(
|
|
|
- '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
- {
|
|
|
- input: inputTokens,
|
|
|
- price: inputRatioPrice,
|
|
|
- completion: completionTokens,
|
|
|
- compPrice: completionRatioPrice,
|
|
|
- ratio: groupRatio,
|
|
|
- total: price.toFixed(6),
|
|
|
- },
|
|
|
- )}
|
|
|
+ '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
|
|
+ {
|
|
|
+ input: inputTokens,
|
|
|
+ price: inputRatioPrice,
|
|
|
+ completion: completionTokens,
|
|
|
+ compPrice: completionRatioPrice,
|
|
|
+ ratio: groupRatio,
|
|
|
+ total: price.toFixed(6),
|
|
|
+ },
|
|
|
+ )}
|
|
|
</p>
|
|
|
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
|
|
</article>
|
|
|
@@ -1128,3 +1129,79 @@ export function renderClaudeModelPriceSimple(
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/**
|
|
|
+ * rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
|
|
|
+ * 仅在流式渲染阶段使用,避免已渲染文字重复动画。
|
|
|
+ */
|
|
|
+export function rehypeSplitWordsIntoSpans(options = {}) {
|
|
|
+ const { previousContentLength = 0 } = options;
|
|
|
+
|
|
|
+ return (tree) => {
|
|
|
+ let currentCharCount = 0; // 当前已处理的字符数
|
|
|
+
|
|
|
+ visit(tree, 'element', (node) => {
|
|
|
+ if (
|
|
|
+ ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(node.tagName) &&
|
|
|
+ node.children
|
|
|
+ ) {
|
|
|
+ const newChildren = [];
|
|
|
+ node.children.forEach((child) => {
|
|
|
+ if (child.type === 'text') {
|
|
|
+ try {
|
|
|
+ // 使用 Intl.Segmenter 精准拆分中英文及标点
|
|
|
+ const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
|
|
|
+ const segments = segmenter.segment(child.value);
|
|
|
+
|
|
|
+ Array.from(segments)
|
|
|
+ .map((seg) => seg.segment)
|
|
|
+ .filter(Boolean)
|
|
|
+ .forEach((word) => {
|
|
|
+ const wordStartPos = currentCharCount;
|
|
|
+ const wordEndPos = currentCharCount + word.length;
|
|
|
+
|
|
|
+ // 判断这个词是否是新增的(在 previousContentLength 之后)
|
|
|
+ const isNewContent = wordStartPos >= previousContentLength;
|
|
|
+
|
|
|
+ newChildren.push({
|
|
|
+ type: 'element',
|
|
|
+ tagName: 'span',
|
|
|
+ properties: {
|
|
|
+ className: isNewContent ? ['animate-fade-in'] : [],
|
|
|
+ },
|
|
|
+ children: [{ type: 'text', value: word }],
|
|
|
+ });
|
|
|
+
|
|
|
+ currentCharCount = wordEndPos;
|
|
|
+ });
|
|
|
+ } catch (_) {
|
|
|
+ // Fallback:如果浏览器不支持 Segmenter
|
|
|
+ const textStartPos = currentCharCount;
|
|
|
+ const isNewContent = textStartPos >= previousContentLength;
|
|
|
+
|
|
|
+ if (isNewContent) {
|
|
|
+ // 新内容,添加动画
|
|
|
+ newChildren.push({
|
|
|
+ type: 'element',
|
|
|
+ tagName: 'span',
|
|
|
+ properties: {
|
|
|
+ className: ['animate-fade-in'],
|
|
|
+ },
|
|
|
+ children: [{ type: 'text', value: child.value }],
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 旧内容,不添加动画
|
|
|
+ newChildren.push(child);
|
|
|
+ }
|
|
|
+
|
|
|
+ currentCharCount += child.value.length;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ newChildren.push(child);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ node.children = newChildren;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+}
|