@@ -12,10 +19,10 @@ const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' })
placeholder={placeholder}
onChange={(value) => onChange(value)}
value={value}
- autoComplete="new-password"
+ autoComplete='new-password'
/>
>
);
-}
+};
-export default TextInput;
\ No newline at end of file
+export default TextInput;
diff --git a/web/src/components/custom/TextNumberInput.js b/web/src/components/custom/TextNumberInput.js
index e25c7725..36e0cac0 100644
--- a/web/src/components/custom/TextNumberInput.js
+++ b/web/src/components/custom/TextNumberInput.js
@@ -12,10 +12,10 @@ const TextNumberInput = ({ label, name, value, onChange, placeholder }) => {
placeholder={placeholder}
onChange={(value) => onChange(value)}
value={value}
- autoComplete="new-password"
+ autoComplete='new-password'
/>
>
);
-}
+};
-export default TextNumberInput;
\ No newline at end of file
+export default TextNumberInput;
diff --git a/web/src/components/fetchTokenKeys.js b/web/src/components/fetchTokenKeys.js
index 46a70f15..e9cec001 100644
--- a/web/src/components/fetchTokenKeys.js
+++ b/web/src/components/fetchTokenKeys.js
@@ -13,7 +13,7 @@ async function fetchTokenKeys() {
throw new Error('Failed to fetch token keys');
}
} catch (error) {
- console.error("Error fetching token keys:", error);
+ console.error('Error fetching token keys:', error);
return [];
}
}
@@ -27,7 +27,7 @@ function getServerAddress() {
status = JSON.parse(status);
serverAddress = status.server_address || '';
} catch (error) {
- console.error("Failed to parse status from localStorage:", error);
+ console.error('Failed to parse status from localStorage:', error);
}
}
@@ -65,4 +65,4 @@ export function useTokenKeys(id) {
}, []);
return { keys, serverAddress, isLoading };
-}
\ No newline at end of file
+}
diff --git a/web/src/components/utils.js b/web/src/components/utils.js
index a22f76ed..93a5fb85 100644
--- a/web/src/components/utils.js
+++ b/web/src/components/utils.js
@@ -16,6 +16,20 @@ export async function getOAuthState() {
}
}
+export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
+ const state = await getOAuthState();
+ if (!state) return;
+ const redirect_uri = `${window.location.origin}/oauth/oidc`;
+ const response_type = 'code';
+ const scope = 'openid profile email';
+ const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
+ if (openInNewTab) {
+ window.open(url);
+ } else {
+ window.location.href = url;
+ }
+}
+
export async function onGitHubOAuthClicked(github_client_id) {
const state = await getOAuthState();
if (!state) return;
diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js
index 5738d656..fa59bcce 100644
--- a/web/src/constants/channel.constants.js
+++ b/web/src/constants/channel.constants.js
@@ -3,88 +3,89 @@ export const CHANNEL_OPTIONS = [
{
value: 2,
color: 'light-blue',
- label: 'Midjourney Proxy'
+ label: 'Midjourney Proxy',
},
{
value: 5,
color: 'blue',
- label: 'Midjourney Proxy Plus'
+ label: 'Midjourney Proxy Plus',
},
{
value: 36,
color: 'purple',
- label: 'Suno API'
+ label: 'Suno API',
},
{ value: 4, color: 'grey', label: 'Ollama' },
{
value: 14,
color: 'indigo',
- label: 'Anthropic Claude'
+ label: 'Anthropic Claude',
},
{
value: 33,
color: 'indigo',
- label: 'AWS Claude'
+ label: 'AWS Claude',
},
{ value: 41, color: 'blue', label: 'Vertex AI' },
{
value: 3,
color: 'teal',
- label: 'Azure OpenAI'
+ label: 'Azure OpenAI',
},
{
value: 34,
color: 'purple',
- label: 'Cohere'
+ label: 'Cohere',
},
{ value: 39, color: 'grey', label: 'Cloudflare' },
{ value: 43, color: 'blue', label: 'DeepSeek' },
{
value: 15,
color: 'blue',
- label: '百度文心千帆'
+ label: '百度文心千帆',
},
{
value: 46,
color: 'blue',
- label: '百度文心千帆V2'
+ label: '百度文心千帆V2',
},
{
value: 17,
color: 'orange',
- label: '阿里通义千问'
+ label: '阿里通义千问',
},
{
value: 18,
color: 'blue',
- label: '讯飞星火认知'
+ label: '讯飞星火认知',
},
{
value: 16,
color: 'violet',
- label: '智谱 ChatGLM'
+ label: '智谱 ChatGLM',
},
{
value: 26,
color: 'purple',
- label: '智谱 GLM-4V'
+ label: '智谱 GLM-4V',
},
{
value: 24,
color: 'orange',
- label: 'Google Gemini'
+ label: 'Google Gemini',
},
{
value: 11,
color: 'orange',
- label: 'Google PaLM2'
+ label: 'Google PaLM2',
},
{
- value: 45,
+ value: 47,
color: 'blue',
- label: '字节火山方舟、豆包、DeepSeek通用'
+ label: 'Xinference',
},
{ value: 25, color: 'green', label: 'Moonshot' },
+ { value: 20, color: 'green', label: 'OpenRouter' },
{ value: 19, color: 'blue', label: '360 智脑' },
{ value: 23, color: 'teal', label: '腾讯混元' },
{ value: 31, color: 'green', label: '零一万物' },
@@ -97,16 +98,26 @@ export const CHANNEL_OPTIONS = [
{
value: 22,
color: 'blue',
- label: '知识库:FastGPT'
+ label: '知识库:FastGPT',
},
{
value: 21,
color: 'purple',
- label: '知识库:AI Proxy'
+ label: '知识库:AI Proxy',
},
{
value: 44,
color: 'purple',
- label: '嵌入模型:MokaAI M3E'
+ label: '嵌入模型:MokaAI M3E',
+ },
+ {
+ value: 45,
+ color: 'blue',
+ label: '字节火山方舟、豆包、DeepSeek通用',
+ },
+ {
+ value: 48,
+ color: 'blue',
+ label: 'xAI'
}
];
diff --git a/web/src/context/Style/index.js b/web/src/context/Style/index.js
index c3f28e62..db9b0dd1 100644
--- a/web/src/context/Style/index.js
+++ b/web/src/context/Style/index.js
@@ -9,8 +9,9 @@ export const StyleContext = React.createContext({
export const StyleProvider = ({ children }) => {
const [state, setState] = useState({
- isMobile: false,
+ isMobile: isMobile(),
showSider: false,
+ siderCollapsed: false,
shouldInnerPadding: false,
});
@@ -18,28 +19,37 @@ export const StyleProvider = ({ children }) => {
if ('type' in action) {
switch (action.type) {
case 'TOGGLE_SIDER':
- setState(prev => ({ ...prev, showSider: !prev.showSider }));
+ setState((prev) => ({ ...prev, showSider: !prev.showSider }));
break;
case 'SET_SIDER':
- setState(prev => ({ ...prev, showSider: action.payload }));
+ setState((prev) => ({ ...prev, showSider: action.payload }));
break;
case 'SET_MOBILE':
- setState(prev => ({ ...prev, isMobile: action.payload }));
+ setState((prev) => ({ ...prev, isMobile: action.payload }));
+ break;
+ case 'SET_SIDER_COLLAPSED':
+ setState((prev) => ({ ...prev, siderCollapsed: action.payload }));
break;
case 'SET_INNER_PADDING':
- setState(prev => ({ ...prev, shouldInnerPadding: action.payload }));
+ setState((prev) => ({ ...prev, shouldInnerPadding: action.payload }));
break;
default:
- setState(prev => ({ ...prev, ...action }));
+ setState((prev) => ({ ...prev, ...action }));
}
} else {
- setState(prev => ({ ...prev, ...action }));
+ setState((prev) => ({ ...prev, ...action }));
}
};
useEffect(() => {
const updateIsMobile = () => {
- dispatch({ type: 'SET_MOBILE', payload: isMobile() });
+ const mobileDetected = isMobile();
+ dispatch({ type: 'SET_MOBILE', payload: mobileDetected });
+
+ // If on mobile, we might want to auto-hide the sidebar
+ if (mobileDetected && state.showSider) {
+ dispatch({ type: 'SET_SIDER', payload: false });
+ }
};
updateIsMobile();
@@ -47,28 +57,44 @@ export const StyleProvider = ({ children }) => {
const updateShowSider = () => {
// check pathname
const pathname = window.location.pathname;
- if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
+ if (
+ pathname === '' ||
+ pathname === '/' ||
+ pathname.includes('/home') ||
+ pathname.includes('/chat')
+ ) {
+ dispatch({ type: 'SET_SIDER', payload: false });
+ dispatch({ type: 'SET_INNER_PADDING', payload: false });
+ } else if (pathname === '/setup') {
dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else {
- dispatch({ type: 'SET_SIDER', payload: true });
+ // Only show sidebar on non-mobile devices by default
+ dispatch({ type: 'SET_SIDER', payload: !isMobile() });
dispatch({ type: 'SET_INNER_PADDING', payload: true });
}
-
- if (isMobile()) {
- dispatch({ type: 'SET_SIDER', payload: false });
- }
};
- updateShowSider()
+ updateShowSider();
+ const updateSiderCollapsed = () => {
+ const isCollapsed =
+ localStorage.getItem('default_collapse_sidebar') === 'true';
+ dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed });
+ };
- // Optionally, add event listeners to handle window resize
- window.addEventListener('resize', updateIsMobile);
+ updateSiderCollapsed();
+
+ // Add event listeners to handle window resize
+ const handleResize = () => {
+ updateIsMobile();
+ };
+
+ window.addEventListener('resize', handleResize);
// Cleanup event listener on component unmount
return () => {
- window.removeEventListener('resize', updateIsMobile);
+ window.removeEventListener('resize', handleResize);
};
}, []);
diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js
index 635d63b8..84d2df1f 100644
--- a/web/src/helpers/api.js
+++ b/web/src/helpers/api.js
@@ -7,8 +7,8 @@ export let API = axios.create({
: '',
headers: {
'New-API-User': getUserIdFromLocalStorage(),
- 'Cache-Control': 'no-store'
- }
+ 'Cache-Control': 'no-store',
+ },
});
export function updateAPI() {
@@ -18,8 +18,8 @@ export function updateAPI() {
: '',
headers: {
'New-API-User': getUserIdFromLocalStorage(),
- 'Cache-Control': 'no-store'
- }
+ 'Cache-Control': 'no-store',
+ },
});
}
diff --git a/web/src/helpers/other.js b/web/src/helpers/other.js
index 3e172180..c5d8c269 100644
--- a/web/src/helpers/other.js
+++ b/web/src/helpers/other.js
@@ -1,7 +1,7 @@
-export function getLogOther(otherStr) {
- if (otherStr === undefined || otherStr === '') {
- otherStr = '{}'
- }
- let other = JSON.parse(otherStr)
- return other
-}
\ No newline at end of file
+export function getLogOther(otherStr) {
+ if (otherStr === undefined || otherStr === '') {
+ otherStr = '{}';
+ }
+ let other = JSON.parse(otherStr);
+ return other;
+}
diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js
index 3ac81420..4d1a3113 100644
--- a/web/src/helpers/render.js
+++ b/web/src/helpers/render.js
@@ -44,7 +44,10 @@ export function renderGroup(group) {
if (await copy(group)) {
showSuccess(i18next.t('已复制:') + group);
} else {
- Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: group });
+ Modal.error({
+ title: t('无法复制到剪贴板,请手动复制'),
+ content: group,
+ });
}
}}
>
@@ -64,28 +67,37 @@ export function renderRatio(ratio) {
} else if (ratio > 1) {
color = 'blue';
}
- return
{ratio}x {i18next.t('倍率')};
+ return (
+
+ {ratio}x {i18next.t('倍率')}
+
+ );
}
-const measureTextWidth = (text, style = {
- fontSize: '14px',
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
-}, containerWidth) => {
+const measureTextWidth = (
+ text,
+ style = {
+ fontSize: '14px',
+ fontFamily:
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
+ },
+ containerWidth,
+) => {
const span = document.createElement('span');
-
+
span.style.visibility = 'hidden';
span.style.position = 'absolute';
span.style.whiteSpace = 'nowrap';
span.style.fontSize = style.fontSize;
span.style.fontFamily = style.fontFamily;
-
+
span.textContent = text;
-
+
document.body.appendChild(span);
const width = span.offsetWidth;
-
+
document.body.removeChild(span);
-
+
return width;
};
@@ -94,7 +106,7 @@ export function truncateText(text, maxWidth = 200) {
return text;
}
if (!text) return text;
-
+
try {
// Handle percentage-based maxWidth
let actualMaxWidth = maxWidth;
@@ -103,19 +115,19 @@ export function truncateText(text, maxWidth = 200) {
// Use window width as fallback container width
actualMaxWidth = window.innerWidth * percentage;
}
-
+
const width = measureTextWidth(text);
if (width <= actualMaxWidth) return text;
-
+
let left = 0;
let right = text.length;
let result = text;
-
+
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const truncated = text.slice(0, mid) + '...';
const currentWidth = measureTextWidth(truncated);
-
+
if (currentWidth <= actualMaxWidth) {
result = truncated;
left = mid + 1;
@@ -123,10 +135,13 @@ export function truncateText(text, maxWidth = 200) {
right = mid - 1;
}
}
-
+
return result;
} catch (error) {
- console.warn('Text measurement failed, falling back to character count', error);
+ console.warn(
+ 'Text measurement failed, falling back to character count',
+ error,
+ );
if (text.length > 20) {
return text.slice(0, 17) + '...';
}
@@ -149,11 +164,11 @@ export const renderGroupOption = (item) => {
emptyContent,
...rest
} = item;
-
+
const baseStyle = {
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
padding: '8px 16px',
cursor: disabled ? 'not-allowed' : 'pointer',
backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
@@ -162,8 +177,8 @@ export const renderGroupOption = (item) => {
backgroundColor: 'var(--semi-color-primary-light-default)',
}),
'&:hover': {
- backgroundColor: !disabled && 'var(--semi-color-fill-1)'
- }
+ backgroundColor: !disabled && 'var(--semi-color-fill-1)',
+ },
};
const handleClick = () => {
@@ -177,9 +192,9 @@ export const renderGroupOption = (item) => {
onMouseEnter(e);
}
};
-
+
return (
-
{
{value}
-
+
{label}
@@ -222,8 +237,7 @@ export function renderQuotaNumberWithDigit(num, digits = 2) {
}
export function renderNumberWithPoint(num) {
- if (num === undefined)
- return '';
+ if (num === undefined) return '';
num = num.toFixed(2);
if (num >= 100000) {
// Convert number to string to manipulate it
@@ -302,11 +316,14 @@ export function renderModelPrice(
cacheRatio = 1.0,
) {
if (modelPrice !== -1) {
- return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
- price: modelPrice,
- ratio: groupRatio,
- total: modelPrice * groupRatio
- });
+ return i18next.t(
+ '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
+ {
+ price: modelPrice,
+ ratio: groupRatio,
+ total: modelPrice * groupRatio,
+ },
+ );
} else {
if (completionRatio === undefined) {
completionRatio = 0;
@@ -314,55 +331,72 @@ export function renderModelPrice(
let inputRatioPrice = modelRatio * 2.0;
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
-
+
// Calculate effective input tokens (non-cached + cached with ratio applied)
- const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio);
-
+ const effectiveInputTokens =
+ inputTokens - cacheTokens + cacheTokens * cacheRatio;
+
let price =
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
(completionTokens / 1000000) * completionRatioPrice * groupRatio;
-
+
return (
<>
- {i18next.t('提示价格:${{price}} = ${{total}} / 1M tokens', {
- price: inputRatioPrice,
- total: inputRatioPrice
- })}
- {i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', {
- price: inputRatioPrice,
- total: completionRatioPrice,
- completionRatio: completionRatio
- })}
- {cacheTokens > 0 && (
- {i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
+
+ {i18next.t('提示价格:${{price}} / 1M tokens', {
price: inputRatioPrice,
- total: inputRatioPrice * cacheRatio,
- cacheRatio: cacheRatio
- })}
+ })}
+
+
+ {i18next.t(
+ '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
+ {
+ price: inputRatioPrice,
+ total: completionRatioPrice,
+ completionRatio: completionRatio,
+ },
+ )}
+
+ {cacheTokens > 0 && (
+
+ {i18next.t(
+ '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
+ {
+ price: inputRatioPrice,
+ total: inputRatioPrice * cacheRatio,
+ cacheRatio: cacheRatio,
+ },
+ )}
+
)}
- {cacheTokens > 0 ?
- i18next.t('提示 {{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)
- }) :
- 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)
- })
- }
+ {cacheTokens > 0
+ ? i18next.t(
+ '提示 {{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),
+ },
+ )
+ : 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),
+ },
+ )}
{i18next.t('仅供参考,以实际扣费为准')}
@@ -381,19 +415,22 @@ export function renderModelPriceSimple(
if (modelPrice !== -1) {
return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
price: modelPrice,
- ratio: groupRatio
+ ratio: groupRatio,
});
} else {
if (cacheTokens !== 0) {
- return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}', {
- ratio: modelRatio,
- groupRatio: groupRatio,
- cacheRatio: cacheRatio
- });
+ return i18next.t(
+ '模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}',
+ {
+ ratio: modelRatio,
+ groupRatio: groupRatio,
+ cacheRatio: cacheRatio,
+ },
+ );
} else {
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
ratio: modelRatio,
- groupRatio: groupRatio
+ groupRatio: groupRatio,
});
}
}
@@ -415,11 +452,14 @@ export function renderAudioModelPrice(
) {
// 1 ratio = $0.002 / 1K tokens
if (modelPrice !== -1) {
- return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
- price: modelPrice,
- ratio: groupRatio,
- total: modelPrice * groupRatio
- });
+ return i18next.t(
+ '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
+ {
+ price: modelPrice,
+ ratio: groupRatio,
+ total: modelPrice * groupRatio,
+ },
+ );
} else {
if (completionRatio === undefined) {
completionRatio = 0;
@@ -431,82 +471,120 @@ export function renderAudioModelPrice(
let inputRatioPrice = modelRatio * 2.0;
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
-
+
// Calculate effective input tokens (non-cached + cached with ratio applied)
- const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio);
-
+ const effectiveInputTokens =
+ inputTokens - cacheTokens + cacheTokens * cacheRatio;
+
let textPrice =
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
- (completionTokens / 1000000) * completionRatioPrice * groupRatio
+ (completionTokens / 1000000) * completionRatioPrice * groupRatio;
let audioPrice =
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
- (audioCompletionTokens / 1000000) * inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio;
+ (audioCompletionTokens / 1000000) *
+ inputRatioPrice *
+ audioRatio *
+ audioCompletionRatio *
+ groupRatio;
let price = textPrice + audioPrice;
return (
<>
- {i18next.t('提示价格:${{price}} = ${{total}} / 1M tokens', {
- price: inputRatioPrice,
- total: inputRatioPrice
- })}
- {i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', {
- price: inputRatioPrice,
- total: completionRatioPrice,
- completionRatio: completionRatio
- })}
- {cacheTokens > 0 && (
- {i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
+
+ {i18next.t('提示价格:${{price}} / 1M tokens', {
price: inputRatioPrice,
- total: inputRatioPrice * cacheRatio,
- cacheRatio: cacheRatio
- })}
+ })}
+
+
+ {i18next.t(
+ '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
+ {
+ price: inputRatioPrice,
+ total: completionRatioPrice,
+ completionRatio: completionRatio,
+ },
+ )}
+
+ {cacheTokens > 0 && (
+
+ {i18next.t(
+ '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
+ {
+ price: inputRatioPrice,
+ total: inputRatioPrice * cacheRatio,
+ cacheRatio: cacheRatio,
+ },
+ )}
+
)}
- {i18next.t('音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})', {
- price: inputRatioPrice,
- total: inputRatioPrice * audioRatio,
- audioRatio: audioRatio
- })}
- {i18next.t('音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', {
- price: inputRatioPrice,
- total: inputRatioPrice * audioRatio * audioCompletionRatio,
- audioRatio: audioRatio,
- audioCompRatio: audioCompletionRatio
- })}
- {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,
+ {i18next.t(
+ '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})',
+ {
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)
- })
- }
+ total: inputRatioPrice * audioRatio,
+ audioRatio: audioRatio,
+ },
+ )}
- {i18next.t('音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}', {
- input: audioInputTokens,
- completion: audioCompletionTokens,
- audioInputPrice: audioRatio * inputRatioPrice,
- audioCompPrice: audioRatio * audioCompletionRatio * inputRatioPrice,
- total: audioPrice.toFixed(6)
- })}
+ {i18next.t(
+ '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})',
+ {
+ price: inputRatioPrice,
+ total: inputRatioPrice * audioRatio * audioCompletionRatio,
+ audioRatio: audioRatio,
+ audioCompRatio: audioCompletionRatio,
+ },
+ )}
- {i18next.t('总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}', {
- total: price.toFixed(6),
- textPrice: textPrice.toFixed(6),
- audioPrice: audioPrice.toFixed(6)
- })}
+ {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),
+ },
+ )
+ : 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),
+ },
+ )}
+
+
+ {i18next.t(
+ '音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}',
+ {
+ input: audioInputTokens,
+ completion: audioCompletionTokens,
+ audioInputPrice: audioRatio * inputRatioPrice,
+ audioCompPrice:
+ audioRatio * audioCompletionRatio * inputRatioPrice,
+ total: audioPrice.toFixed(6),
+ },
+ )}
+
+
+ {i18next.t(
+ '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}',
+ {
+ total: price.toFixed(6),
+ textPrice: textPrice.toFixed(6),
+ audioPrice: audioPrice.toFixed(6),
+ },
+ )}
{i18next.t('仅供参考,以实际扣费为准')}
@@ -519,7 +597,9 @@ export function renderQuotaWithPrompt(quota, digits) {
let displayInCurrency = localStorage.getItem('display_in_currency');
displayInCurrency = displayInCurrency === 'true';
if (displayInCurrency) {
- return ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
+ return (
+ ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''
+ );
}
return '';
}
@@ -539,7 +619,7 @@ const colors = [
'red',
'teal',
'violet',
- 'yellow'
+ 'yellow',
];
// 基础10色色板 (N ≤ 10)
@@ -553,7 +633,7 @@ const baseColors = [
'#304D77',
'#B48DEB',
'#009488',
- '#FF7DDA'
+ '#FF7DDA',
];
// 扩展20色色板 (10 < N ≤ 20)
@@ -577,7 +657,7 @@ const extendedColors = [
'#009488',
'#59BAA8',
'#FF7DDA',
- '#FFCFEE'
+ '#FFCFEE',
];
export const modelColorMap = {
@@ -633,14 +713,14 @@ export function modelToColor(modelName) {
// 2. 生成一个稳定的数字作为索引
let hash = 0;
for (let i = 0; i < modelName.length; i++) {
- hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
+ hash = (hash << 5) - hash + modelName.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer
}
hash = Math.abs(hash);
// 3. 根据模型名称长度选择不同的色板
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
-
+
// 4. 使用hash值选择颜色
const index = hash % colorPalette.length;
return colorPalette[index];
@@ -654,3 +734,229 @@ export function stringToColor(str) {
let i = sum % colors.length;
return colors[i];
}
+
+export function renderClaudeModelPrice(
+ inputTokens,
+ completionTokens,
+ modelRatio,
+ modelPrice = -1,
+ completionRatio,
+ groupRatio,
+ cacheTokens = 0,
+ cacheRatio = 1.0,
+ cacheCreationTokens = 0,
+ cacheCreationRatio = 1.0,
+) {
+ const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
+
+ if (modelPrice !== -1) {
+ return i18next.t(
+ '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
+ {
+ price: modelPrice,
+ ratioType: ratioLabel,
+ ratio: groupRatio,
+ total: modelPrice * groupRatio,
+ },
+ );
+ } else {
+ if (completionRatio === undefined) {
+ completionRatio = 0;
+ }
+
+ const completionRatioValue = completionRatio || 0;
+ const inputRatioPrice = modelRatio * 2.0;
+ const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
+ let cacheRatioPrice = (modelRatio * 2.0 * cacheRatio).toFixed(2);
+ let cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
+
+ // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
+ const nonCachedTokens = inputTokens;
+ const effectiveInputTokens =
+ nonCachedTokens +
+ cacheTokens * cacheRatio +
+ cacheCreationTokens * cacheCreationRatio;
+
+ let price =
+ (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
+ (completionTokens / 1000000) * completionRatioPrice * groupRatio;
+
+ return (
+ <>
+
+
+ {i18next.t('提示价格:${{price}} / 1M tokens', {
+ price: inputRatioPrice,
+ })}
+
+
+ {i18next.t(
+ '补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens',
+ {
+ price: inputRatioPrice,
+ ratio: completionRatio,
+ total: completionRatioPrice,
+ },
+ )}
+
+ {cacheTokens > 0 && (
+
+ {i18next.t(
+ '缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
+ {
+ price: inputRatioPrice,
+ ratio: cacheRatio,
+ total: cacheRatioPrice,
+ cacheRatio: cacheRatio,
+ },
+ )}
+
+ )}
+ {cacheCreationTokens > 0 && (
+
+ {i18next.t(
+ '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
+ {
+ price: inputRatioPrice,
+ ratio: cacheCreationRatio,
+ total: cacheCreationRatioPrice,
+ cacheCreationRatio: cacheCreationRatio,
+ },
+ )}
+
+ )}
+
+
+ {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),
+ },
+ )
+ : 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),
+ },
+ )}
+
+ {i18next.t('仅供参考,以实际扣费为准')}
+
+ >
+ );
+ }
+}
+
+export function renderClaudeLogContent(
+ modelRatio,
+ completionRatio,
+ modelPrice = -1,
+ groupRatio,
+ cacheRatio = 1.0,
+ cacheCreationRatio = 1.0,
+) {
+ const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
+
+ if (modelPrice !== -1) {
+ return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
+ price: modelPrice,
+ ratioType: ratioLabel,
+ ratio: groupRatio,
+ });
+ } else {
+ return i18next.t(
+ '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}',
+ {
+ modelRatio: modelRatio,
+ completionRatio: completionRatio,
+ cacheRatio: cacheRatio,
+ cacheCreationRatio: cacheCreationRatio,
+ ratioType: ratioLabel,
+ ratio: groupRatio,
+ },
+ );
+ }
+}
+
+export function renderClaudeModelPriceSimple(
+ modelRatio,
+ modelPrice = -1,
+ groupRatio,
+ cacheTokens = 0,
+ cacheRatio = 1.0,
+ cacheCreationTokens = 0,
+ cacheCreationRatio = 1.0,
+) {
+ const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组');
+
+ if (modelPrice !== -1) {
+ return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
+ price: modelPrice,
+ ratioType: ratioLabel,
+ ratio: groupRatio,
+ });
+ } else {
+ if (cacheTokens !== 0 || cacheCreationTokens !== 0) {
+ return i18next.t(
+ '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}',
+ {
+ ratio: modelRatio,
+ ratioType: ratioLabel,
+ groupRatio: groupRatio,
+ cacheRatio: cacheRatio,
+ cacheCreationRatio: cacheCreationRatio,
+ },
+ );
+ } else {
+ return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', {
+ ratio: modelRatio,
+ ratioType: ratioLabel,
+ groupRatio: groupRatio,
+ });
+ }
+ }
+}
+
+export function renderLogContent(
+ modelRatio,
+ completionRatio,
+ modelPrice = -1,
+ groupRatio,
+) {
+ const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
+
+ if (modelPrice !== -1) {
+ return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
+ price: modelPrice,
+ ratioType: ratioLabel,
+ ratio: groupRatio,
+ });
+ } else {
+ return i18next.t(
+ '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
+ {
+ modelRatio: modelRatio,
+ completionRatio: completionRatio,
+ ratioType: ratioLabel,
+ ratio: groupRatio,
+ },
+ );
+ }
+}
diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js
index a40b2079..38c25045 100644
--- a/web/src/helpers/utils.js
+++ b/web/src/helpers/utils.js
@@ -51,11 +51,11 @@ export async function copy(text) {
} catch (e) {
try {
// 构建input 执行 复制命令
- var _input = window.document.createElement("input");
+ var _input = window.document.createElement('input');
_input.value = text;
window.document.body.appendChild(_input);
_input.select();
- window.document.execCommand("Copy");
+ window.document.execCommand('Copy');
window.document.body.removeChild(_input);
} catch (e) {
okay = false;
@@ -143,6 +143,7 @@ export function openPage(url) {
}
export function removeTrailingSlash(url) {
+ if (!url) return '';
if (url.endsWith('/')) {
return url.slice(0, -1);
} else {
@@ -191,7 +192,7 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
let day = date.getDate().toString();
let hour = date.getHours().toString();
if (day === '24') {
- console.log("timestamp", timestamp);
+ console.log('timestamp', timestamp);
}
if (month.length === 1) {
month = '0' + month;
@@ -247,7 +248,6 @@ export function verifyJSONPromise(value) {
}
}
-
export function shouldShowPrompt(id) {
let prompt = localStorage.getItem(`prompt-${id}`);
return !prompt;
diff --git a/web/src/i18n/i18n.js b/web/src/i18n/i18n.js
index f0d6687d..c1bf5860 100644
--- a/web/src/i18n/i18n.js
+++ b/web/src/i18n/i18n.js
@@ -11,16 +11,16 @@ i18n
.init({
resources: {
en: {
- translation: enTranslation
+ translation: enTranslation,
},
zh: {
- translation: zhTranslation
- }
+ translation: zhTranslation,
+ },
},
fallbackLng: 'zh',
interpolation: {
- escapeValue: false
- }
+ escapeValue: false,
+ },
});
-export default i18n;
\ No newline at end of file
+export default i18n;
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index 9951534e..8aaaab77 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -1,5 +1,6 @@
{
"主页": "Home",
+ "文档": "Docs",
"控制台": "Console",
"$%.6f 额度": "$%.6f quota",
"%d 点额度": "%d point quota",
@@ -192,6 +193,8 @@
"通用设置": "General Settings",
"充值链接": "Recharge Link",
"例如发卡网站的购买链接": "E.g., purchase link from card issuing website",
+ "文档地址": "Document Link",
+ "例如 https://docs.newapi.pro": "E.g., https://docs.newapi.pro",
"聊天页面链接": "Chat Page Link",
"例如 ChatGPT Next Web 的部署地址": "E.g., ChatGPT Next Web deployment address",
"单位美元额度": "Quota per USD",
@@ -489,7 +492,7 @@
"请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters",
"默认": "default",
"图片演示": "Image demo",
- "参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)",
+ "注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.5-preview会请求为gpt-45-preview,所以部署的模型名称需要去掉点": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.5-preview will be requested as gpt-45-preview, so the deployed model name needs to remove the dot",
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
"取消无限额度": "Cancel unlimited quota",
"取消": "Cancel",
@@ -511,7 +514,7 @@
",图片演示。": "related image demo.",
"令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!",
"代理": "Proxy",
- "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com",
+ "此项可选,用于通过自定义API地址来进行 API 调用,请输入API地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com",
"取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?",
"按照如下格式输入:": "Enter in the following format:",
"模型版本": "Model version",
@@ -1062,7 +1065,7 @@
"价格:${{price}} * 分组:{{ratio}}": "Price: ${{price}} * Group: {{ratio}}",
"模型: {{ratio}} * 分组: {{groupRatio}}": "Model: {{ratio}} * Group: {{groupRatio}}",
"统计额度": "Statistical quota",
- "统计Tokens": "Statistical Tokens",
+ "统计Tokens": "Statistical Tokens",
"统计次数": "Statistical count",
"平均RPM": "Average RPM",
"平均TPM": "Average TPM",
@@ -1108,7 +1111,7 @@
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.",
"完整的 Base URL,支持变量{model}": "Complete Base URL, supports variable {model}",
"请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Please enter complete URL, e.g.: https://api.openai.com/v1/chat/completions",
- "此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through proxy sites, do not end with /v1 and /",
+ "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through custom API address, do not add /v1 and / at the end",
"私有部署地址": "Private Deployment Address",
"请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Please enter private deployment address, format: https://fastgpt.run/api/openapi",
"注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Note: For non-Chat API, please make sure to enter the correct API address, otherwise it may not work",
@@ -1269,9 +1272,10 @@
"通知邮箱": "Notification email",
"设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Set the email address for receiving quota warning notifications, if not set, the email address bound to the account will be used",
"留空则使用账号绑定的邮箱": "If left blank, the email address bound to the account will be used",
- "代理站地址": "Base URL",
+ "API地址": "Base URL",
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in",
"渠道额外设置": "Channel extra settings",
+ "参数覆盖": "Parameters override",
"模型请求速率限制": "Model request rate limit",
"启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",
"限制周期": "Limit period",
@@ -1342,5 +1346,26 @@
"提示缓存倍率": "Prompt cache ratio",
"缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (cache ratio: {{cacheRatio}})",
"提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{nonCacheInput}} tokens + cache {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + completion {{completion}} tokens / 1M tokens * ${{compPrice}} * group {{ratio}} = ${{total}}",
- "缓存 Tokens": "Cache Tokens"
+ "缓存 Tokens": "Cache Tokens",
+ "系统初始化": "System initialization",
+ "管理员账号已经初始化过,请继续设置系统参数": "The admin account has already been initialized, please continue to set the system parameters",
+ "管理员账号": "Admin account",
+ "请输入管理员用户名": "Please enter the admin username",
+ "请输入管理员密码": "Please enter the admin password",
+ "请确认管理员密码": "Please confirm the admin password",
+ "请选择使用模式": "Please select the usage mode",
+ "数据库警告": "Database warning",
+ "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "You are using the SQLite database. If you are running in a container environment, please ensure that the database file persistence mapping is correctly set, otherwise all data will be lost after container restart!",
+ "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "It is recommended to use MySQL or PostgreSQL databases in production environments, or ensure that the SQLite database file is mapped to the persistent storage of the host machine.",
+ "使用模式": "Usage mode",
+ "对外运营模式": "Default mode",
+ "密码长度至少为8个字符": "Password must be at least 8 characters long",
+ "表单引用错误,请刷新页面重试": "Form reference error, please refresh the page and try again",
+ "默认模式,适用于为多个用户提供服务的场景。": "Default mode, suitable for scenarios where multiple users are provided.",
+ "此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。": "In this mode, the system will calculate the usage of each call, you need to set the price for each model, if the price is not set, the user will not be able to use the model.",
+ "适用于个人使用的场景。": "Suitable for personal use.",
+ "不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。": "No need to set the model price, the system will weaken the usage calculation, you can focus on using the model.",
+ "适用于展示系统功能的场景。": "Suitable for scenarios where the system functions are displayed.",
+ "可在初始化后修改": "Can be modified after initialization",
+ "初始化系统": "Initialize system"
}
diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json
index b2cf894c..5c7904fc 100644
--- a/web/src/i18n/locales/zh.json
+++ b/web/src/i18n/locales/zh.json
@@ -10,4 +10,4 @@
"展开侧边栏": "展开侧边栏",
"关闭侧边栏": "关闭侧边栏",
"注销成功!": "注销成功!"
-}
\ No newline at end of file
+}
diff --git a/web/src/index.css b/web/src/index.css
index e5f9b530..c2e8ecd0 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -1,9 +1,8 @@
body {
margin: 0;
- padding-top: 55px;
- overflow-y: scroll;
- font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
- sans-serif;
+ padding-top: 0;
+ font-family:
+ Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scrollbar-width: none;
@@ -13,11 +12,26 @@ body {
}
#root {
- height: 100vh;
+ height: 100%;
+ display: flex;
flex-direction: column;
+ overflow: hidden;
}
-#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li > span{
+#root
+ > section
+ > header
+ > section
+ > div
+ > div
+ > div
+ > div.semi-navigation-header-list-outer
+ > div.semi-navigation-list-wrapper
+ > ul
+ > div
+ > a
+ > li
+ > span {
font-weight: 600 !important;
}
@@ -29,18 +43,59 @@ body {
/*.semi-navigation-sub-wrap .semi-navigation-sub-title, .semi-navigation-item {*/
/* padding: 0 0;*/
/*}*/
+ .topnav {
+ padding: 0 8px;
+ }
+
+ .topnav .semi-navigation-item {
+ margin: 0 1px;
+ padding: 0 4px;
+ }
+
.topnav .semi-navigation-list-wrapper {
max-width: calc(55vw - 20px);
overflow-x: auto;
scrollbar-width: none;
}
- #root > section > header > section > div > div > div > div.semi-navigation-footer > div > a > li {
+ #root
+ > section
+ > header
+ > section
+ > div
+ > div
+ > div
+ > div.semi-navigation-footer
+ > div
+ > a
+ > li {
padding: 0 0;
}
- #root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
+ #root
+ > section
+ > header
+ > section
+ > div
+ > div
+ > div
+ > div.semi-navigation-header-list-outer
+ > div.semi-navigation-list-wrapper
+ > ul
+ > div
+ > a
+ > li {
padding: 0 5px;
}
- #root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
+ #root
+ > section
+ > header
+ > section
+ > div
+ > div
+ > div
+ > div.semi-navigation-footer
+ > div:nth-child(1)
+ > a
+ > li {
padding: 0 5px;
}
.semi-navigation-footer {
@@ -72,6 +127,31 @@ body {
.semi-navigation-horizontal .semi-navigation-header {
margin-right: 0;
}
+
+ /* 确保移动端内容可滚动 */
+ .semi-layout-content {
+ -webkit-overflow-scrolling: touch !important;
+ overscroll-behavior-y: auto !important;
+ }
+
+ /* 修复移动端下拉刷新 */
+ body {
+ overflow: visible !important;
+ overscroll-behavior-y: auto !important;
+ position: static !important;
+ height: 100% !important;
+ }
+
+ /* 确保内容区域在移动端可以正常滚动 */
+ #root {
+ overflow: visible !important;
+ height: 100% !important;
+ }
+
+ /* 隐藏在移动设备上 */
+ .hide-on-mobile {
+ display: none !important;
+ }
}
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
@@ -112,23 +192,55 @@ body::-webkit-scrollbar {
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
+ font-family:
+ source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.semi-navigation-item {
margin-bottom: 0;
}
-.semi-navigation-vertical {
- /*flex: 0 0 auto;*/
- /*display: flex;*/
- /*flex-direction: column;*/
- /*width: 100%;*/
- height: 100%;
+/* 自定义侧边栏按钮悬停效果 */
+.semi-navigation-item:hover {
+ transform: translateX(2px);
+ box-shadow: 0 2px 8px rgba(var(--semi-color-primary-rgb), 0.2);
+}
+
+/* 自定义侧边栏按钮选中效果 */
+.semi-navigation-item-selected {
+ position: relative;
overflow: hidden;
}
+.semi-navigation-item-selected::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 4px;
+ background-color: var(--semi-color-primary);
+ animation: slideIn 0.3s ease;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateY(-100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+/*.semi-navigation-vertical {*/
+/* !*flex: 0 0 auto;*!*/
+/* !*display: flex;*!*/
+/* !*flex-direction: column;*!*/
+/* !*width: 100%;*!*/
+/* height: 100%;*/
+/* overflow: hidden;*/
+/*}*/
+
.main-content {
padding: 4px;
height: 100%;
@@ -142,8 +254,67 @@ code {
font-size: 1.1em;
}
-@media only screen and (max-width: 600px) {
- .hide-on-mobile {
- display: none !important;
- }
+/* 顶部栏样式 */
+.topnav {
+ padding: 0 16px;
}
+
+.topnav .semi-navigation-item {
+ border-radius: 4px;
+ margin: 0 2px;
+ transition: all 0.3s ease;
+}
+
+.topnav .semi-navigation-item:hover {
+ background-color: var(--semi-color-primary-light-default);
+ transform: translateY(-2px);
+ box-shadow: 0 2px 8px rgba(var(--semi-color-primary-rgb), 0.2);
+}
+
+.topnav .semi-navigation-item-selected {
+ background-color: var(--semi-color-primary-light-default);
+ color: var(--semi-color-primary);
+ font-weight: 600;
+}
+
+/* 顶部栏文本样式 */
+.header-bar-text {
+ color: var(--semi-color-text-0);
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.header-bar-text:hover {
+ color: var(--semi-color-primary);
+}
+
+/* 自定义滚动条样式 */
+.semi-layout-content::-webkit-scrollbar,
+.semi-sider::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+.semi-layout-content::-webkit-scrollbar-thumb,
+.semi-sider::-webkit-scrollbar-thumb {
+ background: var(--semi-color-tertiary-light-default);
+ border-radius: 3px;
+}
+
+.semi-layout-content::-webkit-scrollbar-thumb:hover,
+.semi-sider::-webkit-scrollbar-thumb:hover {
+ background: var(--semi-color-tertiary);
+}
+
+.semi-layout-content::-webkit-scrollbar-track,
+.semi-sider::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+/* Custom sidebar shadow */
+/*.custom-sidebar-nav {*/
+/* box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
+/* -webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
+/* -moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
+/* min-height: 100%;*/
+/*}*/
diff --git a/web/src/index.js b/web/src/index.js
index bc16e36b..aa709ff8 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -28,7 +28,7 @@ root.render(
-
+
diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js
index bfc611fe..037f5e18 100644
--- a/web/src/pages/Channel/EditChannel.js
+++ b/web/src/pages/Channel/EditChannel.js
@@ -7,7 +7,8 @@ import {
showError,
showInfo,
showSuccess,
- verifyJSON
+ showWarning,
+ verifyJSON,
} from '../../helpers';
import { CHANNEL_OPTIONS } from '../../constants';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
@@ -22,27 +23,24 @@ import {
Select,
TextArea,
Checkbox,
- Banner
+ Banner,
+ Modal,
} from '@douyinfe/semi-ui';
-import { Divider } from 'semantic-ui-react';
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
-import axios from 'axios';
const MODEL_MAPPING_EXAMPLE = {
- 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
+ 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
};
const STATUS_CODE_MAPPING_EXAMPLE = {
- 400: '500'
+ 400: '500',
};
const REGION_EXAMPLE = {
- 'default': 'us-central1',
- 'claude-3-5-sonnet-20240620': 'europe-west1'
+ default: 'us-central1',
+ 'claude-3-5-sonnet-20240620': 'europe-west1',
};
-const fetchButtonTips = '1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出';
-
function type2secretPrompt(type) {
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
switch (type) {
@@ -86,7 +84,7 @@ const EditChannel = (props) => {
groups: ['default'],
priority: 0,
weight: 0,
- tag: ''
+ tag: '',
};
const [batch, setBatch] = useState(false);
const [autoBan, setAutoBan] = useState(true);
@@ -99,6 +97,17 @@ const EditChannel = (props) => {
const [fullModels, setFullModels] = useState([]);
const [customModel, setCustomModel] = useState('');
const handleInputChange = (name, value) => {
+ if (name === 'base_url' && value.endsWith('/v1')) {
+ Modal.confirm({
+ title: '警告',
+ content:
+ '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
+ onOk: () => {
+ setInputs((inputs) => ({ ...inputs, [name]: value }));
+ },
+ });
+ return;
+ }
setInputs((inputs) => ({ ...inputs, [name]: value }));
if (name === 'type') {
let localModels = [];
@@ -111,7 +120,7 @@ const EditChannel = (props) => {
'mj_blend',
'mj_upscale',
'mj_describe',
- 'mj_uploads'
+ 'mj_uploads',
];
break;
case 5:
@@ -131,14 +140,11 @@ const EditChannel = (props) => {
'mj_high_variation',
'mj_low_variation',
'mj_pan',
- 'mj_uploads'
+ 'mj_uploads',
];
break;
case 36:
- localModels = [
- 'suno_music',
- 'suno_lyrics'
- ];
+ localModels = ['suno_music', 'suno_lyrics'];
break;
default:
localModels = getChannelModels(value);
@@ -174,7 +180,7 @@ const EditChannel = (props) => {
data.model_mapping = JSON.stringify(
JSON.parse(data.model_mapping),
null,
- 2
+ 2,
);
}
setInputs(data);
@@ -191,7 +197,6 @@ const EditChannel = (props) => {
setLoading(false);
};
-
const fetchUpstreamModelList = async (name) => {
// if (inputs['type'] !== 1) {
// showError(t('仅支持 OpenAI 接口格式'));
@@ -219,9 +224,9 @@ const EditChannel = (props) => {
const res = await API.post('/api/channel/fetch_models', {
base_url: inputs['base_url'],
type: inputs['type'],
- key: inputs['key']
+ key: inputs['key'],
});
-
+
if (res.data && res.data.success) {
models.push(...res.data.data);
} else {
@@ -248,7 +253,7 @@ const EditChannel = (props) => {
let res = await API.get(`/api/channel/models`);
let localModelOptions = res.data.data.map((model) => ({
label: model.id,
- value: model.id
+ value: model.id,
}));
setOriginModelOptions(localModelOptions);
setFullModels(res.data.data.map((model) => model.id));
@@ -257,7 +262,7 @@ const EditChannel = (props) => {
.filter((model) => {
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
})
- .map((model) => model.id)
+ .map((model) => model.id),
);
} catch (error) {
showError(error.message);
@@ -273,8 +278,8 @@ const EditChannel = (props) => {
setGroupOptions(
res.data.data.map((group) => ({
label: group,
- value: group
- }))
+ value: group,
+ })),
);
} catch (error) {
showError(error.message);
@@ -287,7 +292,7 @@ const EditChannel = (props) => {
if (!localModelOptions.find((option) => option.label === model)) {
localModelOptions.push({
label: model,
- value: model
+ value: model,
});
}
});
@@ -298,7 +303,7 @@ const EditChannel = (props) => {
fetchModels().then();
fetchGroups().then();
if (isEdit) {
- loadChannel().then(() => {});
+ loadChannel().then(() => { });
} else {
setInputs(originInputs);
let localModels = getChannelModels(inputs.type);
@@ -324,7 +329,7 @@ const EditChannel = (props) => {
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
localInputs.base_url = localInputs.base_url.slice(
0,
- localInputs.base_url.length - 1
+ localInputs.base_url.length - 1,
);
}
if (localInputs.type === 18 && localInputs.other === '') {
@@ -342,7 +347,7 @@ const EditChannel = (props) => {
if (isEdit) {
res = await API.put(`/api/channel/`, {
...localInputs,
- id: parseInt(channelId)
+ id: parseInt(channelId),
});
} else {
res = await API.post(`/api/channel/`, localInputs);
@@ -376,7 +381,7 @@ const EditChannel = (props) => {
localModelOptions.push({
key: model,
text: model,
- value: model
+ value: model,
});
} else if (model) {
showError(t('某些模型已存在!'));
@@ -391,14 +396,15 @@ const EditChannel = (props) => {
handleInputChange('models', localModels);
};
-
return (
<>
{isEdit ? t('更新渠道信息') : t('创建新的渠道')}
+
+ {isEdit ? t('更新渠道信息') : t('创建新的渠道')}
+
}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -406,11 +412,11 @@ const EditChannel = (props) => {
footer={
-