|
|
@@ -1,8 +1,9 @@
|
|
|
import React, { useEffect, useState } from 'react';
|
|
|
-import { Button, Label, Pagination, Select, Table } from 'semantic-ui-react';
|
|
|
+import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react';
|
|
|
import { API, isAdmin, showError, timestamp2string } from '../helpers';
|
|
|
|
|
|
import { ITEMS_PER_PAGE } from '../constants';
|
|
|
+import { renderQuota } from '../helpers/render';
|
|
|
|
|
|
function renderTimestamp(timestamp) {
|
|
|
return (
|
|
|
@@ -14,7 +15,7 @@ function renderTimestamp(timestamp) {
|
|
|
|
|
|
const MODE_OPTIONS = [
|
|
|
{ key: 'all', text: '全部用户', value: 'all' },
|
|
|
- { key: 'self', text: '当前用户', value: 'self' },
|
|
|
+ { key: 'self', text: '当前用户', value: 'self' }
|
|
|
];
|
|
|
|
|
|
const LOG_OPTIONS = [
|
|
|
@@ -47,13 +48,57 @@ const LogsTable = () => {
|
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
|
|
const [searching, setSearching] = useState(false);
|
|
|
const [logType, setLogType] = useState(0);
|
|
|
- const [mode, setMode] = useState('self'); // all, self
|
|
|
- const showModePanel = isAdmin();
|
|
|
+ const isAdminUser = isAdmin();
|
|
|
+ let now = new Date();
|
|
|
+ const [inputs, setInputs] = useState({
|
|
|
+ name: '',
|
|
|
+ model_name: '',
|
|
|
+ start_timestamp: timestamp2string(0),
|
|
|
+ end_timestamp: timestamp2string(now.getTime() / 1000 + 3600)
|
|
|
+ });
|
|
|
+ const { name, model_name, start_timestamp, end_timestamp } = inputs;
|
|
|
+
|
|
|
+ const [stat, setStat] = useState({
|
|
|
+ quota: 0,
|
|
|
+ token: 0
|
|
|
+ });
|
|
|
+
|
|
|
+ const handleInputChange = (e, { name, value }) => {
|
|
|
+ setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const getLogSelfStat = async () => {
|
|
|
+ let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
+ let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
+ let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
|
|
|
+ const { success, message, data } = res.data;
|
|
|
+ if (success) {
|
|
|
+ setStat(data);
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const getLogStat = async () => {
|
|
|
+ let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
+ let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
+ let res = await API.get(`/api/log/stat?type=${logType}&username=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
|
|
|
+ const { success, message, data } = res.data;
|
|
|
+ if (success) {
|
|
|
+ setStat(data);
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
const loadLogs = async (startIdx) => {
|
|
|
- let url = `/api/log/self/?p=${startIdx}&type=${logType}`;
|
|
|
- if (mode === 'all') {
|
|
|
- url = `/api/log/?p=${startIdx}&type=${logType}`;
|
|
|
+ let url = '';
|
|
|
+ let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
+ let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
+ if (isAdminUser) {
|
|
|
+ url = `/api/log/?p=${startIdx}&type=${logType}&username=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
|
|
+ } else {
|
|
|
+ url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
|
|
}
|
|
|
const res = await API.get(url);
|
|
|
const { success, message, data } = res.data;
|
|
|
@@ -84,19 +129,16 @@ const LogsTable = () => {
|
|
|
const refresh = async () => {
|
|
|
setLoading(true);
|
|
|
await loadLogs(0);
|
|
|
+ if (isAdminUser) {
|
|
|
+ getLogStat().then();
|
|
|
+ } else {
|
|
|
+ getLogSelfStat().then();
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- loadLogs(0)
|
|
|
- .then()
|
|
|
- .catch((reason) => {
|
|
|
- showError(reason);
|
|
|
- });
|
|
|
- }, []);
|
|
|
-
|
|
|
useEffect(() => {
|
|
|
refresh().then();
|
|
|
- }, [mode, logType]);
|
|
|
+ }, [logType]);
|
|
|
|
|
|
const searchLogs = async () => {
|
|
|
if (searchKeyword === '') {
|
|
|
@@ -137,118 +179,161 @@ const LogsTable = () => {
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
- <Table basic>
|
|
|
- <Table.Header>
|
|
|
- <Table.Row>
|
|
|
- <Table.HeaderCell
|
|
|
- style={{ cursor: 'pointer' }}
|
|
|
- onClick={() => {
|
|
|
- sortLog('created_time');
|
|
|
- }}
|
|
|
- width={3}
|
|
|
- >
|
|
|
- 时间
|
|
|
- </Table.HeaderCell>
|
|
|
- {
|
|
|
- showModePanel && (
|
|
|
- <Table.HeaderCell
|
|
|
- style={{ cursor: 'pointer' }}
|
|
|
- onClick={() => {
|
|
|
- sortLog('user_id');
|
|
|
- }}
|
|
|
- width={1}
|
|
|
- >
|
|
|
- 用户
|
|
|
- </Table.HeaderCell>
|
|
|
+ <Segment>
|
|
|
+ <Header as='h3'>使用明细(总消耗额度:{renderQuota(stat.quota)})</Header>
|
|
|
+ <Form>
|
|
|
+ <Form.Group>
|
|
|
+ <Form.Input fluid label={isAdminUser ? '用户名称' : '令牌名称'} width={3} value={name}
|
|
|
+ placeholder={isAdminUser ? '留空则查询全部用户' : '留空则查询全部令牌'} name='name'
|
|
|
+ onChange={handleInputChange} />
|
|
|
+ <Form.Input fluid label='模型名称' width={3} value={model_name} placeholder='留空则查询全部模型' name='model_name'
|
|
|
+ onChange={handleInputChange} />
|
|
|
+ <Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
|
|
|
+ name='start_timestamp'
|
|
|
+ onChange={handleInputChange} />
|
|
|
+ <Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
|
|
|
+ name='end_timestamp'
|
|
|
+ onChange={handleInputChange} />
|
|
|
+ <Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
|
|
|
+ </Form.Group>
|
|
|
+ </Form>
|
|
|
+ <Table basic compact size='small'>
|
|
|
+ <Table.Header>
|
|
|
+ <Table.Row>
|
|
|
+ <Table.HeaderCell
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
+ onClick={() => {
|
|
|
+ sortLog('created_time');
|
|
|
+ }}
|
|
|
+ width={3}
|
|
|
+ >
|
|
|
+ 时间
|
|
|
+ </Table.HeaderCell>
|
|
|
+ <Table.HeaderCell
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
+ width={1}
|
|
|
+ >
|
|
|
+ {isAdminUser ? '用户' : '令牌'}
|
|
|
+ </Table.HeaderCell>
|
|
|
+ <Table.HeaderCell
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
+ onClick={() => {
|
|
|
+ sortLog('type');
|
|
|
+ }}
|
|
|
+ width={2}
|
|
|
+ >
|
|
|
+ 类型
|
|
|
+ </Table.HeaderCell>
|
|
|
+ <Table.HeaderCell
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
+ onClick={() => {
|
|
|
+ sortLog('model_name');
|
|
|
+ }}
|
|
|
+ width={2}
|
|
|
+ >
|
|
|
+ 模型
|
|
|
+ </Table.HeaderCell>
|
|
|
+ <Table.HeaderCell
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
+ onClick={() => {
|
|
|
+ sortLog('prompt_tokens');
|
|
|
+ }}
|
|
|
+ width={1}
|
|
|
+ >
|
|
|
+ 提示令牌
|
|
|
+ </Table.HeaderCell>
|
|
|
+ <Table.HeaderCell
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
+ onClick={() => {
|
|
|
+ sortLog('completion_tokens');
|
|
|
+ }}
|
|
|
+ width={1}
|
|
|
+ >
|
|
|
+ 补全令牌
|
|
|
+ </Table.HeaderCell>
|
|
|
+ <Table.HeaderCell
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
+ onClick={() => {
|
|
|
+ sortLog('quota');
|
|
|
+ }}
|
|
|
+ width={2}
|
|
|
+ >
|
|
|
+ 消耗额度
|
|
|
+ </Table.HeaderCell>
|
|
|
+ <Table.HeaderCell
|
|
|
+ style={{ cursor: 'pointer' }}
|
|
|
+ onClick={() => {
|
|
|
+ sortLog('content');
|
|
|
+ }}
|
|
|
+ width={4}
|
|
|
+ >
|
|
|
+ 详情
|
|
|
+ </Table.HeaderCell>
|
|
|
+ </Table.Row>
|
|
|
+ </Table.Header>
|
|
|
+
|
|
|
+ <Table.Body>
|
|
|
+ {logs
|
|
|
+ .slice(
|
|
|
+ (activePage - 1) * ITEMS_PER_PAGE,
|
|
|
+ activePage * ITEMS_PER_PAGE
|
|
|
)
|
|
|
- }
|
|
|
- <Table.HeaderCell
|
|
|
- style={{ cursor: 'pointer' }}
|
|
|
- onClick={() => {
|
|
|
- sortLog('type');
|
|
|
- }}
|
|
|
- width={2}
|
|
|
- >
|
|
|
- 类型
|
|
|
- </Table.HeaderCell>
|
|
|
- <Table.HeaderCell
|
|
|
- style={{ cursor: 'pointer' }}
|
|
|
- onClick={() => {
|
|
|
- sortLog('content');
|
|
|
- }}
|
|
|
- width={showModePanel ? 10 : 11}
|
|
|
- >
|
|
|
- 详情
|
|
|
- </Table.HeaderCell>
|
|
|
- </Table.Row>
|
|
|
- </Table.Header>
|
|
|
+ .map((log, idx) => {
|
|
|
+ if (log.deleted) return <></>;
|
|
|
+ return (
|
|
|
+ <Table.Row key={log.created_at}>
|
|
|
+ <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
|
|
|
+ {
|
|
|
+ isAdminUser && (
|
|
|
+ <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ {
|
|
|
+ !isAdminUser && (
|
|
|
+ <Table.Cell>{log.token_name ? <Label>{log.token_name}</Label> : ''}</Table.Cell>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ <Table.Cell>{renderType(log.type)}</Table.Cell>
|
|
|
+ <Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell>
|
|
|
+ <Table.Cell>{log.prompt_tokens ? log.prompt_tokens: ''}</Table.Cell>
|
|
|
+ <Table.Cell>{log.completion_tokens ? log.completion_tokens: ''}</Table.Cell>
|
|
|
+ <Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell>
|
|
|
+ <Table.Cell>{log.content}</Table.Cell>
|
|
|
+ </Table.Row>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </Table.Body>
|
|
|
|
|
|
- <Table.Body>
|
|
|
- {logs
|
|
|
- .slice(
|
|
|
- (activePage - 1) * ITEMS_PER_PAGE,
|
|
|
- activePage * ITEMS_PER_PAGE
|
|
|
- )
|
|
|
- .map((log, idx) => {
|
|
|
- if (log.deleted) return <></>;
|
|
|
- return (
|
|
|
- <Table.Row key={log.created_at}>
|
|
|
- <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
|
|
|
- {
|
|
|
- showModePanel && (
|
|
|
- <Table.Cell><Label>{log.user_id}</Label></Table.Cell>
|
|
|
- )
|
|
|
+ <Table.Footer>
|
|
|
+ <Table.Row>
|
|
|
+ <Table.HeaderCell colSpan={'8'}>
|
|
|
+ <Select
|
|
|
+ placeholder='选择明细分类'
|
|
|
+ options={LOG_OPTIONS}
|
|
|
+ style={{ marginRight: '8px' }}
|
|
|
+ name='logType'
|
|
|
+ value={logType}
|
|
|
+ onChange={(e, { name, value }) => {
|
|
|
+ setLogType(value);
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
|
|
|
+ <Pagination
|
|
|
+ floated='right'
|
|
|
+ activePage={activePage}
|
|
|
+ onPageChange={onPaginationChange}
|
|
|
+ size='small'
|
|
|
+ siblingRange={1}
|
|
|
+ totalPages={
|
|
|
+ Math.ceil(logs.length / ITEMS_PER_PAGE) +
|
|
|
+ (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
|
|
}
|
|
|
- <Table.Cell>{renderType(log.type)}</Table.Cell>
|
|
|
- <Table.Cell>{log.content}</Table.Cell>
|
|
|
- </Table.Row>
|
|
|
- );
|
|
|
- })}
|
|
|
- </Table.Body>
|
|
|
-
|
|
|
- <Table.Footer>
|
|
|
- <Table.Row>
|
|
|
- <Table.HeaderCell colSpan={showModePanel ? '5' : '4'}>
|
|
|
- {
|
|
|
- showModePanel && (
|
|
|
- <Select
|
|
|
- placeholder='选择模式'
|
|
|
- options={MODE_OPTIONS}
|
|
|
- style={{ marginRight: '8px' }}
|
|
|
- name='mode'
|
|
|
- value={mode}
|
|
|
- onChange={(e, { name, value }) => {
|
|
|
- setMode(value);
|
|
|
- }}
|
|
|
- />
|
|
|
- )
|
|
|
- }
|
|
|
- <Select
|
|
|
- placeholder='选择明细分类'
|
|
|
- options={LOG_OPTIONS}
|
|
|
- style={{ marginRight: '8px' }}
|
|
|
- name='logType'
|
|
|
- value={logType}
|
|
|
- onChange={(e, { name, value }) => {
|
|
|
- setLogType(value);
|
|
|
- }}
|
|
|
- />
|
|
|
- <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
|
|
|
- <Pagination
|
|
|
- floated='right'
|
|
|
- activePage={activePage}
|
|
|
- onPageChange={onPaginationChange}
|
|
|
- size='small'
|
|
|
- siblingRange={1}
|
|
|
- totalPages={
|
|
|
- Math.ceil(logs.length / ITEMS_PER_PAGE) +
|
|
|
- (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
|
|
- }
|
|
|
- />
|
|
|
- </Table.HeaderCell>
|
|
|
- </Table.Row>
|
|
|
- </Table.Footer>
|
|
|
- </Table>
|
|
|
+ />
|
|
|
+ </Table.HeaderCell>
|
|
|
+ </Table.Row>
|
|
|
+ </Table.Footer>
|
|
|
+ </Table>
|
|
|
+ </Segment>
|
|
|
</>
|
|
|
);
|
|
|
};
|