Commit 51695bef by t0ng7u

โ™ป๏ธ refactor: restructure MjLogsTable into modular component architecture

Refactor the monolithic MjLogsTable component (971 lines) into a modular,
maintainable architecture following the same pattern as LogsTable refactor.

## What Changed

### ๐Ÿ—๏ธ Architecture
- Split large single file into focused, single-responsibility components
- Introduced custom hook `useMjLogsData` for centralized state management
- Created dedicated column definitions file for better organization
- Implemented specialized modal components for Midjourney-specific features

### ๐Ÿ“ New Structure
```
web/src/components/table/mj-logs/
โ”œโ”€โ”€ index.jsx                    # Main page component orchestrator
โ”œโ”€โ”€ MjLogsTable.jsx             # Pure table rendering component
โ”œโ”€โ”€ MjLogsActions.jsx           # Actions area (banner + compact mode)
โ”œโ”€โ”€ MjLogsFilters.jsx           # Search form component
โ”œโ”€โ”€ MjLogsColumnDefs.js         # Column definitions and renderers
โ””โ”€โ”€ modals/
    โ”œโ”€โ”€ ColumnSelectorModal.jsx # Column visibility settings
    โ””โ”€โ”€ ContentModal.jsx        # Content viewer (text + image preview)

web/src/hooks/mj-logs/
โ””โ”€โ”€ useMjLogsData.js            # Custom hook for state & logic
```

### ๐ŸŽฏ Key Improvements
- **Maintainability**: Clear separation of concerns, easier to understand
- **Reusability**: Modular components can be reused independently
- **Performance**: Optimized with `useMemo` for column rendering
- **Testing**: Single-responsibility components easier to test
- **Developer Experience**: Better code organization and readability

### ๐ŸŽจ Midjourney-Specific Features Preserved
- All task type rendering with icons (IMAGINE, UPSCALE, VARIATION, etc.)
- Status rendering with appropriate colors and icons
- Image preview functionality for generated artwork
- Progress indicators for task completion
- Admin-only columns for channel and submission results
- Banner notification system for callback settings

### ๐Ÿ”ง Technical Details
- Centralized all business logic in `useMjLogsData` custom hook
- Extracted comprehensive column definitions with Lucide React icons
- Split complex UI into focused components (table, actions, filters, modals)
- Maintained responsive design patterns for mobile compatibility
- Preserved admin permission handling for restricted features

### ๐Ÿ› Fixes
- Improved component prop passing patterns
- Enhanced type safety through better state management
- Optimized rendering performance with proper memoization

## Breaking Changes
None - all existing imports and functionality preserved.
parent acf6ec93
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Palette,
ZoomIn,
Shuffle,
Move,
FileText,
Blend,
Upload,
Minimize2,
RotateCcw,
PaintBucket,
Focus,
Move3D,
Monitor,
UserCheck,
HelpCircle,
CheckCircle,
Clock,
Copy,
FileX,
Pause,
XCircle,
Loader,
AlertCircle,
Hash,
} from 'lucide-react';
import {
API,
copy,
isAdmin,
showError,
showSuccess,
timestamp2string
} from '../../helpers';
import {
Button,
Checkbox,
Empty,
Form,
ImagePreview,
Layout,
Modal,
Progress,
Skeleton,
Table,
Tag,
Typography
} from '@douyinfe/semi-ui';
import CardPro from '../common/ui/CardPro';
import {
IllustrationNoResult,
IllustrationNoResultDark
} from '@douyinfe/semi-illustrations';
import { ITEMS_PER_PAGE } from '../../constants';
import {
IconEyeOpened,
IconSearch,
} from '@douyinfe/semi-icons';
import { useTableCompactMode } from '../../hooks/common/useTableCompactMode';
const { Text } = Typography;
const colors = [
'amber',
'blue',
'cyan',
'green',
'grey',
'indigo',
'light-blue',
'lime',
'orange',
'pink',
'purple',
'red',
'teal',
'violet',
'yellow',
];
// ๅฎšไน‰ๅˆ—้”ฎๅ€ผๅธธ้‡
const COLUMN_KEYS = {
SUBMIT_TIME: 'submit_time',
DURATION: 'duration',
CHANNEL: 'channel',
TYPE: 'type',
TASK_ID: 'task_id',
SUBMIT_RESULT: 'submit_result',
TASK_STATUS: 'task_status',
PROGRESS: 'progress',
IMAGE: 'image',
PROMPT: 'prompt',
PROMPT_EN: 'prompt_en',
FAIL_REASON: 'fail_reason',
};
const LogsTable = () => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
// ๅˆ—ๅฏ่งๆ€ง็Šถๆ€
const [visibleColumns, setVisibleColumns] = useState({});
const [showColumnSelector, setShowColumnSelector] = useState(false);
const isAdminUser = isAdmin();
const [compactMode, setCompactMode] = useTableCompactMode('mjLogs');
// ๅŠ ่ฝฝไฟๅญ˜็š„ๅˆ—ๅๅฅฝ่ฎพ็ฝฎ
useEffect(() => {
const savedColumns = localStorage.getItem('mj-logs-table-columns');
if (savedColumns) {
try {
const parsed = JSON.parse(savedColumns);
const defaults = getDefaultColumnVisibility();
const merged = { ...defaults, ...parsed };
setVisibleColumns(merged);
} catch (e) {
console.error('Failed to parse saved column preferences', e);
initDefaultColumns();
}
} else {
initDefaultColumns();
}
}, []);
// ่Žทๅ–้ป˜่ฎคๅˆ—ๅฏ่งๆ€ง
const getDefaultColumnVisibility = () => {
return {
[COLUMN_KEYS.SUBMIT_TIME]: true,
[COLUMN_KEYS.DURATION]: true,
[COLUMN_KEYS.CHANNEL]: isAdminUser,
[COLUMN_KEYS.TYPE]: true,
[COLUMN_KEYS.TASK_ID]: true,
[COLUMN_KEYS.SUBMIT_RESULT]: isAdminUser,
[COLUMN_KEYS.TASK_STATUS]: true,
[COLUMN_KEYS.PROGRESS]: true,
[COLUMN_KEYS.IMAGE]: true,
[COLUMN_KEYS.PROMPT]: true,
[COLUMN_KEYS.PROMPT_EN]: true,
[COLUMN_KEYS.FAIL_REASON]: true,
};
};
// ๅˆๅง‹ๅŒ–้ป˜่ฎคๅˆ—ๅฏ่งๆ€ง
const initDefaultColumns = () => {
const defaults = getDefaultColumnVisibility();
setVisibleColumns(defaults);
localStorage.setItem('mj-logs-table-columns', JSON.stringify(defaults));
};
// ๅค„็†ๅˆ—ๅฏ่งๆ€งๅ˜ๅŒ–
const handleColumnVisibilityChange = (columnKey, checked) => {
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
setVisibleColumns(updatedColumns);
};
// ๅค„็†ๅ…จ้€‰
const handleSelectAll = (checked) => {
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
const updatedColumns = {};
allKeys.forEach((key) => {
if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.SUBMIT_RESULT) && !isAdminUser) {
updatedColumns[key] = false;
} else {
updatedColumns[key] = checked;
}
});
setVisibleColumns(updatedColumns);
};
// ๆ›ดๆ–ฐ่กจๆ ผๆ—ถไฟๅญ˜ๅˆ—ๅฏ่งๆ€ง
useEffect(() => {
if (Object.keys(visibleColumns).length > 0) {
localStorage.setItem('mj-logs-table-columns', JSON.stringify(visibleColumns));
}
}, [visibleColumns]);
function renderType(type) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' shape='circle' prefixIcon={<Palette size={14} />}>
{t('็ป˜ๅ›พ')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' shape='circle' prefixIcon={<ZoomIn size={14} />}>
{t('ๆ”พๅคง')}
</Tag>
);
case 'VIDEO':
return (
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('่ง†้ข‘')}
</Tag>
);
case 'EDITS':
return (
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('็ผ–่พ‘')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('ๅ˜ๆข')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('ๅผบๅ˜ๆข')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('ๅผฑๅ˜ๆข')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' shape='circle' prefixIcon={<Move size={14} />}>
{t('ๅนณ็งป')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' shape='circle' prefixIcon={<FileText size={14} />}>
{t('ๅ›พ็”Ÿๆ–‡')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' shape='circle' prefixIcon={<Blend size={14} />}>
{t('ๅ›พๆททๅˆ')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' shape='circle' prefixIcon={<Upload size={14} />}>
ไธŠไผ ๆ–‡ไปถ
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' shape='circle' prefixIcon={<Minimize2 size={14} />}>
{t('็ผฉ่ฏ')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' shape='circle' prefixIcon={<RotateCcw size={14} />}>
{t('้‡็ป˜')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
{t('ๅฑ€้ƒจ้‡็ป˜-ๆไบค')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' shape='circle' prefixIcon={<Focus size={14} />}>
{t('ๅ˜็„ฆ')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' shape='circle' prefixIcon={<Move3D size={14} />}>
{t('่‡ชๅฎšไน‰ๅ˜็„ฆ-ๆไบค')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' shape='circle' prefixIcon={<Monitor size={14} />}>
{t('็ช—ๅฃๅค„็†')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
{t('ๆข่„ธ')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('ๆœช็Ÿฅ')}
</Tag>
);
}
}
function renderCode(code) {
switch (code) {
case 1:
return (
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('ๅทฒๆไบค')}
</Tag>
);
case 21:
return (
<Tag color='lime' shape='circle' prefixIcon={<Clock size={14} />}>
{t('็ญ‰ๅพ…ไธญ')}
</Tag>
);
case 22:
return (
<Tag color='orange' shape='circle' prefixIcon={<Copy size={14} />}>
{t('้‡ๅคๆไบค')}
</Tag>
);
case 0:
return (
<Tag color='yellow' shape='circle' prefixIcon={<FileX size={14} />}>
{t('ๆœชๆไบค')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('ๆœช็Ÿฅ')}
</Tag>
);
}
}
function renderStatus(type) {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('ๆˆๅŠŸ')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
{t('ๆœชๅฏๅŠจ')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
{t('้˜Ÿๅˆ—ไธญ')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' shape='circle' prefixIcon={<Loader size={14} />}>
{t('ๆ‰ง่กŒไธญ')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('ๅคฑ่ดฅ')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
{t('็ช—ๅฃ็ญ‰ๅพ…')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('ๆœช็Ÿฅ')}
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000); // ไปŽ็ง’่ฝฌๆขไธบๆฏซ็ง’
const year = date.getFullYear(); // ่Žทๅ–ๅนดไปฝ
const month = ('0' + (date.getMonth() + 1)).slice(-2); // ่Žทๅ–ๆœˆไปฝ๏ผŒไปŽ0ๅผ€ๅง‹้œ€่ฆ+1๏ผŒๅนถไฟ่ฏไธคไฝๆ•ฐ
const day = ('0' + date.getDate()).slice(-2); // ่Žทๅ–ๆ—ฅๆœŸ๏ผŒๅนถไฟ่ฏไธคไฝๆ•ฐ
const hours = ('0' + date.getHours()).slice(-2); // ่Žทๅ–ๅฐๆ—ถ๏ผŒๅนถไฟ่ฏไธคไฝๆ•ฐ
const minutes = ('0' + date.getMinutes()).slice(-2); // ่Žทๅ–ๅˆ†้’Ÿ๏ผŒๅนถไฟ่ฏไธคไฝๆ•ฐ
const seconds = ('0' + date.getSeconds()).slice(-2); // ่Žทๅ–็ง’้’Ÿ๏ผŒๅนถไฟ่ฏไธคไฝๆ•ฐ
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // ๆ ผๅผๅŒ–่พ“ๅ‡บ
};
// ไฟฎๆ”นrenderDurationๅ‡ฝๆ•ฐไปฅๅŒ…ๅซ้ขœ่‰ฒ้€ป่พ‘
function renderDuration(submit_time, finishTime) {
if (!submit_time || !finishTime) return 'N/A';
const start = new Date(submit_time);
const finish = new Date(finishTime);
const durationMs = finish - start;
const durationSec = (durationMs / 1000).toFixed(1);
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}>
{durationSec} {t('็ง’')}
</Tag>
);
}
// ๅฎšไน‰ๆ‰€ๆœ‰ๅˆ—
const allColumns = [
{
key: COLUMN_KEYS.SUBMIT_TIME,
title: t('ๆไบคๆ—ถ้—ด'),
dataIndex: 'submit_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text / 1000)}</div>;
},
},
{
key: COLUMN_KEYS.DURATION,
title: t('่Šฑ่ดนๆ—ถ้—ด'),
dataIndex: 'finish_time',
render: (finish, record) => {
return renderDuration(record.submit_time, finish);
},
},
{
key: COLUMN_KEYS.CHANNEL,
title: t('ๆธ ้“'),
dataIndex: 'channel_id',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return isAdminUser ? (
<div>
<Tag
color={colors[parseInt(text) % colors.length]}
shape='circle'
prefixIcon={<Hash size={14} />}
onClick={() => {
copyText(text);
}}
>
{' '}
{text}{' '}
</Tag>
</div>
) : (
<></>
);
},
},
{
key: COLUMN_KEYS.TYPE,
title: t('็ฑปๅž‹'),
dataIndex: 'action',
render: (text, record, index) => {
return <div>{renderType(text)}</div>;
},
},
{
key: COLUMN_KEYS.TASK_ID,
title: t('ไปปๅŠกID'),
dataIndex: 'mj_id',
render: (text, record, index) => {
return <div>{text}</div>;
},
},
{
key: COLUMN_KEYS.SUBMIT_RESULT,
title: t('ๆไบค็ป“ๆžœ'),
dataIndex: 'code',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return isAdminUser ? <div>{renderCode(text)}</div> : <></>;
},
},
{
key: COLUMN_KEYS.TASK_STATUS,
title: t('ไปปๅŠก็Šถๆ€'),
dataIndex: 'status',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return <div>{renderStatus(text)}</div>;
},
},
{
key: COLUMN_KEYS.PROGRESS,
title: t('่ฟ›ๅบฆ'),
dataIndex: 'progress',
render: (text, record, index) => {
return (
<div>
{
<Progress
stroke={
record.status === 'FAILURE'
? 'var(--semi-color-warning)'
: null
}
percent={text ? parseInt(text.replace('%', '')) : 0}
showInfo={true}
aria-label='drawing progress'
style={{ minWidth: '160px' }}
/>
}
</div>
);
},
},
{
key: COLUMN_KEYS.IMAGE,
title: t('็ป“ๆžœๅ›พ็‰‡'),
dataIndex: 'image_url',
render: (text, record, index) => {
if (!text) {
return t('ๆ— ');
}
return (
<Button
size="small"
onClick={() => {
setModalImageUrl(text);
setIsModalOpenurl(true);
}}
>
{t('ๆŸฅ็œ‹ๅ›พ็‰‡')}
</Button>
);
},
},
{
key: COLUMN_KEYS.PROMPT,
title: 'Prompt',
dataIndex: 'prompt',
render: (text, record, index) => {
if (!text) {
return t('ๆ— ');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
},
},
{
key: COLUMN_KEYS.PROMPT_EN,
title: 'PromptEn',
dataIndex: 'prompt_en',
render: (text, record, index) => {
if (!text) {
return t('ๆ— ');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
},
},
{
key: COLUMN_KEYS.FAIL_REASON,
title: t('ๅคฑ่ดฅๅŽŸๅ› '),
dataIndex: 'fail_reason',
fixed: 'right',
render: (text, record, index) => {
if (!text) {
return t('ๆ— ');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
},
},
];
// ๆ นๆฎๅฏ่งๆ€ง่ฎพ็ฝฎ่ฟ‡ๆปคๅˆ—
const getVisibleColumns = () => {
return allColumns.filter((column) => visibleColumns[column.key]);
};
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(0);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const [showBanner, setShowBanner] = useState(false);
// ๅฎšไน‰ๆจกๆ€ๆก†ๅ›พ็‰‡URL็š„็Šถๆ€ๅ’Œๆ›ดๆ–ฐๅ‡ฝๆ•ฐ
const [modalImageUrl, setModalImageUrl] = useState('');
let now = new Date();
// Form ๅˆๅง‹ๅ€ผ
const formInitValues = {
channel_id: '',
mj_id: '',
dateRange: [
timestamp2string(now.getTime() / 1000 - 2592000),
timestamp2string(now.getTime() / 1000 + 3600)
],
};
// Form API ๅผ•็”จ
const [formApi, setFormApi] = useState(null);
const [stat, setStat] = useState({
quota: 0,
token: 0,
});
// ่Žทๅ–่กจๅ•ๅ€ผ็š„่พ…ๅŠฉๅ‡ฝๆ•ฐ
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
// ๅค„็†ๆ—ถ้—ด่Œƒๅ›ด
let start_timestamp = timestamp2string(now.getTime() / 1000 - 2592000);
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
start_timestamp = formValues.dateRange[0];
end_timestamp = formValues.dateRange[1];
}
return {
channel_id: formValues.channel_id || '',
mj_id: formValues.mj_id || '',
start_timestamp,
end_timestamp,
};
};
const enrichLogs = (items) => {
return items.map((log) => ({
...log,
timestamp2string: timestamp2string(log.created_at),
key: '' + log.id,
}));
};
const syncPageData = (payload) => {
const items = enrichLogs(payload.items || []);
setLogs(items);
setLogCount(payload.total || 0);
setActivePage(payload.page || 1);
setPageSize(payload.page_size || pageSize);
};
const loadLogs = async (page = 1, size = pageSize) => {
setLoading(true);
const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
let localStartTimestamp = Date.parse(start_timestamp);
let localEndTimestamp = Date.parse(end_timestamp);
const url = isAdminUser
? `/api/mj/?p=${page}&page_size=${size}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
: `/api/mj/self/?p=${page}&page_size=${size}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
syncPageData(data);
} else {
showError(message);
}
setLoading(false);
};
const pageData = logs;
const handlePageChange = (page) => {
loadLogs(page, pageSize).then();
};
const handlePageSizeChange = async (size) => {
localStorage.setItem('mj-page-size', size + '');
await loadLogs(1, size);
};
const refresh = async () => {
await loadLogs(1, pageSize);
};
const copyText = async (text) => {
if (await copy(text)) {
showSuccess(t('ๅทฒๅคๅˆถ๏ผš') + text);
} else {
// setSearchKeyword(text);
Modal.error({ title: t('ๆ— ๆณ•ๅคๅˆถๅˆฐๅ‰ช่ดดๆฟ๏ผŒ่ฏทๆ‰‹ๅŠจๅคๅˆถ'), content: text });
}
};
useEffect(() => {
const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize);
loadLogs(1, localPageSize).then();
}, []);
useEffect(() => {
const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
if (mjNotifyEnabled !== 'true') {
setShowBanner(true);
}
}, []);
// ๅˆ—้€‰ๆ‹ฉๅ™จๆจกๆ€ๆก†
const renderColumnSelector = () => {
return (
<Modal
title={t('ๅˆ—่ฎพ็ฝฎ')}
visible={showColumnSelector}
onCancel={() => setShowColumnSelector(false)}
footer={
<div className="flex justify-end">
<Button onClick={() => initDefaultColumns()}>
{t('้‡็ฝฎ')}
</Button>
<Button onClick={() => setShowColumnSelector(false)}>
{t('ๅ–ๆถˆ')}
</Button>
<Button onClick={() => setShowColumnSelector(false)}>
{t('็กฎๅฎš')}
</Button>
</div>
}
>
<div style={{ marginBottom: 20 }}>
<Checkbox
checked={Object.values(visibleColumns).every((v) => v === true)}
indeterminate={
Object.values(visibleColumns).some((v) => v === true) &&
!Object.values(visibleColumns).every((v) => v === true)
}
onChange={(e) => handleSelectAll(e.target.checked)}
>
{t('ๅ…จ้€‰')}
</Checkbox>
</div>
<div className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4" style={{ border: '1px solid var(--semi-color-border)' }}>
{allColumns.map((column) => {
// ไธบ้ž็ฎก็†ๅ‘˜็”จๆˆท่ทณ่ฟ‡็ฎก็†ๅ‘˜ไธ“็”จๅˆ—
if (
!isAdminUser &&
(column.key === COLUMN_KEYS.CHANNEL ||
column.key === COLUMN_KEYS.SUBMIT_RESULT)
) {
return null;
}
return (
<div key={column.key} className="w-1/2 mb-4 pr-2">
<Checkbox
checked={!!visibleColumns[column.key]}
onChange={(e) =>
handleColumnVisibilityChange(column.key, e.target.checked)
}
>
{column.title}
</Checkbox>
</div>
);
})}
</div>
</Modal>
);
};
return (
<>
{renderColumnSelector()}
<Layout>
<CardPro
type="type2"
className="mb-4"
statsArea={
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
<IconEyeOpened className="mr-2" />
{loading ? (
<Skeleton.Title
style={{
width: 300,
marginBottom: 0,
marginTop: 0
}}
/>
) : (
<Text>
{isAdminUser && showBanner
? t('ๅฝ“ๅ‰ๆœชๅผ€ๅฏMidjourneyๅ›ž่ฐƒ๏ผŒ้ƒจๅˆ†้กน็›ฎๅฏ่ƒฝๆ— ๆณ•่Žทๅพ—็ป˜ๅ›พ็ป“ๆžœ๏ผŒๅฏๅœจ่ฟ่ฅ่ฎพ็ฝฎไธญๅผ€ๅฏใ€‚')
: t('Midjourney ไปปๅŠก่ฎฐๅฝ•')}
</Text>
)}
</div>
<Button
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('่‡ช้€‚ๅบ”ๅˆ—่กจ') : t('็ดงๅ‡‘ๅˆ—่กจ')}
</Button>
</div>
}
searchArea={
<Form
initValues={formInitValues}
getFormApi={(api) => setFormApi(api)}
onSubmit={refresh}
allowEmpty={true}
autoComplete="off"
layout="vertical"
trigger="change"
stopValidateWithError={false}
>
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* ๆ—ถ้—ด้€‰ๆ‹ฉๅ™จ */}
<div className="col-span-1 lg:col-span-2">
<Form.DatePicker
field='dateRange'
className="w-full"
type='dateTimeRange'
placeholder={[t('ๅผ€ๅง‹ๆ—ถ้—ด'), t('็ป“ๆŸๆ—ถ้—ด')]}
showClear
pure
size="small"
/>
</div>
{/* ไปปๅŠก ID */}
<Form.Input
field='mj_id'
prefix={<IconSearch />}
placeholder={t('ไปปๅŠก ID')}
showClear
pure
size="small"
/>
{/* ๆธ ้“ ID - ไป…็ฎก็†ๅ‘˜ๅฏ่ง */}
{isAdminUser && (
<Form.Input
field='channel_id'
prefix={<IconSearch />}
placeholder={t('ๆธ ้“ ID')}
showClear
pure
size="small"
/>
)}
</div>
{/* ๆ“ไฝœๆŒ‰้’ฎๅŒบๅŸŸ */}
<div className="flex justify-between items-center">
<div></div>
<div className="flex gap-2">
<Button
type='tertiary'
htmlType='submit'
loading={loading}
size="small"
>
{t('ๆŸฅ่ฏข')}
</Button>
<Button
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
// ้‡็ฝฎๅŽ็ซ‹ๅณๆŸฅ่ฏข๏ผŒไฝฟ็”จsetTimeout็กฎไฟ่กจๅ•้‡็ฝฎๅฎŒๆˆ
setTimeout(() => {
refresh();
}, 100);
}
}}
size="small"
>
{t('้‡็ฝฎ')}
</Button>
<Button
type='tertiary'
onClick={() => setShowColumnSelector(true)}
size="small"
>
{t('ๅˆ—่ฎพ็ฝฎ')}
</Button>
</div>
</div>
</div>
</Form>
}
>
<Table
columns={compactMode ? getVisibleColumns().map(({ fixed, ...rest }) => rest) : getVisibleColumns()}
dataSource={logs}
rowKey='key'
loading={loading}
scroll={compactMode ? undefined : { x: 'max-content' }}
className="rounded-xl overflow-hidden"
size="middle"
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
description={t('ๆœ็ดขๆ— ็ป“ๆžœ')}
style={{ padding: 30 }}
/>
}
pagination={{
currentPage: activePage,
pageSize: pageSize,
total: logCount,
pageSizeOptions: [10, 20, 50, 100],
showSizeChanger: true,
onPageSizeChange: handlePageSizeChange,
onPageChange: handlePageChange,
}}
/>
</CardPro>
<Modal
visible={isModalOpen}
onOk={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
closable={null}
bodyStyle={{ height: '400px', overflow: 'auto' }} // ่ฎพ็ฝฎๆจกๆ€ๆก†ๅ†…ๅฎนๅŒบๅŸŸๆ ทๅผ
width={800} // ่ฎพ็ฝฎๆจกๆ€ๆก†ๅฎฝๅบฆ
>
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
</Modal>
<ImagePreview
src={modalImageUrl}
visible={isModalOpenurl}
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
/>
</Layout>
</>
);
};
export default LogsTable;
// ้‡ๆž„ๅŽ็š„ MjLogsTable - ไฝฟ็”จๆ–ฐ็š„ๆจกๅ—ๅŒ–ๆžถๆž„
export { default } from './mj-logs/index.jsx';
\ No newline at end of file
// ้‡ๆž„ๅŽ็š„ LogsTable - ไฝฟ็”จๆ–ฐ็š„ๆจกๅ—ๅŒ–ๆžถๆž„
// ้‡ๆž„ๅŽ็š„ UsageLogsTable - ไฝฟ็”จๆ–ฐ็š„ๆจกๅ—ๅŒ–ๆžถๆž„
export { default } from './usage-logs/index.jsx';
\ No newline at end of file
import React from 'react';
import { Button, Skeleton, Typography } from '@douyinfe/semi-ui';
import { IconEyeOpened } from '@douyinfe/semi-icons';
const { Text } = Typography;
const MjLogsActions = ({
loading,
showBanner,
isAdminUser,
compactMode,
setCompactMode,
t,
}) => {
return (
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
<IconEyeOpened className="mr-2" />
{loading ? (
<Skeleton.Title
style={{
width: 300,
marginBottom: 0,
marginTop: 0
}}
/>
) : (
<Text>
{isAdminUser && showBanner
? t('ๅฝ“ๅ‰ๆœชๅผ€ๅฏMidjourneyๅ›ž่ฐƒ๏ผŒ้ƒจๅˆ†้กน็›ฎๅฏ่ƒฝๆ— ๆณ•่Žทๅพ—็ป˜ๅ›พ็ป“ๆžœ๏ผŒๅฏๅœจ่ฟ่ฅ่ฎพ็ฝฎไธญๅผ€ๅฏใ€‚')
: t('Midjourney ไปปๅŠก่ฎฐๅฝ•')}
</Text>
)}
</div>
<Button
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('่‡ช้€‚ๅบ”ๅˆ—่กจ') : t('็ดงๅ‡‘ๅˆ—่กจ')}
</Button>
</div>
);
};
export default MjLogsActions;
\ No newline at end of file
import React from 'react';
import {
Button,
Progress,
Tag,
Typography
} from '@douyinfe/semi-ui';
import {
Palette,
ZoomIn,
Shuffle,
Move,
FileText,
Blend,
Upload,
Minimize2,
RotateCcw,
PaintBucket,
Focus,
Move3D,
Monitor,
UserCheck,
HelpCircle,
CheckCircle,
Clock,
Copy,
FileX,
Pause,
XCircle,
Loader,
AlertCircle,
Hash,
Video
} from 'lucide-react';
const colors = [
'amber',
'blue',
'cyan',
'green',
'grey',
'indigo',
'light-blue',
'lime',
'orange',
'pink',
'purple',
'red',
'teal',
'violet',
'yellow',
];
// Render functions
function renderType(type, t) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' shape='circle' prefixIcon={<Palette size={14} />}>
{t('็ป˜ๅ›พ')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' shape='circle' prefixIcon={<ZoomIn size={14} />}>
{t('ๆ”พๅคง')}
</Tag>
);
case 'VIDEO':
return (
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('่ง†้ข‘')}
</Tag>
);
case 'EDITS':
return (
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('็ผ–่พ‘')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('ๅ˜ๆข')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('ๅผบๅ˜ๆข')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('ๅผฑๅ˜ๆข')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' shape='circle' prefixIcon={<Move size={14} />}>
{t('ๅนณ็งป')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' shape='circle' prefixIcon={<FileText size={14} />}>
{t('ๅ›พ็”Ÿๆ–‡')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' shape='circle' prefixIcon={<Blend size={14} />}>
{t('ๅ›พๆททๅˆ')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' shape='circle' prefixIcon={<Upload size={14} />}>
ไธŠไผ ๆ–‡ไปถ
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' shape='circle' prefixIcon={<Minimize2 size={14} />}>
{t('็ผฉ่ฏ')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' shape='circle' prefixIcon={<RotateCcw size={14} />}>
{t('้‡็ป˜')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
{t('ๅฑ€้ƒจ้‡็ป˜-ๆไบค')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' shape='circle' prefixIcon={<Focus size={14} />}>
{t('ๅ˜็„ฆ')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' shape='circle' prefixIcon={<Move3D size={14} />}>
{t('่‡ชๅฎšไน‰ๅ˜็„ฆ-ๆไบค')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' shape='circle' prefixIcon={<Monitor size={14} />}>
{t('็ช—ๅฃๅค„็†')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
{t('ๆข่„ธ')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('ๆœช็Ÿฅ')}
</Tag>
);
}
}
function renderCode(code, t) {
switch (code) {
case 1:
return (
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('ๅทฒๆไบค')}
</Tag>
);
case 21:
return (
<Tag color='lime' shape='circle' prefixIcon={<Clock size={14} />}>
{t('็ญ‰ๅพ…ไธญ')}
</Tag>
);
case 22:
return (
<Tag color='orange' shape='circle' prefixIcon={<Copy size={14} />}>
{t('้‡ๅคๆไบค')}
</Tag>
);
case 0:
return (
<Tag color='yellow' shape='circle' prefixIcon={<FileX size={14} />}>
{t('ๆœชๆไบค')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('ๆœช็Ÿฅ')}
</Tag>
);
}
}
function renderStatus(type, t) {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('ๆˆๅŠŸ')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
{t('ๆœชๅฏๅŠจ')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
{t('้˜Ÿๅˆ—ไธญ')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' shape='circle' prefixIcon={<Loader size={14} />}>
{t('ๆ‰ง่กŒไธญ')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('ๅคฑ่ดฅ')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
{t('็ช—ๅฃ็ญ‰ๅพ…')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('ๆœช็Ÿฅ')}
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000);
const year = date.getFullYear();
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);
const hours = ('0' + date.getHours()).slice(-2);
const minutes = ('0' + date.getMinutes()).slice(-2);
const seconds = ('0' + date.getSeconds()).slice(-2);
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
function renderDuration(submit_time, finishTime, t) {
if (!submit_time || !finishTime) return 'N/A';
const start = new Date(submit_time);
const finish = new Date(finishTime);
const durationMs = finish - start;
const durationSec = (durationMs / 1000).toFixed(1);
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}>
{durationSec} {t('็ง’')}
</Tag>
);
}
export const getMjLogsColumns = ({
t,
COLUMN_KEYS,
copyText,
openContentModal,
openImageModal,
isAdminUser,
}) => {
return [
{
key: COLUMN_KEYS.SUBMIT_TIME,
title: t('ๆไบคๆ—ถ้—ด'),
dataIndex: 'submit_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text / 1000)}</div>;
},
},
{
key: COLUMN_KEYS.DURATION,
title: t('่Šฑ่ดนๆ—ถ้—ด'),
dataIndex: 'finish_time',
render: (finish, record) => {
return renderDuration(record.submit_time, finish, t);
},
},
{
key: COLUMN_KEYS.CHANNEL,
title: t('ๆธ ้“'),
dataIndex: 'channel_id',
render: (text, record, index) => {
return isAdminUser ? (
<div>
<Tag
color={colors[parseInt(text) % colors.length]}
shape='circle'
prefixIcon={<Hash size={14} />}
onClick={() => {
copyText(text);
}}
>
{' '}
{text}{' '}
</Tag>
</div>
) : (
<></>
);
},
},
{
key: COLUMN_KEYS.TYPE,
title: t('็ฑปๅž‹'),
dataIndex: 'action',
render: (text, record, index) => {
return <div>{renderType(text, t)}</div>;
},
},
{
key: COLUMN_KEYS.TASK_ID,
title: t('ไปปๅŠกID'),
dataIndex: 'mj_id',
render: (text, record, index) => {
return <div>{text}</div>;
},
},
{
key: COLUMN_KEYS.SUBMIT_RESULT,
title: t('ๆไบค็ป“ๆžœ'),
dataIndex: 'code',
render: (text, record, index) => {
return isAdminUser ? <div>{renderCode(text, t)}</div> : <></>;
},
},
{
key: COLUMN_KEYS.TASK_STATUS,
title: t('ไปปๅŠก็Šถๆ€'),
dataIndex: 'status',
render: (text, record, index) => {
return <div>{renderStatus(text, t)}</div>;
},
},
{
key: COLUMN_KEYS.PROGRESS,
title: t('่ฟ›ๅบฆ'),
dataIndex: 'progress',
render: (text, record, index) => {
return (
<div>
{
<Progress
stroke={
record.status === 'FAILURE'
? 'var(--semi-color-warning)'
: null
}
percent={text ? parseInt(text.replace('%', '')) : 0}
showInfo={true}
aria-label='drawing progress'
style={{ minWidth: '160px' }}
/>
}
</div>
);
},
},
{
key: COLUMN_KEYS.IMAGE,
title: t('็ป“ๆžœๅ›พ็‰‡'),
dataIndex: 'image_url',
render: (text, record, index) => {
if (!text) {
return t('ๆ— ');
}
return (
<Button
size="small"
onClick={() => {
openImageModal(text);
}}
>
{t('ๆŸฅ็œ‹ๅ›พ็‰‡')}
</Button>
);
},
},
{
key: COLUMN_KEYS.PROMPT,
title: 'Prompt',
dataIndex: 'prompt',
render: (text, record, index) => {
if (!text) {
return t('ๆ— ');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
openContentModal(text);
}}
>
{text}
</Typography.Text>
);
},
},
{
key: COLUMN_KEYS.PROMPT_EN,
title: 'PromptEn',
dataIndex: 'prompt_en',
render: (text, record, index) => {
if (!text) {
return t('ๆ— ');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
openContentModal(text);
}}
>
{text}
</Typography.Text>
);
},
},
{
key: COLUMN_KEYS.FAIL_REASON,
title: t('ๅคฑ่ดฅๅŽŸๅ› '),
dataIndex: 'fail_reason',
fixed: 'right',
render: (text, record, index) => {
if (!text) {
return t('ๆ— ');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
openContentModal(text);
}}
>
{text}
</Typography.Text>
);
},
},
];
};
\ No newline at end of file
import React from 'react';
import { Button, Form } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons';
const MjLogsFilters = ({
formInitValues,
setFormApi,
refresh,
setShowColumnSelector,
formApi,
loading,
isAdminUser,
t,
}) => {
return (
<Form
initValues={formInitValues}
getFormApi={(api) => setFormApi(api)}
onSubmit={refresh}
allowEmpty={true}
autoComplete="off"
layout="vertical"
trigger="change"
stopValidateWithError={false}
>
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
{/* ๆ—ถ้—ด้€‰ๆ‹ฉๅ™จ */}
<div className="col-span-1 lg:col-span-2">
<Form.DatePicker
field='dateRange'
className="w-full"
type='dateTimeRange'
placeholder={[t('ๅผ€ๅง‹ๆ—ถ้—ด'), t('็ป“ๆŸๆ—ถ้—ด')]}
showClear
pure
size="small"
/>
</div>
{/* ไปปๅŠก ID */}
<Form.Input
field='mj_id'
prefix={<IconSearch />}
placeholder={t('ไปปๅŠก ID')}
showClear
pure
size="small"
/>
{/* ๆธ ้“ ID - ไป…็ฎก็†ๅ‘˜ๅฏ่ง */}
{isAdminUser && (
<Form.Input
field='channel_id'
prefix={<IconSearch />}
placeholder={t('ๆธ ้“ ID')}
showClear
pure
size="small"
/>
)}
</div>
{/* ๆ“ไฝœๆŒ‰้’ฎๅŒบๅŸŸ */}
<div className="flex justify-between items-center">
<div></div>
<div className="flex gap-2">
<Button
type='tertiary'
htmlType='submit'
loading={loading}
size="small"
>
{t('ๆŸฅ่ฏข')}
</Button>
<Button
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
setTimeout(() => {
refresh();
}, 100);
}
}}
size="small"
>
{t('้‡็ฝฎ')}
</Button>
<Button
type='tertiary'
onClick={() => setShowColumnSelector(true)}
size="small"
>
{t('ๅˆ—่ฎพ็ฝฎ')}
</Button>
</div>
</div>
</div>
</Form>
);
};
export default MjLogsFilters;
\ No newline at end of file
import React, { useMemo } from 'react';
import { Table, Empty } from '@douyinfe/semi-ui';
import {
IllustrationNoResult,
IllustrationNoResultDark,
} from '@douyinfe/semi-illustrations';
import { getMjLogsColumns } from './MjLogsColumnDefs.js';
const MjLogsTable = (mjLogsData) => {
const {
logs,
loading,
activePage,
pageSize,
logCount,
compactMode,
visibleColumns,
handlePageChange,
handlePageSizeChange,
copyText,
openContentModal,
openImageModal,
isAdminUser,
t,
COLUMN_KEYS,
} = mjLogsData;
// Get all columns
const allColumns = useMemo(() => {
return getMjLogsColumns({
t,
COLUMN_KEYS,
copyText,
openContentModal,
openImageModal,
isAdminUser,
});
}, [
t,
COLUMN_KEYS,
copyText,
openContentModal,
openImageModal,
isAdminUser,
]);
// Filter columns based on visibility settings
const getVisibleColumns = () => {
return allColumns.filter((column) => visibleColumns[column.key]);
};
const visibleColumnsList = useMemo(() => {
return getVisibleColumns();
}, [visibleColumns, allColumns]);
const tableColumns = useMemo(() => {
return compactMode
? visibleColumnsList.map(({ fixed, ...rest }) => rest)
: visibleColumnsList;
}, [compactMode, visibleColumnsList]);
return (
<Table
columns={tableColumns}
dataSource={logs}
rowKey='key'
loading={loading}
scroll={compactMode ? undefined : { x: 'max-content' }}
className="rounded-xl overflow-hidden"
size="middle"
empty={
<Empty
image={
<IllustrationNoResult style={{ width: 150, height: 150 }} />
}
darkModeImage={
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
}
description={t('ๆœ็ดขๆ— ็ป“ๆžœ')}
style={{ padding: 30 }}
/>
}
pagination={{
currentPage: activePage,
pageSize: pageSize,
total: logCount,
pageSizeOptions: [10, 20, 50, 100],
showSizeChanger: true,
onPageSizeChange: handlePageSizeChange,
onPageChange: handlePageChange,
}}
/>
);
};
export default MjLogsTable;
\ No newline at end of file
import React from 'react';
import { Layout } from '@douyinfe/semi-ui';
import CardPro from '../../common/ui/CardPro.js';
import MjLogsTable from './MjLogsTable.jsx';
import MjLogsActions from './MjLogsActions.jsx';
import MjLogsFilters from './MjLogsFilters.jsx';
import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx';
import ContentModal from './modals/ContentModal.jsx';
import { useMjLogsData } from '../../../hooks/mj-logs/useMjLogsData.js';
const MjLogsPage = () => {
const mjLogsData = useMjLogsData();
return (
<>
{/* Modals */}
<ColumnSelectorModal {...mjLogsData} />
<ContentModal {...mjLogsData} />
<Layout>
<CardPro
type="type2"
statsArea={<MjLogsActions {...mjLogsData} />}
searchArea={<MjLogsFilters {...mjLogsData} />}
>
<MjLogsTable {...mjLogsData} />
</CardPro>
</Layout>
</>
);
};
export default MjLogsPage;
\ No newline at end of file
import React from 'react';
import { Modal, Button, Checkbox } from '@douyinfe/semi-ui';
import { getMjLogsColumns } from '../MjLogsColumnDefs.js';
const ColumnSelectorModal = ({
showColumnSelector,
setShowColumnSelector,
visibleColumns,
handleColumnVisibilityChange,
handleSelectAll,
initDefaultColumns,
COLUMN_KEYS,
isAdminUser,
copyText,
openContentModal,
openImageModal,
t,
}) => {
// Get all columns for display in selector
const allColumns = getMjLogsColumns({
t,
COLUMN_KEYS,
copyText,
openContentModal,
openImageModal,
isAdminUser,
});
return (
<Modal
title={t('ๅˆ—่ฎพ็ฝฎ')}
visible={showColumnSelector}
onCancel={() => setShowColumnSelector(false)}
footer={
<div className="flex justify-end">
<Button onClick={() => initDefaultColumns()}>
{t('้‡็ฝฎ')}
</Button>
<Button onClick={() => setShowColumnSelector(false)}>
{t('ๅ–ๆถˆ')}
</Button>
<Button onClick={() => setShowColumnSelector(false)}>
{t('็กฎๅฎš')}
</Button>
</div>
}
>
<div style={{ marginBottom: 20 }}>
<Checkbox
checked={Object.values(visibleColumns).every((v) => v === true)}
indeterminate={
Object.values(visibleColumns).some((v) => v === true) &&
!Object.values(visibleColumns).every((v) => v === true)
}
onChange={(e) => handleSelectAll(e.target.checked)}
>
{t('ๅ…จ้€‰')}
</Checkbox>
</div>
<div
className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4"
style={{ border: '1px solid var(--semi-color-border)' }}
>
{allColumns.map((column) => {
// Skip admin-only columns for non-admin users
if (
!isAdminUser &&
(column.key === COLUMN_KEYS.CHANNEL ||
column.key === COLUMN_KEYS.SUBMIT_RESULT)
) {
return null;
}
return (
<div key={column.key} className="w-1/2 mb-4 pr-2">
<Checkbox
checked={!!visibleColumns[column.key]}
onChange={(e) =>
handleColumnVisibilityChange(column.key, e.target.checked)
}
>
{column.title}
</Checkbox>
</div>
);
})}
</div>
</Modal>
);
};
export default ColumnSelectorModal;
\ No newline at end of file
import React from 'react';
import { Modal, ImagePreview } from '@douyinfe/semi-ui';
const ContentModal = ({
isModalOpen,
setIsModalOpen,
modalContent,
isModalOpenurl,
setIsModalOpenurl,
modalImageUrl,
}) => {
return (
<>
{/* Text Content Modal */}
<Modal
visible={isModalOpen}
onOk={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
closable={null}
bodyStyle={{ height: '400px', overflow: 'auto' }}
width={800}
>
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
</Modal>
{/* Image Preview Modal */}
<ImagePreview
src={modalImageUrl}
visible={isModalOpenurl}
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
/>
</>
);
};
export default ContentModal;
\ No newline at end of file
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@douyinfe/semi-ui';
import {
API,
copy,
isAdmin,
showError,
showSuccess,
timestamp2string
} from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
import { useTableCompactMode } from '../common/useTableCompactMode';
export const useMjLogsData = () => {
const { t } = useTranslation();
// Define column keys for selection
const COLUMN_KEYS = {
SUBMIT_TIME: 'submit_time',
DURATION: 'duration',
CHANNEL: 'channel',
TYPE: 'type',
TASK_ID: 'task_id',
SUBMIT_RESULT: 'submit_result',
TASK_STATUS: 'task_status',
PROGRESS: 'progress',
IMAGE: 'image',
PROMPT: 'prompt',
PROMPT_EN: 'prompt_en',
FAIL_REASON: 'fail_reason',
};
// Basic state
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(0);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [showBanner, setShowBanner] = useState(false);
// User and admin
const isAdminUser = isAdmin();
// Modal states
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const [modalImageUrl, setModalImageUrl] = useState('');
// Form state
const [formApi, setFormApi] = useState(null);
let now = new Date();
const formInitValues = {
channel_id: '',
mj_id: '',
dateRange: [
timestamp2string(now.getTime() / 1000 - 2592000),
timestamp2string(now.getTime() / 1000 + 3600)
],
};
// Column visibility state
const [visibleColumns, setVisibleColumns] = useState({});
const [showColumnSelector, setShowColumnSelector] = useState(false);
// Compact mode
const [compactMode, setCompactMode] = useTableCompactMode('mjLogs');
// Load saved column preferences from localStorage
useEffect(() => {
const savedColumns = localStorage.getItem('mj-logs-table-columns');
if (savedColumns) {
try {
const parsed = JSON.parse(savedColumns);
const defaults = getDefaultColumnVisibility();
const merged = { ...defaults, ...parsed };
setVisibleColumns(merged);
} catch (e) {
console.error('Failed to parse saved column preferences', e);
initDefaultColumns();
}
} else {
initDefaultColumns();
}
}, []);
// Check banner notification
useEffect(() => {
const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
if (mjNotifyEnabled !== 'true') {
setShowBanner(true);
}
}, []);
// Get default column visibility based on user role
const getDefaultColumnVisibility = () => {
return {
[COLUMN_KEYS.SUBMIT_TIME]: true,
[COLUMN_KEYS.DURATION]: true,
[COLUMN_KEYS.CHANNEL]: isAdminUser,
[COLUMN_KEYS.TYPE]: true,
[COLUMN_KEYS.TASK_ID]: true,
[COLUMN_KEYS.SUBMIT_RESULT]: isAdminUser,
[COLUMN_KEYS.TASK_STATUS]: true,
[COLUMN_KEYS.PROGRESS]: true,
[COLUMN_KEYS.IMAGE]: true,
[COLUMN_KEYS.PROMPT]: true,
[COLUMN_KEYS.PROMPT_EN]: true,
[COLUMN_KEYS.FAIL_REASON]: true,
};
};
// Initialize default column visibility
const initDefaultColumns = () => {
const defaults = getDefaultColumnVisibility();
setVisibleColumns(defaults);
localStorage.setItem('mj-logs-table-columns', JSON.stringify(defaults));
};
// Handle column visibility change
const handleColumnVisibilityChange = (columnKey, checked) => {
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
setVisibleColumns(updatedColumns);
};
// Handle "Select All" checkbox
const handleSelectAll = (checked) => {
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
const updatedColumns = {};
allKeys.forEach((key) => {
if (
(key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.SUBMIT_RESULT) &&
!isAdminUser
) {
updatedColumns[key] = false;
} else {
updatedColumns[key] = checked;
}
});
setVisibleColumns(updatedColumns);
};
// Update table when column visibility changes
useEffect(() => {
if (Object.keys(visibleColumns).length > 0) {
localStorage.setItem('mj-logs-table-columns', JSON.stringify(visibleColumns));
}
}, [visibleColumns]);
// Get form values helper function
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
let start_timestamp = timestamp2string(now.getTime() / 1000 - 2592000);
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
if (
formValues.dateRange &&
Array.isArray(formValues.dateRange) &&
formValues.dateRange.length === 2
) {
start_timestamp = formValues.dateRange[0];
end_timestamp = formValues.dateRange[1];
}
return {
channel_id: formValues.channel_id || '',
mj_id: formValues.mj_id || '',
start_timestamp,
end_timestamp,
};
};
// Enrich logs data
const enrichLogs = (items) => {
return items.map((log) => ({
...log,
timestamp2string: timestamp2string(log.created_at),
key: '' + log.id,
}));
};
// Sync page data
const syncPageData = (payload) => {
const items = enrichLogs(payload.items || []);
setLogs(items);
setLogCount(payload.total || 0);
setActivePage(payload.page || 1);
setPageSize(payload.page_size || pageSize);
};
// Load logs function
const loadLogs = async (page = 1, size = pageSize) => {
setLoading(true);
const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
let localStartTimestamp = Date.parse(start_timestamp);
let localEndTimestamp = Date.parse(end_timestamp);
const url = isAdminUser
? `/api/mj/?p=${page}&page_size=${size}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
: `/api/mj/self/?p=${page}&page_size=${size}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
syncPageData(data);
} else {
showError(message);
}
setLoading(false);
};
// Page handlers
const handlePageChange = (page) => {
loadLogs(page, pageSize).then();
};
const handlePageSizeChange = async (size) => {
localStorage.setItem('mj-page-size', size + '');
await loadLogs(1, size);
};
// Refresh function
const refresh = async () => {
await loadLogs(1, pageSize);
};
// Copy text function
const copyText = async (text) => {
if (await copy(text)) {
showSuccess(t('ๅทฒๅคๅˆถ๏ผš') + text);
} else {
Modal.error({ title: t('ๆ— ๆณ•ๅคๅˆถๅˆฐๅ‰ช่ดดๆฟ๏ผŒ่ฏทๆ‰‹ๅŠจๅคๅˆถ'), content: text });
}
};
// Modal handlers
const openContentModal = (content) => {
setModalContent(content);
setIsModalOpen(true);
};
const openImageModal = (imageUrl) => {
setModalImageUrl(imageUrl);
setIsModalOpenurl(true);
};
// Initialize data
useEffect(() => {
const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize);
loadLogs(1, localPageSize).then();
}, []);
return {
// Basic state
logs,
loading,
activePage,
logCount,
pageSize,
showBanner,
isAdminUser,
// Modal state
isModalOpen,
setIsModalOpen,
modalContent,
isModalOpenurl,
setIsModalOpenurl,
modalImageUrl,
// Form state
formApi,
setFormApi,
formInitValues,
getFormValues,
// Column visibility
visibleColumns,
showColumnSelector,
setShowColumnSelector,
handleColumnVisibilityChange,
handleSelectAll,
initDefaultColumns,
COLUMN_KEYS,
// Compact mode
compactMode,
setCompactMode,
// Functions
loadLogs,
handlePageChange,
handlePageSizeChange,
refresh,
copyText,
openContentModal,
openImageModal,
enrichLogs,
syncPageData,
// Translation
t,
};
};
\ No newline at end of file
......@@ -2,600 +2,600 @@ import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@douyinfe/semi-ui';
import {
API,
getTodayStartTimestamp,
isAdmin,
showError,
showSuccess,
timestamp2string,
renderQuota,
renderNumber,
getLogOther,
copy,
renderClaudeLogContent,
renderLogContent,
renderAudioModelPrice,
renderClaudeModelPrice,
renderModelPrice
API,
getTodayStartTimestamp,
isAdmin,
showError,
showSuccess,
timestamp2string,
renderQuota,
renderNumber,
getLogOther,
copy,
renderClaudeLogContent,
renderLogContent,
renderAudioModelPrice,
renderClaudeModelPrice,
renderModelPrice
} from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
import { useTableCompactMode } from '../common/useTableCompactMode';
export const useLogsData = () => {
const { t } = useTranslation();
// Define column keys for selection
const COLUMN_KEYS = {
TIME: 'time',
CHANNEL: 'channel',
USERNAME: 'username',
TOKEN: 'token',
GROUP: 'group',
TYPE: 'type',
MODEL: 'model',
USE_TIME: 'use_time',
PROMPT: 'prompt',
COMPLETION: 'completion',
COST: 'cost',
RETRY: 'retry',
IP: 'ip',
DETAILS: 'details',
};
// Basic state
const [logs, setLogs] = useState([]);
const [expandData, setExpandData] = useState({});
const [showStat, setShowStat] = useState(false);
const [loading, setLoading] = useState(false);
const [loadingStat, setLoadingStat] = useState(false);
const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [logType, setLogType] = useState(0);
// User and admin
const isAdminUser = isAdmin();
// Statistics state
const [stat, setStat] = useState({
quota: 0,
token: 0,
});
// Form state
const [formApi, setFormApi] = useState(null);
let now = new Date();
const formInitValues = {
username: '',
token_name: '',
model_name: '',
channel: '',
group: '',
dateRange: [
timestamp2string(getTodayStartTimestamp()),
timestamp2string(now.getTime() / 1000 + 3600),
],
logType: '0',
};
// Column visibility state
const [visibleColumns, setVisibleColumns] = useState({});
const [showColumnSelector, setShowColumnSelector] = useState(false);
// Compact mode
const [compactMode, setCompactMode] = useTableCompactMode('logs');
// User info modal state
const [showUserInfo, setShowUserInfoModal] = useState(false);
const [userInfoData, setUserInfoData] = useState(null);
// Load saved column preferences from localStorage
useEffect(() => {
const savedColumns = localStorage.getItem('logs-table-columns');
if (savedColumns) {
try {
const parsed = JSON.parse(savedColumns);
const defaults = getDefaultColumnVisibility();
const merged = { ...defaults, ...parsed };
setVisibleColumns(merged);
} catch (e) {
console.error('Failed to parse saved column preferences', e);
initDefaultColumns();
}
} else {
initDefaultColumns();
}
}, []);
// Get default column visibility based on user role
const getDefaultColumnVisibility = () => {
return {
[COLUMN_KEYS.TIME]: true,
[COLUMN_KEYS.CHANNEL]: isAdminUser,
[COLUMN_KEYS.USERNAME]: isAdminUser,
[COLUMN_KEYS.TOKEN]: true,
[COLUMN_KEYS.GROUP]: true,
[COLUMN_KEYS.TYPE]: true,
[COLUMN_KEYS.MODEL]: true,
[COLUMN_KEYS.USE_TIME]: true,
[COLUMN_KEYS.PROMPT]: true,
[COLUMN_KEYS.COMPLETION]: true,
[COLUMN_KEYS.COST]: true,
[COLUMN_KEYS.RETRY]: isAdminUser,
[COLUMN_KEYS.IP]: true,
[COLUMN_KEYS.DETAILS]: true,
};
};
// Initialize default column visibility
const initDefaultColumns = () => {
const defaults = getDefaultColumnVisibility();
setVisibleColumns(defaults);
localStorage.setItem('logs-table-columns', JSON.stringify(defaults));
};
// Handle column visibility change
const handleColumnVisibilityChange = (columnKey, checked) => {
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
setVisibleColumns(updatedColumns);
};
// Handle "Select All" checkbox
const handleSelectAll = (checked) => {
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
const updatedColumns = {};
allKeys.forEach((key) => {
if (
(key === COLUMN_KEYS.CHANNEL ||
key === COLUMN_KEYS.USERNAME ||
key === COLUMN_KEYS.RETRY) &&
!isAdminUser
) {
updatedColumns[key] = false;
} else {
updatedColumns[key] = checked;
}
});
setVisibleColumns(updatedColumns);
};
// Update table when column visibility changes
useEffect(() => {
if (Object.keys(visibleColumns).length > 0) {
localStorage.setItem(
'logs-table-columns',
JSON.stringify(visibleColumns),
);
}
}, [visibleColumns]);
// ่Žทๅ–่กจๅ•ๅ€ผ็š„่พ…ๅŠฉๅ‡ฝๆ•ฐ๏ผŒ็กฎไฟๆ‰€ๆœ‰ๅ€ผ้ƒฝๆ˜ฏๅญ—็ฌฆไธฒ
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
let start_timestamp = timestamp2string(getTodayStartTimestamp());
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
if (
formValues.dateRange &&
Array.isArray(formValues.dateRange) &&
formValues.dateRange.length === 2
) {
start_timestamp = formValues.dateRange[0];
end_timestamp = formValues.dateRange[1];
}
return {
username: formValues.username || '',
token_name: formValues.token_name || '',
model_name: formValues.model_name || '',
start_timestamp,
end_timestamp,
channel: formValues.channel || '',
group: formValues.group || '',
logType: formValues.logType ? parseInt(formValues.logType) : 0,
};
};
// Statistics functions
const getLogSelfStat = async () => {
const {
token_name,
model_name,
start_timestamp,
end_timestamp,
group,
logType: formLogType,
} = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/self/stat?type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
url = encodeURI(url);
let res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setStat(data);
} else {
showError(message);
}
};
const getLogStat = async () => {
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
group,
logType: formLogType,
} = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/stat?type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
url = encodeURI(url);
let res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setStat(data);
} else {
showError(message);
}
};
const handleEyeClick = async () => {
if (loadingStat) {
return;
}
setLoadingStat(true);
if (isAdminUser) {
await getLogStat();
} else {
await getLogSelfStat();
}
setShowStat(true);
setLoadingStat(false);
};
// User info function
const showUserInfoFunc = async (userId) => {
if (!isAdminUser) {
return;
}
const res = await API.get(`/api/user/${userId}`);
const { success, message, data } = res.data;
if (success) {
setUserInfoData(data);
setShowUserInfoModal(true);
} else {
showError(message);
}
};
// Format logs data
const setLogsFormat = (logs) => {
let expandDatesLocal = {};
for (let i = 0; i < logs.length; i++) {
logs[i].timestamp2string = timestamp2string(logs[i].created_at);
logs[i].key = logs[i].id;
let other = getLogOther(logs[i].other);
let expandDataLocal = [];
if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
expandDataLocal.push({
key: t('ๆธ ้“ไฟกๆฏ'),
value: `${logs[i].channel} - ${logs[i].channel_name || '[ๆœช็Ÿฅ]'}`,
});
}
if (other?.ws || other?.audio) {
expandDataLocal.push({
key: t('่ฏญ้Ÿณ่พ“ๅ…ฅ'),
value: other.audio_input,
});
expandDataLocal.push({
key: t('่ฏญ้Ÿณ่พ“ๅ‡บ'),
value: other.audio_output,
});
expandDataLocal.push({
key: t('ๆ–‡ๅญ—่พ“ๅ…ฅ'),
value: other.text_input,
});
expandDataLocal.push({
key: t('ๆ–‡ๅญ—่พ“ๅ‡บ'),
value: other.text_output,
});
}
if (other?.cache_tokens > 0) {
expandDataLocal.push({
key: t('็ผ“ๅญ˜ Tokens'),
value: other.cache_tokens,
});
}
if (other?.cache_creation_tokens > 0) {
expandDataLocal.push({
key: t('็ผ“ๅญ˜ๅˆ›ๅปบ Tokens'),
value: other.cache_creation_tokens,
});
}
if (logs[i].type === 2) {
expandDataLocal.push({
key: t('ๆ—ฅๅฟ—่ฏฆๆƒ…'),
value: other?.claude
? renderClaudeLogContent(
other?.model_ratio,
other.completion_ratio,
other.model_price,
other.group_ratio,
other?.user_group_ratio,
other.cache_ratio || 1.0,
other.cache_creation_ratio || 1.0,
)
: renderLogContent(
other?.model_ratio,
other.completion_ratio,
other.model_price,
other.group_ratio,
other?.user_group_ratio,
false,
1.0,
other.web_search || false,
other.web_search_call_count || 0,
other.file_search || false,
other.file_search_call_count || 0,
),
});
}
if (logs[i].type === 2) {
let modelMapped =
other?.is_model_mapped &&
other?.upstream_model_name &&
other?.upstream_model_name !== '';
if (modelMapped) {
expandDataLocal.push({
key: t('่ฏทๆฑ‚ๅนถ่ฎก่ดนๆจกๅž‹'),
value: logs[i].model_name,
});
expandDataLocal.push({
key: t('ๅฎž้™…ๆจกๅž‹'),
value: other.upstream_model_name,
});
}
let content = '';
if (other?.ws || other?.audio) {
content = renderAudioModelPrice(
other?.text_input,
other?.text_output,
other?.model_ratio,
other?.model_price,
other?.completion_ratio,
other?.audio_input,
other?.audio_output,
other?.audio_ratio,
other?.audio_completion_ratio,
other?.group_ratio,
other?.user_group_ratio,
other?.cache_tokens || 0,
other?.cache_ratio || 1.0,
);
} else if (other?.claude) {
content = renderClaudeModelPrice(
logs[i].prompt_tokens,
logs[i].completion_tokens,
other.model_ratio,
other.model_price,
other.completion_ratio,
other.group_ratio,
other?.user_group_ratio,
other.cache_tokens || 0,
other.cache_ratio || 1.0,
other.cache_creation_tokens || 0,
other.cache_creation_ratio || 1.0,
);
} else {
content = renderModelPrice(
logs[i].prompt_tokens,
logs[i].completion_tokens,
other?.model_ratio,
other?.model_price,
other?.completion_ratio,
other?.group_ratio,
other?.user_group_ratio,
other?.cache_tokens || 0,
other?.cache_ratio || 1.0,
other?.image || false,
other?.image_ratio || 0,
other?.image_output || 0,
other?.web_search || false,
other?.web_search_call_count || 0,
other?.web_search_price || 0,
other?.file_search || false,
other?.file_search_call_count || 0,
other?.file_search_price || 0,
other?.audio_input_seperate_price || false,
other?.audio_input_token_count || 0,
other?.audio_input_price || 0,
);
}
expandDataLocal.push({
key: t('่ฎก่ดน่ฟ‡็จ‹'),
value: content,
});
if (other?.reasoning_effort) {
expandDataLocal.push({
key: t('Reasoning Effort'),
value: other.reasoning_effort,
});
}
}
expandDatesLocal[logs[i].key] = expandDataLocal;
}
setExpandData(expandDatesLocal);
setLogs(logs);
};
// Load logs function
const loadLogs = async (startIdx, pageSize, customLogType = null) => {
setLoading(true);
let url = '';
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
group,
logType: formLogType,
} = getFormValues();
const currentLogType =
customLogType !== null
? customLogType
: formLogType !== undefined
? formLogType
: logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
} else {
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
}
url = encodeURI(url);
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
const newPageData = data.items;
setActivePage(data.page);
setPageSize(data.page_size);
setLogCount(data.total);
setLogsFormat(newPageData);
} else {
showError(message);
}
setLoading(false);
};
// Page handlers
const handlePageChange = (page) => {
setActivePage(page);
loadLogs(page, pageSize).then((r) => { });
};
const handlePageSizeChange = async (size) => {
localStorage.setItem('page-size', size + '');
setPageSize(size);
setActivePage(1);
loadLogs(activePage, size)
.then()
.catch((reason) => {
showError(reason);
});
};
// Refresh function
const refresh = async () => {
setActivePage(1);
handleEyeClick();
await loadLogs(1, pageSize);
};
// Copy text function
const copyText = async (e, text) => {
e.stopPropagation();
if (await copy(text)) {
showSuccess('ๅทฒๅคๅˆถ๏ผš' + text);
} else {
Modal.error({ title: t('ๆ— ๆณ•ๅคๅˆถๅˆฐๅ‰ช่ดดๆฟ๏ผŒ่ฏทๆ‰‹ๅŠจๅคๅˆถ'), content: text });
}
};
// Initialize data
useEffect(() => {
const localPageSize =
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize);
loadLogs(activePage, localPageSize)
.then()
.catch((reason) => {
showError(reason);
});
}, []);
// Initialize statistics when formApi is available
useEffect(() => {
if (formApi) {
handleEyeClick();
}
}, [formApi]);
// Check if any record has expandable content
const hasExpandableRows = () => {
return logs.some(
(log) => expandData[log.key] && expandData[log.key].length > 0,
);
};
return {
// Basic state
logs,
expandData,
showStat,
loading,
loadingStat,
activePage,
logCount,
pageSize,
logType,
stat,
isAdminUser,
// Form state
formApi,
setFormApi,
formInitValues,
getFormValues,
// Column visibility
visibleColumns,
showColumnSelector,
setShowColumnSelector,
handleColumnVisibilityChange,
handleSelectAll,
initDefaultColumns,
COLUMN_KEYS,
// Compact mode
compactMode,
setCompactMode,
// User info modal
showUserInfo,
setShowUserInfoModal,
userInfoData,
showUserInfoFunc,
// Functions
loadLogs,
handlePageChange,
handlePageSizeChange,
refresh,
copyText,
handleEyeClick,
setLogsFormat,
hasExpandableRows,
setLogType,
// Translation
t,
};
const { t } = useTranslation();
// Define column keys for selection
const COLUMN_KEYS = {
TIME: 'time',
CHANNEL: 'channel',
USERNAME: 'username',
TOKEN: 'token',
GROUP: 'group',
TYPE: 'type',
MODEL: 'model',
USE_TIME: 'use_time',
PROMPT: 'prompt',
COMPLETION: 'completion',
COST: 'cost',
RETRY: 'retry',
IP: 'ip',
DETAILS: 'details',
};
// Basic state
const [logs, setLogs] = useState([]);
const [expandData, setExpandData] = useState({});
const [showStat, setShowStat] = useState(false);
const [loading, setLoading] = useState(false);
const [loadingStat, setLoadingStat] = useState(false);
const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [logType, setLogType] = useState(0);
// User and admin
const isAdminUser = isAdmin();
// Statistics state
const [stat, setStat] = useState({
quota: 0,
token: 0,
});
// Form state
const [formApi, setFormApi] = useState(null);
let now = new Date();
const formInitValues = {
username: '',
token_name: '',
model_name: '',
channel: '',
group: '',
dateRange: [
timestamp2string(getTodayStartTimestamp()),
timestamp2string(now.getTime() / 1000 + 3600),
],
logType: '0',
};
// Column visibility state
const [visibleColumns, setVisibleColumns] = useState({});
const [showColumnSelector, setShowColumnSelector] = useState(false);
// Compact mode
const [compactMode, setCompactMode] = useTableCompactMode('logs');
// User info modal state
const [showUserInfo, setShowUserInfoModal] = useState(false);
const [userInfoData, setUserInfoData] = useState(null);
// Load saved column preferences from localStorage
useEffect(() => {
const savedColumns = localStorage.getItem('logs-table-columns');
if (savedColumns) {
try {
const parsed = JSON.parse(savedColumns);
const defaults = getDefaultColumnVisibility();
const merged = { ...defaults, ...parsed };
setVisibleColumns(merged);
} catch (e) {
console.error('Failed to parse saved column preferences', e);
initDefaultColumns();
}
} else {
initDefaultColumns();
}
}, []);
// Get default column visibility based on user role
const getDefaultColumnVisibility = () => {
return {
[COLUMN_KEYS.TIME]: true,
[COLUMN_KEYS.CHANNEL]: isAdminUser,
[COLUMN_KEYS.USERNAME]: isAdminUser,
[COLUMN_KEYS.TOKEN]: true,
[COLUMN_KEYS.GROUP]: true,
[COLUMN_KEYS.TYPE]: true,
[COLUMN_KEYS.MODEL]: true,
[COLUMN_KEYS.USE_TIME]: true,
[COLUMN_KEYS.PROMPT]: true,
[COLUMN_KEYS.COMPLETION]: true,
[COLUMN_KEYS.COST]: true,
[COLUMN_KEYS.RETRY]: isAdminUser,
[COLUMN_KEYS.IP]: true,
[COLUMN_KEYS.DETAILS]: true,
};
};
// Initialize default column visibility
const initDefaultColumns = () => {
const defaults = getDefaultColumnVisibility();
setVisibleColumns(defaults);
localStorage.setItem('logs-table-columns', JSON.stringify(defaults));
};
// Handle column visibility change
const handleColumnVisibilityChange = (columnKey, checked) => {
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
setVisibleColumns(updatedColumns);
};
// Handle "Select All" checkbox
const handleSelectAll = (checked) => {
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
const updatedColumns = {};
allKeys.forEach((key) => {
if (
(key === COLUMN_KEYS.CHANNEL ||
key === COLUMN_KEYS.USERNAME ||
key === COLUMN_KEYS.RETRY) &&
!isAdminUser
) {
updatedColumns[key] = false;
} else {
updatedColumns[key] = checked;
}
});
setVisibleColumns(updatedColumns);
};
// Update table when column visibility changes
useEffect(() => {
if (Object.keys(visibleColumns).length > 0) {
localStorage.setItem(
'logs-table-columns',
JSON.stringify(visibleColumns),
);
}
}, [visibleColumns]);
// ่Žทๅ–่กจๅ•ๅ€ผ็š„่พ…ๅŠฉๅ‡ฝๆ•ฐ๏ผŒ็กฎไฟๆ‰€ๆœ‰ๅ€ผ้ƒฝๆ˜ฏๅญ—็ฌฆไธฒ
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
let start_timestamp = timestamp2string(getTodayStartTimestamp());
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
if (
formValues.dateRange &&
Array.isArray(formValues.dateRange) &&
formValues.dateRange.length === 2
) {
start_timestamp = formValues.dateRange[0];
end_timestamp = formValues.dateRange[1];
}
return {
username: formValues.username || '',
token_name: formValues.token_name || '',
model_name: formValues.model_name || '',
start_timestamp,
end_timestamp,
channel: formValues.channel || '',
group: formValues.group || '',
logType: formValues.logType ? parseInt(formValues.logType) : 0,
};
};
// Statistics functions
const getLogSelfStat = async () => {
const {
token_name,
model_name,
start_timestamp,
end_timestamp,
group,
logType: formLogType,
} = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/self/stat?type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
url = encodeURI(url);
let res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setStat(data);
} else {
showError(message);
}
};
const getLogStat = async () => {
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
group,
logType: formLogType,
} = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/stat?type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
url = encodeURI(url);
let res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setStat(data);
} else {
showError(message);
}
};
const handleEyeClick = async () => {
if (loadingStat) {
return;
}
setLoadingStat(true);
if (isAdminUser) {
await getLogStat();
} else {
await getLogSelfStat();
}
setShowStat(true);
setLoadingStat(false);
};
// User info function
const showUserInfoFunc = async (userId) => {
if (!isAdminUser) {
return;
}
const res = await API.get(`/api/user/${userId}`);
const { success, message, data } = res.data;
if (success) {
setUserInfoData(data);
setShowUserInfoModal(true);
} else {
showError(message);
}
};
// Format logs data
const setLogsFormat = (logs) => {
let expandDatesLocal = {};
for (let i = 0; i < logs.length; i++) {
logs[i].timestamp2string = timestamp2string(logs[i].created_at);
logs[i].key = logs[i].id;
let other = getLogOther(logs[i].other);
let expandDataLocal = [];
if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
expandDataLocal.push({
key: t('ๆธ ้“ไฟกๆฏ'),
value: `${logs[i].channel} - ${logs[i].channel_name || '[ๆœช็Ÿฅ]'}`,
});
}
if (other?.ws || other?.audio) {
expandDataLocal.push({
key: t('่ฏญ้Ÿณ่พ“ๅ…ฅ'),
value: other.audio_input,
});
expandDataLocal.push({
key: t('่ฏญ้Ÿณ่พ“ๅ‡บ'),
value: other.audio_output,
});
expandDataLocal.push({
key: t('ๆ–‡ๅญ—่พ“ๅ…ฅ'),
value: other.text_input,
});
expandDataLocal.push({
key: t('ๆ–‡ๅญ—่พ“ๅ‡บ'),
value: other.text_output,
});
}
if (other?.cache_tokens > 0) {
expandDataLocal.push({
key: t('็ผ“ๅญ˜ Tokens'),
value: other.cache_tokens,
});
}
if (other?.cache_creation_tokens > 0) {
expandDataLocal.push({
key: t('็ผ“ๅญ˜ๅˆ›ๅปบ Tokens'),
value: other.cache_creation_tokens,
});
}
if (logs[i].type === 2) {
expandDataLocal.push({
key: t('ๆ—ฅๅฟ—่ฏฆๆƒ…'),
value: other?.claude
? renderClaudeLogContent(
other?.model_ratio,
other.completion_ratio,
other.model_price,
other.group_ratio,
other?.user_group_ratio,
other.cache_ratio || 1.0,
other.cache_creation_ratio || 1.0,
)
: renderLogContent(
other?.model_ratio,
other.completion_ratio,
other.model_price,
other.group_ratio,
other?.user_group_ratio,
false,
1.0,
other.web_search || false,
other.web_search_call_count || 0,
other.file_search || false,
other.file_search_call_count || 0,
),
});
}
if (logs[i].type === 2) {
let modelMapped =
other?.is_model_mapped &&
other?.upstream_model_name &&
other?.upstream_model_name !== '';
if (modelMapped) {
expandDataLocal.push({
key: t('่ฏทๆฑ‚ๅนถ่ฎก่ดนๆจกๅž‹'),
value: logs[i].model_name,
});
expandDataLocal.push({
key: t('ๅฎž้™…ๆจกๅž‹'),
value: other.upstream_model_name,
});
}
let content = '';
if (other?.ws || other?.audio) {
content = renderAudioModelPrice(
other?.text_input,
other?.text_output,
other?.model_ratio,
other?.model_price,
other?.completion_ratio,
other?.audio_input,
other?.audio_output,
other?.audio_ratio,
other?.audio_completion_ratio,
other?.group_ratio,
other?.user_group_ratio,
other?.cache_tokens || 0,
other?.cache_ratio || 1.0,
);
} else if (other?.claude) {
content = renderClaudeModelPrice(
logs[i].prompt_tokens,
logs[i].completion_tokens,
other.model_ratio,
other.model_price,
other.completion_ratio,
other.group_ratio,
other?.user_group_ratio,
other.cache_tokens || 0,
other.cache_ratio || 1.0,
other.cache_creation_tokens || 0,
other.cache_creation_ratio || 1.0,
);
} else {
content = renderModelPrice(
logs[i].prompt_tokens,
logs[i].completion_tokens,
other?.model_ratio,
other?.model_price,
other?.completion_ratio,
other?.group_ratio,
other?.user_group_ratio,
other?.cache_tokens || 0,
other?.cache_ratio || 1.0,
other?.image || false,
other?.image_ratio || 0,
other?.image_output || 0,
other?.web_search || false,
other?.web_search_call_count || 0,
other?.web_search_price || 0,
other?.file_search || false,
other?.file_search_call_count || 0,
other?.file_search_price || 0,
other?.audio_input_seperate_price || false,
other?.audio_input_token_count || 0,
other?.audio_input_price || 0,
);
}
expandDataLocal.push({
key: t('่ฎก่ดน่ฟ‡็จ‹'),
value: content,
});
if (other?.reasoning_effort) {
expandDataLocal.push({
key: t('Reasoning Effort'),
value: other.reasoning_effort,
});
}
}
expandDatesLocal[logs[i].key] = expandDataLocal;
}
setExpandData(expandDatesLocal);
setLogs(logs);
};
// Load logs function
const loadLogs = async (startIdx, pageSize, customLogType = null) => {
setLoading(true);
let url = '';
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
group,
logType: formLogType,
} = getFormValues();
const currentLogType =
customLogType !== null
? customLogType
: formLogType !== undefined
? formLogType
: logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
} else {
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
}
url = encodeURI(url);
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
const newPageData = data.items;
setActivePage(data.page);
setPageSize(data.page_size);
setLogCount(data.total);
setLogsFormat(newPageData);
} else {
showError(message);
}
setLoading(false);
};
// Page handlers
const handlePageChange = (page) => {
setActivePage(page);
loadLogs(page, pageSize).then((r) => { });
};
const handlePageSizeChange = async (size) => {
localStorage.setItem('page-size', size + '');
setPageSize(size);
setActivePage(1);
loadLogs(activePage, size)
.then()
.catch((reason) => {
showError(reason);
});
};
// Refresh function
const refresh = async () => {
setActivePage(1);
handleEyeClick();
await loadLogs(1, pageSize);
};
// Copy text function
const copyText = async (e, text) => {
e.stopPropagation();
if (await copy(text)) {
showSuccess('ๅทฒๅคๅˆถ๏ผš' + text);
} else {
Modal.error({ title: t('ๆ— ๆณ•ๅคๅˆถๅˆฐๅ‰ช่ดดๆฟ๏ผŒ่ฏทๆ‰‹ๅŠจๅคๅˆถ'), content: text });
}
};
// Initialize data
useEffect(() => {
const localPageSize =
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize);
loadLogs(activePage, localPageSize)
.then()
.catch((reason) => {
showError(reason);
});
}, []);
// Initialize statistics when formApi is available
useEffect(() => {
if (formApi) {
handleEyeClick();
}
}, [formApi]);
// Check if any record has expandable content
const hasExpandableRows = () => {
return logs.some(
(log) => expandData[log.key] && expandData[log.key].length > 0,
);
};
return {
// Basic state
logs,
expandData,
showStat,
loading,
loadingStat,
activePage,
logCount,
pageSize,
logType,
stat,
isAdminUser,
// Form state
formApi,
setFormApi,
formInitValues,
getFormValues,
// Column visibility
visibleColumns,
showColumnSelector,
setShowColumnSelector,
handleColumnVisibilityChange,
handleSelectAll,
initDefaultColumns,
COLUMN_KEYS,
// Compact mode
compactMode,
setCompactMode,
// User info modal
showUserInfo,
setShowUserInfoModal,
userInfoData,
showUserInfoFunc,
// Functions
loadLogs,
handlePageChange,
handlePageSizeChange,
refresh,
copyText,
handleEyeClick,
setLogsFormat,
hasExpandableRows,
setLogType,
// Translation
t,
};
};
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment