mirror of
https://github.com/chaitin/MonkeyCode.git
synced 2026-02-11 03:04:09 +08:00
@@ -14,6 +14,7 @@
|
||||
"@c-x/ui": "^1.0.9",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@monaco-editor/react": "4.7.0-rc.0",
|
||||
"@mui/icons-material": "^6.4.12",
|
||||
"@mui/lab": "6.0.0-beta.19",
|
||||
"@mui/material": "^6.4.12",
|
||||
|
||||
34
ui/pnpm-lock.yaml
generated
34
ui/pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
||||
'@emotion/styled':
|
||||
specifier: ^11.14.0
|
||||
version: 11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)
|
||||
'@monaco-editor/react':
|
||||
specifier: 4.7.0-rc.0
|
||||
version: 4.7.0-rc.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@mui/icons-material':
|
||||
specifier: ^6.4.12
|
||||
version: 6.4.12(@mui/material@6.4.12(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)
|
||||
@@ -548,6 +551,16 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@monaco-editor/loader@1.5.0':
|
||||
resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==}
|
||||
|
||||
'@monaco-editor/react@4.7.0-rc.0':
|
||||
resolution: {integrity: sha512-YfjXkDK0bcwS0zo8PXptvQdCQfOPPtzGsAzmIv7PnoUGFdIohsR+NVDyjbajMddF+3cWUm/3q9NzP/DUke9a+w==}
|
||||
peerDependencies:
|
||||
monaco-editor: '>= 0.25.0 < 1'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@mui/base@5.0.0-beta.66':
|
||||
resolution: {integrity: sha512-1SzcNbtIms0o/Dx+599B6QbvR5qUMBUjwc2Gs47h1HsF7RcEFXxqaq7zrWkIWbvGctIIPx0j330oGx/SkF+UmA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -1836,6 +1849,9 @@ packages:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
monaco-editor@0.52.2:
|
||||
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -2203,6 +2219,9 @@ packages:
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
state-local@1.0.7:
|
||||
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2843,6 +2862,17 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@monaco-editor/loader@1.5.0':
|
||||
dependencies:
|
||||
state-local: 1.0.7
|
||||
|
||||
'@monaco-editor/react@4.7.0-rc.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@monaco-editor/loader': 1.5.0
|
||||
monaco-editor: 0.52.2
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@mui/base@5.0.0-beta.66(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
@@ -4363,6 +4393,8 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
monaco-editor@0.52.2: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
@@ -4783,6 +4815,8 @@ snapshots:
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
state-local@1.0.7: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
|
||||
@@ -131,6 +131,7 @@ export interface DomainCompletionInfo {
|
||||
content?: string;
|
||||
created_at?: number;
|
||||
id?: string;
|
||||
prompt?: string;
|
||||
}
|
||||
|
||||
export interface DomainCompletionRecord {
|
||||
@@ -330,11 +331,13 @@ export interface DomainProviderModel {
|
||||
|
||||
export interface DomainRegisterReq {
|
||||
/** 邀请码 */
|
||||
code?: string;
|
||||
code: string;
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
email: string;
|
||||
/** 密码 */
|
||||
password?: string;
|
||||
password: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface DomainSetting {
|
||||
@@ -585,6 +588,12 @@ export interface GetChatInfoParams {
|
||||
}
|
||||
|
||||
export interface GetListChatRecordParams {
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
/** 是否接受筛选 */
|
||||
is_accept?: boolean;
|
||||
/** 语言 */
|
||||
language?: string;
|
||||
/** 下一页标识 */
|
||||
next_token?: string;
|
||||
/** 分页 */
|
||||
@@ -599,6 +608,12 @@ export interface GetCompletionInfoParams {
|
||||
}
|
||||
|
||||
export interface GetListCompletionRecordParams {
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
/** 是否接受筛选 */
|
||||
is_accept?: boolean;
|
||||
/** 语言 */
|
||||
language?: string;
|
||||
/** 下一页标识 */
|
||||
next_token?: string;
|
||||
/** 分页 */
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,16 +1,10 @@
|
||||
import Avatar from '@/components/avatar';
|
||||
import Card from '@/components/card';
|
||||
import { getChatInfo } from '@/api/Billing';
|
||||
import MarkDown from '@/components/markDown';
|
||||
import { addCommasToNumber, processText } from '@/utils';
|
||||
import { Ellipsis, Icon, Modal } from '@c-x/ui';
|
||||
import { Box, Stack, Tooltip, useTheme } from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DomainCompletionRecord } from '@/api/types';
|
||||
import { getCompletionInfo } from '@/api/Billing';
|
||||
import { Modal } from '@c-x/ui';
|
||||
import MonacoEditor from '@monaco-editor/react';
|
||||
|
||||
type ConversationItem = any;
|
||||
type ToolInfo = any;
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { DomainCompletionRecord } from '@/api/types';
|
||||
|
||||
const ChatDetailModal = ({
|
||||
data,
|
||||
@@ -21,51 +15,77 @@ const ChatDetailModal = ({
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [ChatDetailModal, setChatDetailModal] =
|
||||
useState<ConversationItem | null>(null);
|
||||
const [content, setContent] = useState<string>('');
|
||||
const [showToolInfo, setShowToolInfo] = useState<{ [key: string]: ToolInfo }>(
|
||||
{}
|
||||
);
|
||||
const [editorValue, setEditorValue] = useState<string>('');
|
||||
const editorRef = useRef<any>(null);
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [highlightInfo, setHighlightInfo] = useState<any>(null);
|
||||
|
||||
const getChatDetailModal = () => {
|
||||
if (!data) return;
|
||||
getChatInfo({ id: data.id! }).then((res) => {
|
||||
setContent(
|
||||
data.program_language
|
||||
? `\`\`\`${data.program_language}\n${res.content || ''}\n\`\`\``
|
||||
: res.content || ''
|
||||
);
|
||||
getCompletionInfo({ id: data.id! }).then((res) => {
|
||||
const rawPrompt = res.prompt || '';
|
||||
const content = res.content || '';
|
||||
// 找到三个特殊标记的位置
|
||||
const prefixTag = '<|fim_prefix|>';
|
||||
const suffixTag = '<|fim_suffix|>';
|
||||
const middleTag = '<|fim_middle|>';
|
||||
const prefixIdx = rawPrompt.indexOf(prefixTag);
|
||||
const suffixIdx = rawPrompt.indexOf(suffixTag);
|
||||
const middleIdx = rawPrompt.indexOf(middleTag);
|
||||
// 去掉特殊标记
|
||||
const prompt = rawPrompt
|
||||
.replace(prefixTag, '')
|
||||
.replace(suffixTag, '')
|
||||
.replace(middleTag, '');
|
||||
// 重新定位插入点(因为去掉了前面的 tag,位置会变)
|
||||
// 计算插入点:suffixTag 在原始 prompt 的位置,去掉 prefixTag 后的 offset
|
||||
let insertIdx = suffixIdx;
|
||||
if (prefixIdx !== -1 && prefixIdx < suffixIdx) {
|
||||
insertIdx -= prefixTag.length;
|
||||
}
|
||||
if (middleIdx !== -1 && middleIdx < suffixIdx) {
|
||||
insertIdx -= middleTag.length;
|
||||
}
|
||||
// 插入 content
|
||||
const newValue =
|
||||
prompt.slice(0, insertIdx) + content + prompt.slice(insertIdx);
|
||||
setEditorValue(newValue);
|
||||
// 计算高亮范围(行列)
|
||||
const before = newValue.slice(0, insertIdx);
|
||||
const contentLines = content.split('\n');
|
||||
const beforeLines = before.split('\n');
|
||||
const startLine = beforeLines.length;
|
||||
const startColumn = beforeLines[beforeLines.length - 1].length + 1;
|
||||
const endLine = startLine + contentLines.length - 1;
|
||||
const endColumn =
|
||||
contentLines.length === 1
|
||||
? startColumn + content.length
|
||||
: contentLines[contentLines.length - 1].length + 1;
|
||||
setHighlightInfo({ startLine, startColumn, endLine, endColumn });
|
||||
});
|
||||
// getConversationChatDetailModal({ id }).then((res) => {
|
||||
// const newAnswer = res.answer
|
||||
// const toolWrapsIds = newAnswer.match(/<tools id="([^"]+)">/g)?.map(match => {
|
||||
// const idMatch = match.match(/<tools id="([^"]+)">/);
|
||||
// return idMatch ? idMatch[1] : null;
|
||||
// }).filter(Boolean) || [];
|
||||
// const toolIds = newAnswer.match(/<tool id="([^"]+)">/g)?.map(match => {
|
||||
// const idMatch = match.match(/<tool id="([^"]+)">/);
|
||||
// return idMatch ? idMatch[1] : null;
|
||||
// }).filter(Boolean) || [];
|
||||
// const obj: { [key: string]: ToolInfo } = {}
|
||||
// toolWrapsIds.forEach(id => {
|
||||
// obj[id!] = {
|
||||
// done: true,
|
||||
// }
|
||||
// })
|
||||
// toolIds.forEach(id => {
|
||||
// obj[id!] = {
|
||||
// args: false,
|
||||
// result: false,
|
||||
// done: true,
|
||||
// }
|
||||
// })
|
||||
// setShowToolInfo(obj)
|
||||
// setChatDetailModal({ ...res, answer: processText(res.answer) })
|
||||
// })
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editorReady && highlightInfo && editorRef.current) {
|
||||
editorRef.current.deltaDecorations(
|
||||
[],
|
||||
[
|
||||
{
|
||||
range: {
|
||||
startLineNumber: highlightInfo.startLine,
|
||||
startColumn: highlightInfo.startColumn,
|
||||
endLineNumber: highlightInfo.endLine,
|
||||
endColumn: highlightInfo.endColumn,
|
||||
},
|
||||
options: {
|
||||
inlineClassName: 'completion-highlight',
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}, [editorReady, highlightInfo, editorValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) getChatDetailModal();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -79,164 +99,71 @@ const ChatDetailModal = ({
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
>
|
||||
{ChatDetailModal ? (
|
||||
<Box sx={{ fontSize: 14 }}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={3}
|
||||
sx={{
|
||||
<Card sx={{ p: 0 }}>
|
||||
<div style={{ height: 420 }}>
|
||||
<MonacoEditor
|
||||
height='100%'
|
||||
language={data?.program_language || 'plaintext'}
|
||||
value={editorValue}
|
||||
theme='vs-dark'
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
color: 'text.auxiliary',
|
||||
}}
|
||||
>
|
||||
{ChatDetailModal.created_at && (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={1}>
|
||||
<Icon type='icon-a-shijian2' />
|
||||
{dayjs(ChatDetailModal.created_at).format(
|
||||
'YYYY-MM-DD HH:mm:ss'
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{ChatDetailModal.remote_ip && (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={1}>
|
||||
<Icon type='icon-IPdizhijiancha' />
|
||||
{ChatDetailModal.remote_ip}
|
||||
</Stack>
|
||||
)}
|
||||
{ChatDetailModal.model && (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={1}>
|
||||
<Icon type='icon-moxing' />
|
||||
使用模型
|
||||
<Box>{ChatDetailModal.model}</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{data?.input_tokens && data?.output_tokens && (
|
||||
<Tooltip
|
||||
title={
|
||||
<Stack gap={1} sx={{ minWidth: 100, py: 1 }}>
|
||||
<Box>
|
||||
输入 Token 使用: {addCommasToNumber(data?.input_tokens)}
|
||||
</Box>
|
||||
<Box>
|
||||
输出 Token 使用: {addCommasToNumber(data?.output_tokens)}
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Icon type='icon-moxing' />
|
||||
Token 统计
|
||||
<Box>
|
||||
{addCommasToNumber(
|
||||
data?.input_tokens + data?.output_tokens
|
||||
)}
|
||||
</Box>
|
||||
<Icon type='icon-a-wenhao8' />
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
{ChatDetailModal.references?.length > 0 && (
|
||||
<>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
mt: 2,
|
||||
mb: 1,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: '4px',
|
||||
height: '12px',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
内容来源
|
||||
</Stack>
|
||||
<Card sx={{ p: 2, bgcolor: 'background.paper2' }}>
|
||||
{ChatDetailModal.references.map((item: any, index: number) => (
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
key={index}
|
||||
>
|
||||
<Avatar
|
||||
src={item.favicon}
|
||||
sx={{ width: 18, height: 18 }}
|
||||
errorIcon={
|
||||
<Icon
|
||||
type='icon-ditu_diqiu'
|
||||
sx={{ fontSize: 18, color: 'text.auxiliary' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Ellipsis>
|
||||
<Box
|
||||
component={'a'}
|
||||
href={item.url}
|
||||
target='_blank'
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Box>
|
||||
</Ellipsis>
|
||||
</Stack>
|
||||
))}
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
mt: 2,
|
||||
mb: 1,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: '4px',
|
||||
height: '12px',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
lineNumbers: 'on',
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
handleMouseWheel: false,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
useShadows: false,
|
||||
},
|
||||
overviewRulerLanes: 0,
|
||||
guides: {
|
||||
indentation: true,
|
||||
highlightActiveIndentation: true,
|
||||
highlightActiveBracketPair: false,
|
||||
},
|
||||
renderLineHighlight: 'none',
|
||||
cursorStyle: 'line',
|
||||
cursorBlinking: 'solid',
|
||||
cursorWidth: 0,
|
||||
contextmenu: false,
|
||||
selectionHighlight: false,
|
||||
selectOnLineNumbers: false,
|
||||
occurrencesHighlight: 'off',
|
||||
links: false,
|
||||
hover: { enabled: false },
|
||||
codeLens: false,
|
||||
dragAndDrop: false,
|
||||
mouseWheelZoom: false,
|
||||
accessibilitySupport: 'off',
|
||||
bracketPairColorization: { enabled: false },
|
||||
matchBrackets: 'never',
|
||||
}}
|
||||
>
|
||||
回答
|
||||
</Stack>
|
||||
</Box>
|
||||
) : (
|
||||
<Box></Box>
|
||||
)}
|
||||
<Card
|
||||
sx={{
|
||||
'.markdown-body': {
|
||||
background: 'transparent',
|
||||
},
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<MarkDown
|
||||
showToolInfo={showToolInfo}
|
||||
setShowToolInfo={setShowToolInfo}
|
||||
content={content}
|
||||
/>
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor;
|
||||
setEditorReady(true);
|
||||
// 隐藏光标
|
||||
const editorDom = editor.getDomNode();
|
||||
if (editorDom) {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `.monaco-editor .cursor { display: none !important; }`;
|
||||
editorDom.appendChild(style);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<style>{`
|
||||
.completion-highlight {
|
||||
background: #264f78 !important;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
`}</style>
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
67
ui/src/pages/completion/constant.ts
Normal file
67
ui/src/pages/completion/constant.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export const LANG_OPTIONS = [
|
||||
'JavaScript',
|
||||
'JavaScriptReact',
|
||||
'TypeScript',
|
||||
'TypeScriptReact',
|
||||
'Python',
|
||||
'Java',
|
||||
'C',
|
||||
'C++',
|
||||
'C#',
|
||||
'Go',
|
||||
'PHP',
|
||||
'Ruby',
|
||||
'Swift',
|
||||
'Kotlin',
|
||||
'Rust',
|
||||
'Dart',
|
||||
'Objective-C',
|
||||
'Scala',
|
||||
'Perl',
|
||||
'R',
|
||||
'Shell Script',
|
||||
'PowerShell',
|
||||
'HTML',
|
||||
'CSS',
|
||||
'SCSS',
|
||||
'Less',
|
||||
'JSON',
|
||||
'YAML',
|
||||
'XML',
|
||||
'Markdown',
|
||||
'SQL',
|
||||
'GraphQL',
|
||||
'Dockerfile',
|
||||
'Makefile',
|
||||
'Lua',
|
||||
'Haskell',
|
||||
'Elixir',
|
||||
'Erlang',
|
||||
'F#',
|
||||
'Groovy',
|
||||
'Visual Basic',
|
||||
'Assembly',
|
||||
'Matlab',
|
||||
'Fortran',
|
||||
'COBOL',
|
||||
'Prolog',
|
||||
'Scheme',
|
||||
'Lisp',
|
||||
'Julia',
|
||||
'SASS',
|
||||
'TOML',
|
||||
'INI',
|
||||
'LaTeX',
|
||||
'CMake',
|
||||
'Batch',
|
||||
'CoffeeScript',
|
||||
'Crystal',
|
||||
'OCaml',
|
||||
'Nim',
|
||||
'ReScript',
|
||||
'Solidity',
|
||||
'Vue',
|
||||
'Svelte',
|
||||
'JSX',
|
||||
'TSX',
|
||||
];
|
||||
@@ -1,15 +1,54 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { DomainCompletionRecord } from '@/api/types';
|
||||
import { getListCompletionRecord } from '@/api/Billing';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Table } from '@c-x/ui';
|
||||
import Card from '@/components/card';
|
||||
import { Box, Button, ButtonBase, Chip, Stack, alpha } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Chip,
|
||||
Stack,
|
||||
alpha,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import { getListUser } from '@/api/User';
|
||||
import dayjs from 'dayjs';
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
import CompletionDetailModal from './completionDetailModal';
|
||||
import StyledLabel from '@/components/label';
|
||||
import { LANG_OPTIONS } from './constant';
|
||||
import { ArrowDropDown as ArrowDropDownIcon } from '@mui/icons-material';
|
||||
|
||||
// 防抖 hook
|
||||
function useDebounce(fn: (...args: any[]) => void, delay: number) {
|
||||
const timer = useRef<number | null>(null);
|
||||
const fnRef = useRef(fn);
|
||||
fnRef.current = fn;
|
||||
return useCallback(
|
||||
(...args: any[]) => {
|
||||
if (timer.current) clearTimeout(timer.current);
|
||||
timer.current = setTimeout(() => {
|
||||
fnRef.current(...args);
|
||||
}, delay);
|
||||
},
|
||||
[delay]
|
||||
);
|
||||
}
|
||||
|
||||
const Completion = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -20,26 +59,60 @@ const Completion = () => {
|
||||
const [completionDetailModal, setCompletionDetailModal] = useState<
|
||||
DomainCompletionRecord | undefined
|
||||
>();
|
||||
const fetchData = async () => {
|
||||
|
||||
// 新增筛选项 state
|
||||
const [filterUser, setFilterUser] = useState('');
|
||||
const [filterLang, setFilterLang] = useState('');
|
||||
const [filterAccept, setFilterAccept] = useState<
|
||||
'accepted' | 'unaccepted' | ''
|
||||
>('');
|
||||
|
||||
const { data: userOptions = { users: [] } } = useRequest(() =>
|
||||
getListUser({
|
||||
page: 1,
|
||||
size: 99999,
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1); // 筛选变化时重置页码
|
||||
fetchData({
|
||||
page: 1,
|
||||
language: filterLang,
|
||||
author: filterUser,
|
||||
is_accept: filterAccept,
|
||||
});
|
||||
}, [filterUser, filterLang, filterAccept]);
|
||||
|
||||
const fetchData = async (params: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
language?: string;
|
||||
author?: string;
|
||||
is_accept?: 'accepted' | 'unaccepted' | '';
|
||||
}) => {
|
||||
setLoading(true);
|
||||
const res = await getListCompletionRecord({
|
||||
page: page,
|
||||
size: size,
|
||||
page: params.page || page,
|
||||
size: params.size || size,
|
||||
language: params.language || filterLang,
|
||||
author: params.author || filterUser,
|
||||
is_accept:
|
||||
params.is_accept === 'accepted'
|
||||
? true
|
||||
: params.is_accept === 'unaccepted'
|
||||
? false
|
||||
: undefined,
|
||||
});
|
||||
setLoading(false);
|
||||
setTotal(res?.total_count || 0);
|
||||
setDataSource(res.records || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, size]);
|
||||
const columns: ColumnsType<DomainCompletionRecord> = [
|
||||
{
|
||||
dataIndex: 'user',
|
||||
title: '成员',
|
||||
|
||||
render(value: DomainCompletionRecord['user']) {
|
||||
return value?.username;
|
||||
},
|
||||
@@ -60,7 +133,6 @@ const Completion = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
dataIndex: 'is_accept',
|
||||
title: '是否采纳',
|
||||
@@ -72,7 +144,6 @@ const Completion = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
dataIndex: 'program_language',
|
||||
title: '编程语言',
|
||||
@@ -88,7 +159,6 @@ const Completion = () => {
|
||||
{
|
||||
dataIndex: 'output_tokens',
|
||||
title: '输出 Token',
|
||||
|
||||
render(value: number) {
|
||||
return addCommasToNumber(value);
|
||||
},
|
||||
@@ -102,11 +172,72 @@ const Completion = () => {
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const debounceSetFilterLang = useDebounce(
|
||||
(val: string) => setFilterLang(val),
|
||||
500
|
||||
);
|
||||
|
||||
return (
|
||||
<Card sx={{ flex: 1, height: '100%' }}>
|
||||
{/* 筛选项 */}
|
||||
<Stack direction='row' spacing={2} sx={{ mb: 2 }}>
|
||||
{/* 成员筛选 Autocomplete */}
|
||||
<Autocomplete
|
||||
size='small'
|
||||
sx={{ minWidth: 220 }}
|
||||
options={userOptions.users || []}
|
||||
getOptionLabel={(option) => option.username || ''}
|
||||
value={
|
||||
userOptions.users?.find((item) => item.username === filterUser) ||
|
||||
null
|
||||
}
|
||||
onChange={(_, newValue) =>
|
||||
setFilterUser(newValue ? newValue.username! : '')
|
||||
}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
option.username === value.username
|
||||
}
|
||||
renderInput={(params) => <TextField {...params} label='成员' />}
|
||||
clearOnEscape
|
||||
/>
|
||||
{/* 语言筛选 Autocomplete */}
|
||||
<Autocomplete
|
||||
size='small'
|
||||
sx={{ minWidth: 220 }}
|
||||
options={LANG_OPTIONS}
|
||||
getOptionLabel={(option) => option || ''}
|
||||
value={filterLang || ''}
|
||||
freeSolo
|
||||
onChange={(_, newValue) => {
|
||||
setFilterLang(newValue ? String(newValue) : '');
|
||||
}}
|
||||
onInputChange={(_, newInputValue) =>
|
||||
debounceSetFilterLang(newInputValue)
|
||||
}
|
||||
popupIcon={<ArrowDropDownIcon />}
|
||||
renderInput={(params) => <TextField {...params} label='语言' />}
|
||||
clearOnEscape
|
||||
/>
|
||||
{/* 是否采纳筛选 Select 保持不变 */}
|
||||
<FormControl size='small' sx={{ minWidth: 180 }}>
|
||||
<InputLabel>是否采纳</InputLabel>
|
||||
<Select
|
||||
label='是否采纳'
|
||||
value={filterAccept}
|
||||
onChange={(e) =>
|
||||
setFilterAccept(e.target.value as 'accepted' | 'unaccepted')
|
||||
}
|
||||
>
|
||||
<MenuItem value=''>全部</MenuItem>
|
||||
<MenuItem value='accepted'>已采纳</MenuItem>
|
||||
<MenuItem value='unaccepted'>未采纳</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<Table
|
||||
size='large'
|
||||
height='100%'
|
||||
height='calc(100% - 56px)'
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={dataSource || []}
|
||||
@@ -125,6 +256,10 @@ const Completion = () => {
|
||||
onChange: (page, size) => {
|
||||
setPage(page);
|
||||
setSize(size);
|
||||
fetchData({
|
||||
page,
|
||||
size,
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -50,7 +50,7 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||
padding: theme.spacing(4),
|
||||
background: 'rgba(255, 255, 255, 0.85)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
width: 600,
|
||||
width: 500,
|
||||
borderRadius: theme.spacing(2),
|
||||
boxShadow:
|
||||
'0px 0px 4px 0px rgba(54,59,76,0.1), 0px 20px 40px 0px rgba(54,59,76,0.1)',
|
||||
@@ -67,6 +67,27 @@ const StepCard = styled(Box)(({ theme }) => ({
|
||||
borderRadius: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const IconWrapper = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: theme.palette.text.primary,
|
||||
marginRight: theme.spacing(2),
|
||||
fontSize: 16,
|
||||
}));
|
||||
|
||||
const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||
'.MuiInputBase-root': {
|
||||
backgroundColor: '#fff',
|
||||
paddingLeft: '20px',
|
||||
},
|
||||
'.MuiInputBase-input': {
|
||||
paddingTop: '16px',
|
||||
paddingBottom: '16px',
|
||||
fontSize: 14,
|
||||
},
|
||||
}));
|
||||
|
||||
const Invite = () => {
|
||||
const { id, step } = useParams();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
@@ -79,6 +100,7 @@ const Invite = () => {
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
username: '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,7 +114,7 @@ const Invite = () => {
|
||||
};
|
||||
|
||||
const onRegister = handleSubmit((data) => {
|
||||
register({ ...data, code: id }).then(() => {
|
||||
register({ ...data, code: id! }).then(() => {
|
||||
onNext();
|
||||
});
|
||||
});
|
||||
@@ -127,6 +149,35 @@ const Invite = () => {
|
||||
return !loginSetting.enable_dingtalk_oauth ? (
|
||||
<Box component='form' onSubmit={onRegister}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={12}>
|
||||
<Controller
|
||||
name='username'
|
||||
control={control}
|
||||
rules={{
|
||||
required: '请输入用户名',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<StyledTextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='请输入您的用户名'
|
||||
variant='outlined'
|
||||
error={!!errors.username}
|
||||
helperText={errors.username?.message}
|
||||
disabled={loading}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<IconWrapper>
|
||||
<Icon type='icon-zhanghao' />
|
||||
</IconWrapper>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Controller
|
||||
name='email'
|
||||
@@ -139,7 +190,7 @@ const Invite = () => {
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
<StyledTextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='请输入您的邮箱地址'
|
||||
@@ -150,14 +201,9 @@ const Invite = () => {
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<Icon
|
||||
type='icon-youxiang'
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
mr: 1,
|
||||
fontSize: 18,
|
||||
}}
|
||||
/>
|
||||
<IconWrapper>
|
||||
<Icon type='icon-youxiang' />
|
||||
</IconWrapper>
|
||||
),
|
||||
},
|
||||
}}
|
||||
@@ -178,7 +224,7 @@ const Invite = () => {
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
<StyledTextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='请设置您的密码'
|
||||
@@ -190,14 +236,9 @@ const Invite = () => {
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<Icon
|
||||
type='icon-mima'
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
mr: 1,
|
||||
fontSize: 18,
|
||||
}}
|
||||
/>
|
||||
<IconWrapper>
|
||||
<Icon type='icon-mima' />
|
||||
</IconWrapper>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
@@ -349,7 +390,7 @@ const Invite = () => {
|
||||
</LogoContainer>
|
||||
|
||||
<Stepper
|
||||
activeStep={activeStep}
|
||||
activeStep={activeStep - 1}
|
||||
alternativeLabel
|
||||
sx={{
|
||||
mb: 4,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { createTheme } from '@mui/material';
|
||||
import { createTheme, Paper } from '@mui/material';
|
||||
import type { Shadows } from '@mui/material';
|
||||
import { zhCN } from '@mui/material/locale';
|
||||
import { zhCN as CuiZhCN } from '@c-x/ui/dist/local';
|
||||
@@ -141,6 +141,24 @@ const lightTheme = createTheme(
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAutocomplete: {
|
||||
defaultProps: {
|
||||
slotProps: {
|
||||
paper: {
|
||||
elevation: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
borderRadius: '10px',
|
||||
},
|
||||
option: {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'var(--font-gilory), var(--font-HarmonyOS)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiFormLabel: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
|
||||
Reference in New Issue
Block a user