Compare commits

...

27 Commits

Author SHA1 Message Date
CalciumIon
263547ebb7 refactor: Simplify average calculations in Detail component
- Streamlined the calculation of average RPM and average TPM by removing unnecessary function calls and directly applying the `toFixed(3)` method within the JSX.
- Improved code readability and maintainability by reducing the number of lines and enhancing clarity in the calculation logic.
2024-12-12 19:21:08 +08:00
CalciumIon
5d338337a0 feat: 兼容OpenAI格式下设置gemini模型联网搜索 #615 2024-12-12 17:58:25 +08:00
CalciumIon
b1fb595610 feat: add model gemini-2.0-flash-exp 2024-12-12 17:21:37 +08:00
CalciumIon
44512d3c28 feat: Enhance group label display in Playground component
- Updated the group selection input to truncate long labels on mobile devices, ensuring better readability and a cleaner interface.
- Implemented a conditional label adjustment that shortens group names exceeding 18 characters, appending '...' for clarity.
2024-12-12 16:35:13 +08:00
Calcium-Ion
430d5fcd6a Merge pull request #616 from Calcium-Ion/panel
feat: 完善数据看板功能
2024-12-12 16:19:27 +08:00
CalciumIon
6625563f80 feat: Enhance quota data handling and CSS styling
- Updated the `increaseQuotaData` function to include `tokenUsed` parameter for better quota tracking.
- Modified the `GetAllQuotaDates` function to sum `token_used` alongside `count` and `quota` for comprehensive data retrieval.
- Improved CSS styles for better layout responsiveness, including padding adjustments for navigation elements and description cards.
2024-12-12 16:18:14 +08:00
CalciumIon
b2d36b946d feat: Update SiderBar and Detail components for improved navigation and data visualization
- Removed the '模型价格' (Pricing) link from the SiderBar for a cleaner interface.
- Added a new '数据看板' (Data Dashboard) link to the SiderBar, enhancing navigation options.
- Refactored the Detail component to include user context and style context for better state management.
- Introduced new state variables to track token consumption and updated data handling for charts.
- Enhanced the layout with additional cards and tabs for displaying user quota and usage statistics.
- Improved data processing logic for pie and line charts, ensuring accurate representation of user data.
2024-12-12 16:11:17 +08:00
CalciumIon
ab4c9fdb8f feat: Enhance color mapping and chart rendering in Detail component
- Added base and extended color palettes for improved model color mapping.
- Introduced a new `modelToColor` function to dynamically assign colors based on model names.
- Updated the Detail component to utilize the new color mapping for pie and line charts.
- Refactored chart data handling to support dynamic color assignment and improved data visualization.
- Cleaned up unused state variables and optimized data loading logic for better performance.
2024-12-12 14:56:16 +08:00
CalciumIon
79de02b05f chore: Update dependencies and refactor JSON handling #614
- Removed the `bytedance/sonic` dependency and replaced its usage with the standard `encoding/json` package for JSON marshalling in `relay-text.go`.
- Updated `go.mod` to reflect the removal of `sonic` and adjusted the version of `sonic/loader`.
- Cleaned up `go.sum` to ensure consistency with the updated dependencies.
2024-12-12 14:14:24 +08:00
Calcium-Ion
0455f30d16 Merge pull request #613 from Calcium-Ion/mobile
feat: Add pricing link to HeaderBar component
2024-12-11 23:14:45 +08:00
Calcium-Ion
21d4dcadab Merge pull request #612 from Calcium-Ion/mobile
feat: Refactor style management for inner padding in layout components
2024-12-11 23:14:10 +08:00
CalciumIon
f0d9c89659 feat: Add pricing link to HeaderBar component
- Introduced a new '定价' (Pricing) item in the HeaderBar navigation for better accessibility to pricing information.
- Updated routing to include the new '/pricing' path.
- Adjusted user display in the HeaderBar for mobile responsiveness, hiding the username on smaller screens for a cleaner interface.
2024-12-11 23:13:46 +08:00
CalciumIon
28fa77cc92 feat: Refactor style management for inner padding in layout components
- Updated HeaderBar, PageLayout, and SiderBar components to manage inner padding state based on selected items.
- Replaced `isChatPage` state with `shouldInnerPadding` in Style context for better clarity and functionality.
- Enhanced user experience by dynamically adjusting content padding based on navigation selections.
2024-12-11 23:08:52 +08:00
Calcium-Ion
db84b26e2d Merge pull request #611 from Calcium-Ion/mobile
feat: 前端美化
2024-12-11 21:41:09 +08:00
CalciumIon
024cdb08df feat: Update model lists and enhance model retrieval in Adaptor
- Refactored ModelList in the gemini constant to include new models and remove outdated ones.
- Modified the GetModelList function in the Adaptor to consolidate model lists from multiple sources, ensuring a comprehensive and updated list is returned.
- Commented out deprecated models in the vertex constants for clarity and future reference.
2024-12-11 21:39:41 +08:00
CalciumIon
89136dfa9e feat: Add filtering and search functionality to model selection in EditChannel and EditTagModal
- Implemented filter and search position options in the model selection dropdowns for both EditChannel and EditTagModal components.
- Enhanced user experience by allowing users to easily find and select models from a potentially large list.
2024-12-11 21:33:30 +08:00
CalciumIon
5f0322b672 feat: Add custom model input functionality in EditTagModal
- Introduced a new input field for adding custom model names in the EditTagModal component.
- Implemented logic to handle the addition of custom models, including validation to prevent duplicates.
- Enhanced user experience by providing feedback when attempting to add existing models.
- Updated state management to reflect changes in the model options dynamically.
2024-12-11 21:31:29 +08:00
CalciumIon
379b08f691 feat: Update user group handling in Playground component
- Enhanced the Playground component to prioritize the user's group by moving it to the front of the local group options if it exists.
- Improved user experience by ensuring the default group selection reflects the user's current group, if available.
2024-12-11 21:25:50 +08:00
CalciumIon
afb7b661ee feat: Implement chat page state management in layout and sidebar
- Added `isChatPage` state to the Style context to manage chat page layout.
- Updated `PageLayout` component to adjust padding based on the chat page state.
- Enhanced `SiderBar` component to dispatch chat page state changes when chat-related items are selected.
2024-12-11 21:17:46 +08:00
CalciumIon
60710d6c68 feat: Add renderModelPriceSimple function and update LogsTable component
- Introduced a new helper function `renderModelPriceSimple` to simplify the rendering of model price information.
- Updated the `LogsTable` component to utilize `renderModelPriceSimple`, enhancing the display of model pricing and grouping information.
- Removed the previous implementation of `renderModelPrice` from the `LogsTable` for cleaner code.
2024-12-11 21:06:26 +08:00
CalciumIon
77b8d918de refactor: Simplify PersonalSetting component layout
- Moved footer content from the Card component to a separate Descriptions component for better structure.
- Maintained the display of user quota, historical consumption, and request count while improving readability.
2024-12-11 20:36:44 +08:00
Calcium-Ion
69f57728b2 Merge pull request #610 from Calcium-Ion/mobile
feat: Update dependencies and restructure Playground component
2024-12-11 18:28:11 +08:00
CalciumIon
7cab9d7c8a feat: Update dependencies and restructure Playground component
- Upgraded @douyinfe/semi-ui from version 2.63.1 to 2.69.1 in package.json.
- Updated pnpm-lock.yaml to reflect new dependency versions and lockfile format.
- Moved Playground component to a new directory structure under pages.
- Enhanced Playground component with new features and improved user experience.
2024-12-11 18:27:30 +08:00
Calcium-Ion
e5dc21d56b Merge pull request #609 from Calcium-Ion/mobile
feat: 界面美化
2024-12-11 17:33:32 +08:00
CalciumIon
713de36ecd feat: Enhance EditRedemption component with default name handling 2024-12-11 17:28:59 +08:00
CalciumIon
64e085dc4c feat: 首页优化 2024-12-11 17:19:03 +08:00
CalciumIon
3622c664b6 feat: 侧边栏移动端优化 2024-12-11 16:11:27 +08:00
27 changed files with 3669 additions and 2837 deletions

4
go.mod
View File

@@ -11,7 +11,6 @@ require (
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
github.com/bytedance/sonic v1.12.4
github.com/gin-contrib/cors v1.7.2
github.com/gin-contrib/gzip v0.0.6
github.com/gin-contrib/sessions v0.0.5
@@ -42,7 +41,8 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect

9
go.sum
View File

@@ -22,11 +22,10 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0=
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
@@ -205,6 +204,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -270,3 +270,4 @@ gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -85,7 +85,7 @@ func SaveQuotaDataCache() {
//quotaDataDB.Count += quotaData.Count
//quotaDataDB.Quota += quotaData.Quota
//DB.Table("quota_data").Save(quotaDataDB)
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt)
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt, quotaData.TokenUsed)
} else {
DB.Table("quota_data").Create(quotaData)
}
@@ -94,11 +94,12 @@ func SaveQuotaDataCache() {
common.SysLog(fmt.Sprintf("保存数据看板数据成功,共保存%d条数据", size))
}
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64) {
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64, tokenUsed int) {
err := DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?",
userId, username, modelName, createdAt).Updates(map[string]interface{}{
"count": gorm.Expr("count + ?", count),
"quota": gorm.Expr("quota + ?", quota),
"count": gorm.Expr("count + ?", count),
"quota": gorm.Expr("quota + ?", quota),
"token_used": gorm.Expr("token_used + ?", tokenUsed),
}).Error
if err != nil {
common.SysLog(fmt.Sprintf("increaseQuotaData error: %s", err))
@@ -127,6 +128,6 @@ func GetAllQuotaDates(startTime int64, endTime int64, username string) (quotaDat
// 从quota_data表中查询数据
// only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at;
//err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find(&quotaDatas).Error
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find(&quotaDatas).Error
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find(&quotaDatas).Error
return quotaDatas, err
}

View File

@@ -5,9 +5,10 @@ const (
)
var ModelList = []string{
"gemini-1.0-pro-latest", "gemini-1.0-pro-001", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
"gemini-1.0-pro-vision-latest", "gemini-1.0-pro-vision-001", "gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
"gemini-exp-1114",
"gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
"gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
"gemini-exp-1114", "gemini-exp-1206",
"gemini-2.0-flash-exp",
}
var ChannelName = "google gemini"

View File

@@ -34,6 +34,7 @@ type GeminiChatSafetySettings struct {
}
type GeminiChatTools struct {
GoogleSearch any `json:"googleSearch,omitempty"`
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
}

View File

@@ -45,13 +45,25 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
}
if textRequest.Tools != nil {
functions := make([]dto.FunctionCall, 0, len(textRequest.Tools))
googleSearch := false
for _, tool := range textRequest.Tools {
if tool.Function.Name == "googleSearch" {
googleSearch = true
continue
}
functions = append(functions, tool.Function)
}
geminiRequest.Tools = []GeminiChatTools{
{
FunctionDeclarations: functions,
},
if len(functions) > 0 {
geminiRequest.Tools = []GeminiChatTools{
{
FunctionDeclarations: functions,
},
}
}
if googleSearch {
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTools{
GoogleSearch: make(map[string]string),
})
}
} else if textRequest.Functions != nil {
geminiRequest.Tools = []GeminiChatTools{
@@ -134,7 +146,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
shouldAddDummyModelMessage = false
}
}
return &geminiRequest
}

View File

@@ -176,7 +176,20 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
}
func (a *Adaptor) GetModelList() []string {
return ModelList
var modelList []string
for i, s := range ModelList {
modelList = append(modelList, s)
ModelList[i] = s
}
for i, s := range claude.ModelList {
modelList = append(modelList, s)
claude.ModelList[i] = s
}
for i, s := range gemini.ModelList {
modelList = append(modelList, s)
gemini.ModelList[i] = s
}
return modelList
}
func (a *Adaptor) GetChannelName() string {

View File

@@ -1,13 +1,13 @@
package vertex
var ModelList = []string{
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"claude-3-haiku-20240307",
"claude-3-5-sonnet-20240620",
//"claude-3-sonnet-20240229",
//"claude-3-opus-20240229",
//"claude-3-haiku-20240307",
//"claude-3-5-sonnet-20240620",
//"gemini-1.5-pro-latest", "gemini-1.5-flash-latest",
"gemini-1.5-pro-001", "gemini-1.5-flash-001", "gemini-pro", "gemini-pro-vision",
//"gemini-1.5-pro-001", "gemini-1.5-flash-001", "gemini-pro", "gemini-pro-vision",
"meta/llama3-405b-instruct-maas",
}

View File

@@ -18,8 +18,6 @@ import (
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/gin-gonic/gin"
)
@@ -180,7 +178,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
}
jsonData, err := sonic.Marshal(convertedRequest)
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}

View File

@@ -5,7 +5,7 @@
"type": "module",
"dependencies": {
"@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-ui": "^2.63.1",
"@douyinfe/semi-ui": "^2.69.1",
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8",

5166
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ import { Layout } from '@douyinfe/semi-ui';
import Midjourney from './pages/Midjourney';
import Pricing from './pages/Pricing/index.js';
import Task from "./pages/Task/index.js";
import Playground from './components/Playground.js';
import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js";
const Home = lazy(() => import('./pages/Home'));

View File

@@ -9,17 +9,19 @@ import '../index.css';
import fireworks from 'react-fireworks';
import {
IconClose,
IconHelpCircle,
IconHome,
IconHomeStroked,
IconKey,
IconHomeStroked, IconIndentLeft,
IconKey, IconMenu,
IconNoteMoneyStroked,
IconPriceTag,
IconUser
} from '@douyinfe/semi-icons';
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { Avatar, Button, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { StyleContext } from '../context/Style/index.js';
// HeaderBar Buttons
let headerButtons = [
@@ -31,21 +33,6 @@ let headerButtons = [
},
];
let buttons = [
{
text: '首页',
itemKey: 'home',
to: '/',
// icon: <IconHomeStroked />,
},
// {
// text: 'Playground',
// itemKey: 'playground',
// to: '/playground',
// // icon: <IconNoteMoneyStroked />,
// },
];
if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, {
name: '聊天',
@@ -56,9 +43,9 @@ if (localStorage.getItem('chat_link')) {
const HeaderBar = () => {
const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
let navigate = useNavigate();
const [showSidebar, setShowSidebar] = useState(false);
const systemName = getSystemName();
const logo = getLogo();
const currentDate = new Date();
@@ -69,8 +56,25 @@ const HeaderBar = () => {
currentDate.getDate() >= 9 &&
currentDate.getDate() <= 24);
let buttons = [
{
text: '首页',
itemKey: 'home',
to: '/',
},
{
text: '控制台',
itemKey: 'detail',
to: '/',
},
{
text: '定价',
itemKey: 'pricing',
to: '/pricing',
},
];
async function logout() {
setShowSidebar(false);
await API.get('/api/user/logout');
showSuccess('注销成功!');
userDispatch({ type: 'logout' });
@@ -108,36 +112,57 @@ const HeaderBar = () => {
<div style={{ width: '100%' }}>
<Nav
mode={'horizontal'}
// bodyStyle={{ height: 100 }}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
const routerMap = {
about: '/about',
login: '/login',
register: '/register',
pricing: '/pricing',
detail: '/detail',
home: '/',
};
return (
<Link
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
>
{itemElement}
</Link>
<div onClick={(e) => {
if (props.itemKey === 'home') {
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
styleDispatch({ type: 'SET_SIDER', payload: true });
} else {
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
styleDispatch({ type: 'SET_SIDER', payload: false });
}
}}>
<Link
className="header-bar-text"
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
>
{itemElement}
</Link>
</div>
);
}}
selectedKeys={[]}
// items={headerButtons}
onSelect={(key) => {}}
header={isMobile()?{
header={styleState.isMobile?{
logo: (
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
<>
{
styleState.showSider ?
<Button icon={<IconMenu />} theme="light" aria-label="展开侧边栏" onClick={
() => styleDispatch({ type: 'SET_SIDER', payload: false })
} />:
<Button icon={<IconIndentLeft />} theme="light" aria-label="关闭侧边栏" onClick={
() => styleDispatch({ type: 'SET_SIDER', payload: true })
} />
}
</>
),
}:{
logo: (
<img src={logo} alt='logo' />
),
text: systemName,
}}
items={buttons}
footer={
@@ -159,17 +184,15 @@ const HeaderBar = () => {
)}
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
<>
{!isMobile() && (
<Switch
checkedText='🌞'
size={'large'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
)}
<Switch
checkedText='🌞'
size={styleState.isMobile?'default':'large'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
</>
{userState.user ? (
<>
@@ -188,7 +211,7 @@ const HeaderBar = () => {
>
{userState.user.username[0]}
</Avatar>
<span>{userState.user.username}</span>
{styleState.isMobile?null:<Text>{userState.user.username}</Text>}
</Dropdown>
</>
) : (

View File

@@ -25,7 +25,7 @@ import {
import { ITEMS_PER_PAGE } from '../constants';
import {
renderAudioModelPrice,
renderModelPrice,
renderModelPrice, renderModelPriceSimple,
renderNumber,
renderQuota,
stringToColor
@@ -386,14 +386,11 @@ const LogsTable = () => {
);
}
// let content = renderModelPrice(
// record.prompt_tokens,
// record.completion_tokens,
// other.model_ratio,
// other.model_price,
// other.completion_ratio,
// other.group_ratio,
// );
let content = renderModelPriceSimple(
other.model_ratio,
other.model_price,
other.group_ratio,
);
return (
<Paragraph
ellipsis={{
@@ -401,7 +398,7 @@ const LogsTable = () => {
}}
style={{ maxWidth: 240 }}
>
调用消费
{content}
</Paragraph>
);
},

View File

@@ -0,0 +1,40 @@
import HeaderBar from './HeaderBar.js';
import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './SiderBar.js';
import App from '../App.js';
import FooterBar from './Footer.js';
import { ToastContainer } from 'react-toastify';
import React, { useContext } from 'react';
import { StyleContext } from '../context/Style/index.js';
const { Sider, Content, Header, Footer } = Layout;
const PageLayout = () => {
const [styleState, styleDispatch] = useContext(StyleContext);
return (
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header>
<HeaderBar />
</Header>
<Layout style={{ flex: 1, overflow: 'hidden' }}>
<Sider>
{styleState.showSider ? null : <SiderBar />}
</Sider>
<Layout>
<Content
style={{ overflowY: styleState.shouldInnerPadding?'hidden':'auto', padding: styleState.shouldInnerPadding? '0': '24px' }}
>
<App />
</Content>
<Layout.Footer>
<FooterBar></FooterBar>
</Layout.Footer>
</Layout>
</Layout>
<ToastContainer />
</Layout>
)
}
export default PageLayout;

View File

@@ -363,36 +363,18 @@ const PersonalSetting = () => {
</Space>
</>
}
footer={
<Descriptions row>
<Descriptions.Item itemKey='当前余额'>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='历史消耗'>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='请求次数'>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
}
>
<Typography.Title heading={6}>可用模型</Typography.Title>
<div style={{marginTop: 10}}>
<Space wrap>
{models.map((model) => (
<Tag
key={model}
color='cyan'
onClick={() => {
copyText(model);
}}
>
{model}
</Tag>
))}
</Space>
</div>
<Descriptions row>
<Descriptions.Item itemKey='当前余额'>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='历史消耗'>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='请求次数'>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
</Card>
<Card
style={{marginTop: 10}}

View File

@@ -31,14 +31,15 @@ import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { setStatusData } from '../helpers/data.js';
import { stringToColor } from '../helpers/render.js';
import { useSetTheme, useTheme } from '../context/Theme/index.js';
import { StyleContext } from '../context/Style/index.js';
// HeaderBar Buttons
const SiderBar = () => {
const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
const [statusState, statusDispatch] = useContext(StatusContext);
const defaultIsCollapsed =
isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
localStorage.getItem('default_collapse_sidebar') === 'true';
const [selectedKeys, setSelectedKeys] = useState(['home']);
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
@@ -72,12 +73,6 @@ const SiderBar = () => {
to: '/playground',
icon: <IconCommentStroked />,
},
{
text: '模型价格',
itemKey: 'pricing',
to: '/pricing',
icon: <IconPriceTag />,
},
{
text: '渠道',
itemKey: 'channel',
@@ -101,6 +96,16 @@ const SiderBar = () => {
to: '/token',
icon: <IconKey />,
},
{
text: '数据看板',
itemKey: 'detail',
to: '/detail',
icon: <IconCalendarClock />,
className:
localStorage.getItem('enable_data_export') === 'true'
? 'semi-navigation-item-normal'
: 'tableHiddle',
},
{
text: '兑换码',
itemKey: 'redemption',
@@ -127,16 +132,6 @@ const SiderBar = () => {
to: '/log',
icon: <IconHistogram />,
},
{
text: '数据看板',
itemKey: 'detail',
to: '/detail',
icon: <IconCalendarClock />,
className:
localStorage.getItem('enable_data_export') === 'true'
? 'semi-navigation-item-normal'
: 'tableHiddle',
},
{
text: '绘图',
itemKey: 'midjourney',
@@ -196,7 +191,6 @@ const SiderBar = () => {
useEffect(() => {
loadStatus().then(() => {
setIsCollapsed(
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true',
);
});
@@ -239,7 +233,6 @@ const SiderBar = () => {
<Nav
style={{ maxWidth: 220, height: '100%' }}
defaultIsCollapsed={
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true'
}
isCollapsed={isCollapsed}
@@ -280,21 +273,15 @@ const SiderBar = () => {
}}
items={headerButtons}
onSelect={(key) => {
if (key.itemKey.toString().startsWith('chat')) {
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
} else {
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
}
setSelectedKeys([key.itemKey]);
}}
footer={
<>
{isMobile() && (
<Switch
checkedText='🌞'
size={'small'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
)}
</>
}
>

View File

@@ -0,0 +1,61 @@
// contexts/User/index.jsx
import React, { useState, useEffect } from 'react';
import { isMobile } from '../../helpers/index.js';
export const StyleContext = React.createContext({
dispatch: () => null,
});
export const StyleProvider = ({ children }) => {
const [state, setState] = useState({
isMobile: false,
showSider: false,
shouldInnerPadding: false,
});
const dispatch = (action) => {
if ('type' in action) {
switch (action.type) {
case 'TOGGLE_SIDER':
setState(prev => ({ ...prev, showSider: !prev.showSider }));
break;
case 'SET_SIDER':
setState(prev => ({ ...prev, showSider: action.payload }));
break;
case 'SET_MOBILE':
setState(prev => ({ ...prev, isMobile: action.payload }));
break;
case 'SET_INNER_PADDING':
setState(prev => ({ ...prev, shouldInnerPadding: action.payload }));
break;
default:
setState(prev => ({ ...prev, ...action }));
}
} else {
setState(prev => ({ ...prev, ...action }));
}
};
useEffect(() => {
const updateIsMobile = () => {
dispatch({ type: 'SET_MOBILE', payload: isMobile() });
};
updateIsMobile();
// Optionally, add event listeners to handle window resize
window.addEventListener('resize', updateIsMobile);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', updateIsMobile);
};
}, []);
return (
<StyleContext.Provider value={[state, dispatch]}>
{children}
</StyleContext.Provider>
);
};

View File

@@ -175,6 +175,19 @@ export function renderModelPrice(
}
}
export function renderModelPriceSimple(
modelRatio,
modelPrice = -1,
groupRatio,
) {
// 1 ratio = $0.002 / 1K tokens
if (modelPrice !== -1) {
return '价格:$' + modelPrice + ' * 分组:' + groupRatio;
} else {
return '模型: ' + modelRatio + ' * 分组: ' + groupRatio;
}
}
export function renderAudioModelPrice(
inputTokens,
completionTokens,
@@ -255,6 +268,44 @@ const colors = [
'yellow',
];
// 基础10色色板 (N ≤ 10)
const baseColors = [
'#1664FF', // 主色
'#1AC6FF',
'#FF8A00',
'#3CC780',
'#7442D4',
'#FFC400',
'#304D77',
'#B48DEB',
'#009488',
'#FF7DDA'
];
// 扩展20色色板 (10 < N ≤ 20)
const extendedColors = [
'#1664FF',
'#B2CFFF',
'#1AC6FF',
'#94EFFF',
'#FF8A00',
'#FFCE7A',
'#3CC780',
'#B9EDCD',
'#7442D4',
'#DDC5FA',
'#FFC400',
'#FAE878',
'#304D77',
'#8B959E',
'#B48DEB',
'#EFE3FF',
'#009488',
'#59BAA8',
'#FF7DDA',
'#FFCFEE'
];
export const modelColorMap = {
'dall-e': 'rgb(147,112,219)', // 深紫色
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
@@ -299,14 +350,33 @@ export const modelColorMap = {
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
};
export function modelToColor(modelName) {
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
if (modelColorMap[modelName]) {
return modelColorMap[modelName];
}
// 2. 生成一个稳定的数字作为索引
let hash = 0;
for (let i = 0; i < modelName.length; 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];
}
export function stringToColor(str) {
let sum = 0;
// 对字符串中的每个字符进行操作
for (let i = 0; i < str.length; i++) {
// 将字符的ASCII值加到sum中
sum += str.charCodeAt(i);
}
// 使用模运算得到个位数
let i = sum % colors.length;
return colors[i];
}

View File

@@ -17,7 +17,25 @@ body {
flex-direction: column;
}
#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;
}
.semi-descriptions-double-small .semi-descriptions-item {
padding-right: 30px;
}
.panel-desc-card {
/*min-width: 320px;*/
}
@media only screen and (max-width: 767px) {
#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 {
padding: 0 5px;
}
.semi-table-tbody,
.semi-table-row,
.semi-table-row-cell {
@@ -39,6 +57,10 @@ body {
row-gap: 3px;
column-gap: 10px;
}
.semi-navigation-horizontal .semi-navigation-header {
margin-right: 0;
}
}
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {

View File

@@ -13,6 +13,8 @@ import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './components/SiderBar';
import { ThemeProvider } from './context/Theme';
import FooterBar from './components/Footer';
import { StyleProvider } from './context/Style/index.js';
import PageLayout from './components/PageLayout.js';
// initialization
@@ -24,27 +26,9 @@ root.render(
<UserProvider>
<BrowserRouter>
<ThemeProvider>
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header>
<HeaderBar />
</Header>
<Layout style={{ flex: 1, overflow: 'hidden' }}>
<Sider>
<SiderBar />
</Sider>
<Layout>
<Content
style={{ overflowY: 'auto', padding: '24px' }}
>
<App />
</Content>
<Layout.Footer>
<FooterBar></FooterBar>
</Layout.Footer>
</Layout>
</Layout>
<ToastContainer />
</Layout>
<StyleProvider>
<PageLayout/>
</StyleProvider>
</ThemeProvider>
</BrowserRouter>
</UserProvider>

View File

@@ -710,6 +710,8 @@ const EditChannel = (props) => {
required
multiple
selection
filter
searchPosition='dropdown'
onChange={(value) => {
handleInputChange('models', value);
}}

View File

@@ -16,6 +16,7 @@ const EditTagModal = (props) => {
const [groupOptions, setGroupOptions] = useState([]);
const [basicModels, setBasicModels] = useState([]);
const [fullModels, setFullModels] = useState([]);
const [customModel, setCustomModel] = useState('');
const originInputs = {
tag: '',
new_tag: null,
@@ -183,6 +184,40 @@ const EditTagModal = (props) => {
fetchGroups().then();
}, [visible]);
const addCustomModels = () => {
if (customModel.trim() === '') return;
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
const modelArray = customModel.split(',').map((model) => model.trim());
let localModels = [...inputs.models];
let localModelOptions = [...modelOptions];
let hasError = false;
modelArray.forEach((model) => {
// 检查模型是否已存在,且模型名称非空
if (model && !localModels.includes(model)) {
localModels.push(model); // 添加到模型列表
localModelOptions.push({
// 添加到下拉选项
key: model,
text: model,
value: model
});
} else if (model) {
showError('某些模型已存在!');
hasError = true;
}
});
if (hasError) return; // 如果有错误则终止操作
// 更新状态值
setModelOptions(localModelOptions);
setCustomModel('');
handleInputChange('models', localModels);
};
return (
<SideSheet
title="编辑标签"
@@ -224,6 +259,8 @@ const EditTagModal = (props) => {
required
multiple
selection
filter
searchPosition='dropdown'
onChange={(value) => {
handleInputChange('models', value);
}}
@@ -231,6 +268,18 @@ const EditTagModal = (props) => {
autoComplete="new-password"
optionList={modelOptions}
/>
<Input
addonAfter={
<Button type="primary" onClick={addCustomModels}>
填入
</Button>
}
placeholder="输入自定义模型名称"
value={customModel}
onChange={(value) => {
setCustomModel(value.trim());
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组留空则不更改</Typography.Text>
</div>

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
import VChart from '@visactor/vchart';
import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
import { VChart } from "@visactor/react-vchart";
import {
API,
isAdmin,
@@ -17,11 +17,16 @@ import {
renderQuota,
renderQuotaNumberWithDigit,
stringToColor,
modelToColor,
} from '../../helpers/render';
import { UserContext } from '../../context/User/index.js';
import { StyleContext } from '../../context/Style/index.js';
const Detail = (props) => {
const formRef = useRef();
let now = new Date();
const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
const [inputs, setInputs] = useState({
username: '',
token_name: '',
@@ -40,32 +45,76 @@ const Detail = (props) => {
inputs;
const isAdminUser = isAdmin();
const initialized = useRef(false);
const [modelDataChart, setModelDataChart] = useState(null);
const [modelDataPieChart, setModelDataPieChart] = useState(null);
const [loading, setLoading] = useState(false);
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
const [consumeTokens, setConsumeTokens] = useState(0);
const [times, setTimes] = useState(0);
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
localStorage.getItem('data_export_default_time') || 'hour',
);
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return;
}
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const spec_line = {
type: 'bar',
data: [
{
id: 'barData',
values: [],
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
const [lineData, setLineData] = useState([]);
const [spec_pie, setSpecPie] = useState({
type: 'pie',
data: [{
id: 'id0',
values: pieData
}],
outerRadius: 0.8,
innerRadius: 0.5,
padAngle: 0.6,
valueField: 'value',
categoryField: 'type',
pie: {
style: {
cornerRadius: 10,
},
],
state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
},
},
title: {
visible: true,
text: '模型调用次数占比',
subtext: `总计:${renderNumber(times)}`,
},
legends: {
visible: true,
orient: 'left',
},
label: {
visible: true,
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['type'],
value: (datum) => renderNumber(datum['value']),
},
],
},
},
color: {
specified: modelColorMap,
},
});
const [spec_line, setSpecLine] = useState({
type: 'bar',
data: [{
id: 'barData',
values: lineData
}],
xField: 'Time',
yField: 'Usage',
seriesField: 'Model',
@@ -77,7 +126,7 @@ const Detail = (props) => {
title: {
visible: true,
text: '模型消耗分布',
subtext: '0',
subtext: `总计:${renderQuota(consumeQuota, 2)}`,
},
bar: {
// The state style of bar
@@ -129,196 +178,197 @@ const Detail = (props) => {
color: {
specified: modelColorMap,
},
});
// 添加一个新的状态来存储模型-颜色映射
const [modelColors, setModelColors] = useState({});
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return;
}
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const spec_pie = {
type: 'pie',
data: [
{
id: 'id0',
values: [{ type: 'null', value: '0' }],
},
],
outerRadius: 0.8,
innerRadius: 0.5,
padAngle: 0.6,
valueField: 'value',
categoryField: 'type',
pie: {
style: {
cornerRadius: 10,
},
state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
},
},
title: {
visible: true,
text: '模型调用次数占比',
},
legends: {
visible: true,
orient: 'left',
},
label: {
visible: true,
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['type'],
value: (datum) => renderNumber(datum['value']),
},
],
},
},
color: {
specified: modelColorMap,
},
};
const loadQuotaData = async (lineChart, pieChart) => {
const loadQuotaData = async () => {
setLoading(true);
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setQuotaData(data);
if (data.length === 0) {
data.push({
count: 0,
model_name: '无数据',
quota: 0,
created_at: now.getTime() / 1000,
try {
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setQuotaData(data);
if (data.length === 0) {
data.push({
count: 0,
model_name: '无数据',
quota: 0,
created_at: now.getTime() / 1000,
});
}
// 根据dataExportDefaultTime重制时间粒度
let timeGranularity = 3600;
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
// sort created_at
data.sort((a, b) => a.created_at - b.created_at);
data.forEach((item) => {
item['created_at'] =
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
});
updateChartData(data);
} else {
showError(message);
}
// 根据dataExportDefaultTime重制时间粒度
let timeGranularity = 3600;
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
// sort created_at
data.sort((a, b) => a.created_at - b.created_at);
data.forEach((item) => {
item['created_at'] =
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
});
updateChart(lineChart, pieChart, data);
} else {
showError(message);
} finally {
setLoading(false);
}
setLoading(false);
};
const refresh = async () => {
await loadQuotaData(modelDataChart, modelDataPieChart);
await loadQuotaData();
};
const initChart = async () => {
let lineChart = modelDataChart;
if (!modelDataChart) {
lineChart = new VChart(spec_line, { dom: 'model_data' });
setModelDataChart(lineChart);
lineChart.renderAsync();
}
let pieChart = modelDataPieChart;
if (!modelDataPieChart) {
pieChart = new VChart(spec_pie, { dom: 'model_pie' });
setModelDataPieChart(pieChart);
pieChart.renderAsync();
}
console.log('init vchart');
await loadQuotaData(lineChart, pieChart);
await loadQuotaData();
};
const updateChart = (lineChart, pieChart, data) => {
if (isAdminUser) {
// 将所有用户合并
}
let pieData = [];
let lineData = [];
let consumeQuota = 0;
let times = 0;
for (let i = 0; i < data.length; i++) {
const item = data[i];
consumeQuota += item.quota;
times += item.count;
// 合并model_name
let pieItem = pieData.find((it) => it.type === item.model_name);
const updateChartData = (data) => {
let newPieData = [];
let newLineData = [];
let totalQuota = 0;
let totalTimes = 0;
let uniqueModels = new Set();
let totalTokens = 0;
// 收集所有唯一的模型名称和时间点
let uniqueTimes = new Set();
data.forEach(item => {
uniqueModels.add(item.model_name);
uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
totalTokens += item.token_used;
});
// 处理颜色映射
const newModelColors = {};
Array.from(uniqueModels).forEach((modelName) => {
newModelColors[modelName] = modelColorMap[modelName] ||
modelColors[modelName] ||
modelToColor(modelName);
});
setModelColors(newModelColors);
// 处理饼图数据
for (let item of data) {
totalQuota += item.quota;
totalTimes += item.count;
let pieItem = newPieData.find((it) => it.type === item.model_name);
if (pieItem) {
pieItem.value += item.count;
} else {
pieData.push({
newPieData.push({
type: item.model_name,
value: item.count,
});
}
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
// 转换日期格式
let createTime = timestamp2string1(
item.created_at,
dataExportDefaultTime,
);
let lineItem = lineData.find(
(it) => it.Time === createTime && it.Model === item.model_name,
);
if (lineItem) {
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
} else {
lineData.push({
Time: createTime,
Model: item.model_name,
Usage: parseFloat(getQuotaWithUnit(item.quota)),
});
}
}
setConsumeQuota(consumeQuota);
setTimes(times);
// sort by count
pieData.sort((a, b) => b.value - a.value);
spec_pie.title.subtext = `总计:${renderNumber(times)}`;
spec_pie.data[0].values = pieData;
// 处理柱状图数据
let timePoints = Array.from(uniqueTimes);
if (timePoints.length < 7) {
// 根据时间粒度生成合适的时间点
const generateTimePoints = () => {
let lastTime = Math.max(...data.map(item => item.created_at));
let points = [];
let interval = dataExportDefaultTime === 'hour' ? 3600
: dataExportDefaultTime === 'day' ? 86400
: 604800;
spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
spec_line.data[0].values = lineData;
pieChart.updateSpec(spec_pie);
lineChart.updateSpec(spec_line);
for (let i = 0; i < 7; i++) {
points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
}
return points.reverse();
};
// pieChart.updateData('id0', pieData);
// lineChart.updateData('barData', lineData);
pieChart.reLayout();
lineChart.reLayout();
timePoints = generateTimePoints();
}
// 为每个时间点和模型生成数据
timePoints.forEach(time => {
Array.from(uniqueModels).forEach(model => {
let existingData = data.find(item =>
timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
item.model_name === model
);
newLineData.push({
Time: time,
Model: model,
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
});
});
});
// 排序
newPieData.sort((a, b) => b.value - a.value);
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
// 更新图表配置和数据
setSpecPie(prev => ({
...prev,
data: [{ id: 'id0', values: newPieData }],
title: {
...prev.title,
subtext: `总计:${renderNumber(totalTimes)}`
},
color: {
specified: newModelColors
}
}));
setSpecLine(prev => ({
...prev,
data: [{ id: 'barData', values: newLineData }],
title: {
...prev.title,
subtext: `总计:${renderQuota(totalQuota, 2)}`
},
color: {
specified: newModelColors
}
}));
setPieData(newPieData);
setLineData(newLineData);
setConsumeQuota(totalQuota);
setTimes(totalTimes);
setConsumeTokens(totalTokens);
};
const getUserData = async () => {
let res = await API.get(`/api/user/self`);
const {success, message, data} = res.data;
if (success) {
userDispatch({type: 'login', payload: data});
} else {
showError(message);
}
};
useEffect(() => {
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
// if (dataExportDefaultTime === 'day') {
// // 设置开始时间为7天前
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
// inputs.start_timestamp = st;
// formRef.current.formApi.setValue('start_timestamp', st);
// }
getUserData()
if (!initialized.current) {
initVChartSemiTheme({
isWatchingThemeSwitch: true,
@@ -389,33 +439,93 @@ const Detail = (props) => {
/>
</>
)}
<Button
label='查询'
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
style={{ marginTop: 24 }}
>
查询
</Button>
<Form.Section>
<Button
label='查询'
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
>
查询
</Button>
</Form.Section>
</>
</Form>
<Spin spinning={loading}>
<div style={{ height: 500 }}>
<div
id='model_pie'
style={{ width: '100%', minWidth: 100 }}
></div>
</div>
<div style={{ height: 500 }}>
<div
id='model_data'
style={{ width: '100%', minWidth: 100 }}
></div>
</div>
<Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
<Col span={styleState.isMobile?24:8}>
<Card className='panel-desc-card'>
<Descriptions row size="small">
<Descriptions.Item itemKey='当前余额'>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='历史消耗'>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='请求次数'>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile?24:8}>
<Card>
<Descriptions row size="small">
<Descriptions.Item itemKey='统计额度'>
{renderQuota(consumeQuota)}
</Descriptions.Item>
<Descriptions.Item itemKey='统计Tokens'>
{consumeTokens}
</Descriptions.Item>
<Descriptions.Item itemKey='统计次数'>
{times}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile ? 24 : 8}>
<Card>
<Descriptions row size='small'>
<Descriptions.Item itemKey='平均RPM'>
{(times /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
<Descriptions.Item itemKey='平均TPM'>
{(consumeTokens /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
<Card style={{marginTop: 20}}>
<Tabs type="line" defaultActiveKey="1">
<Tabs.TabPane tab="消耗分布" itemKey="1">
<div style={{ height: 500 }}>
<VChart
spec={spec_line}
option={{ mode: "desktop-browser" }}
/>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab="调用次数分布" itemKey="2">
<div style={{ height: 500 }}>
<VChart
spec={spec_pie}
option={{ mode: "desktop-browser" }}
/>
</div>
</Tabs.TabPane>
</Tabs>
</Card>
</Spin>
</Layout.Content>
</Layout>

View File

@@ -3,11 +3,13 @@ import { Card, Col, Row } from '@douyinfe/semi-ui';
import { API, showError, showNotice, timestamp2string } from '../../helpers';
import { StatusContext } from '../../context/Status';
import { marked } from 'marked';
import { StyleContext } from '../../context/Style/index.js';
const Home = () => {
const [statusState] = useContext(StatusContext);
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState('');
const [styleState, styleDispatch] = useContext(StyleContext);
const displayNotice = async () => {
const res = await API.get('/api/notice');

View File

@@ -1,9 +1,11 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { UserContext } from '../context/User';
import { API, getUserIdFromLocalStorage, showError } from '../helpers';
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography } from '@douyinfe/semi-ui';
import { UserContext } from '../../context/User/index.js';
import { API, getUserIdFromLocalStorage, showError } from '../../helpers/index.js';
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button } from '@douyinfe/semi-ui';
import { SSE } from 'sse';
import { IconSetting } from '@douyinfe/semi-icons';
import { StyleContext } from '../../context/Style/index.js';
const defaultMessage = [
{
@@ -54,6 +56,8 @@ const Playground = () => {
const [message, setMessage] = useState(defaultMessage);
const [models, setModels] = useState([]);
const [groups, setGroups] = useState([]);
const [showSettings, setShowSettings] = useState(true);
const [styleState, styleDispatch] = useContext(StyleContext);
const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -99,11 +103,16 @@ const Playground = () => {
// handleInputChange('group', localGroupOptions[0].value);
if (localGroupOptions.length > 0) {
// set default group at first
localGroupOptions.unshift({
label: '用户分组',
value: '',
});
// set user group at first
if (userState.user && userState.user.group) {
let userGroup = userState.user.group;
// Find and move user's group to the front
const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup);
if (userGroupIndex > -1) {
const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0];
localGroupOptions.unshift(userGroupOption);
}
}
} else {
localGroupOptions = [{
label: '用户分组',
@@ -257,95 +266,146 @@ const Playground = () => {
})
}, []);
const SettingsToggle = () => {
if (!styleState.isMobile) return null;
return (
<Button
icon={<IconSetting />}
style={{
position: 'absolute',
left: showSettings ? -10 : -20,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1000,
width: 40,
height: 40,
borderRadius: '0 20px 20px 0',
padding: 0,
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
}}
onClick={() => setShowSettings(!showSettings)}
theme="solid"
type="primary"
/>
);
};
function CustomInputRender(props) {
const { detailProps } = props;
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
return <div style={{margin: '8px 16px', display: 'flex', flexDirection:'row',
alignItems: 'flex-end', borderRadius: 16,padding: 10, border: '1px solid var(--semi-color-border)'}}
onClick={onClick}
>
{/*{uploadNode}*/}
{inputNode}
{sendNode}
</div>
}
const renderInputArea = useCallback((props) => {
return (<CustomInputRender {...props} />)
}, []);
return (
<Layout style={{height: '100%'}}>
<Layout.Sider>
<Card style={commonOuterStyle}>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组</Typography.Text>
</div>
<Select
placeholder={'请选择分组'}
name='group'
required
selection
onChange={(value) => {
handleInputChange('group', value);
}}
value={inputs.group}
autoComplete='new-password'
optionList={groups}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型</Typography.Text>
</div>
<Select
placeholder={'请选择模型'}
name='model'
required
selection
searchPosition='dropdown'
filter
onChange={(value) => {
handleInputChange('model', value);
}}
value={inputs.model}
autoComplete='new-password'
optionList={models}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>Temperature</Typography.Text>
</div>
<Slider
step={0.1}
min={0.1}
max={1}
value={inputs.temperature}
onChange={(value) => {
handleInputChange('temperature', value);
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>MaxTokens</Typography.Text>
</div>
<Input
placeholder='MaxTokens'
name='max_tokens'
required
autoComplete='new-password'
defaultValue={0}
value={inputs.max_tokens}
onChange={(value) => {
handleInputChange('max_tokens', value);
}}
/>
{(showSettings || !styleState.isMobile) && (
<Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}>
<Card style={commonOuterStyle}>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组</Typography.Text>
</div>
<Select
placeholder={'请选择分组'}
name='group'
required
selection
onChange={(value) => {
handleInputChange('group', value);
}}
value={inputs.group}
autoComplete='new-password'
optionList={groups.map((group) => ({
...group,
label: styleState.isMobile && group.label.length > 18
? group.label.substring(0, 18) + '...'
: group.label,
}))}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型</Typography.Text>
</div>
<Select
placeholder={'请选择模型'}
name='model'
required
selection
searchPosition='dropdown'
filter
onChange={(value) => {
handleInputChange('model', value);
}}
value={inputs.model}
autoComplete='new-password'
optionList={models}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>Temperature</Typography.Text>
</div>
<Slider
step={0.1}
min={0.1}
max={1}
value={inputs.temperature}
onChange={(value) => {
handleInputChange('temperature', value);
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>MaxTokens</Typography.Text>
</div>
<Input
placeholder='MaxTokens'
name='max_tokens'
required
autoComplete='new-password'
defaultValue={0}
value={inputs.max_tokens}
onChange={(value) => {
handleInputChange('max_tokens', value);
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>System</Typography.Text>
</div>
<TextArea
placeholder='System Prompt'
name='system'
required
autoComplete='new-password'
autosize
defaultValue={systemPrompt}
// value={systemPrompt}
onChange={(value) => {
setSystemPrompt(value);
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>System</Typography.Text>
</div>
<TextArea
placeholder='System Prompt'
name='system'
required
autoComplete='new-password'
autosize
defaultValue={systemPrompt}
// value={systemPrompt}
onChange={(value) => {
setSystemPrompt(value);
}}
/>
</Card>
</Layout.Sider>
</Card>
</Layout.Sider>
)}
<Layout.Content>
<div style={{height: '100%'}}>
<div style={{height: '100%', position: 'relative'}}>
<SettingsToggle />
<Chat
chatBoxRenderConfig={{
renderChatBoxAction: () => {
return <div></div>
}
}}
renderInputArea={renderInputArea}
roleConfig={roleInfo}
style={commonOuterStyle}
chats={message}

View File

@@ -7,7 +7,7 @@ import {
showError,
showSuccess,
} from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render';
import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
import {
AutoComplete,
Button,
@@ -66,11 +66,16 @@ const EditRedemption = (props) => {
}, [props.editingRedemption.id]);
const submit = async () => {
if (!isEdit && inputs.name === '') return;
let name = inputs.name;
if (!isEdit && inputs.name === '') {
// set default name
name = '兑换码-' + renderQuota(quota);
}
setLoading(true);
let localInputs = inputs;
localInputs.count = parseInt(localInputs.count);
localInputs.quota = parseInt(localInputs.quota);
localInputs.name = name;
let res;
if (isEdit) {
res = await API.put(`/api/redemption/`, {