feat: 添加i18n,美化界面

This commit is contained in:
2025-03-20 02:29:01 +08:00
parent 27d9bdf54e
commit c206add7eb
56 changed files with 2743 additions and 928 deletions

27
packages/codemirror/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
tsconfig.app.tsbuildinfo
tsconfig.node.tsbuildinfo

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Apps</title>
<link rel="stylesheet" href="./src/global.css">
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-size: 16px;
}
#ai-root {
width: 100%;
height: 100%;
}
</style>
<!-- <script src="/system/lib/app.js"></script> -->
<script src="https://kevisual.xiongxiao.me/system/lib/app.js"></script>
</head>
<body>
<div id="ai-root"></div>
<!-- <div id="ai-bot-root"></div> -->
</body>
<script src="./src/main.ts" type="module"></script>
</html>

View File

@@ -0,0 +1,70 @@
{
"name": "@kevisual/codemirror",
"version": "0.0.1",
"description": "",
"main": "index.js",
"basename": "/root/codemirror",
"scripts": {
"dev": "vite",
"build": "vite build",
"pub": "envision deploy ./dist -k codemirror -v 0.0.1 -u -o root"
},
"files": [
"src"
],
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me>",
"license": "MIT",
"type": "module",
"dependencies": {
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.8.0",
"@codemirror/history": "^0.19.2",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.4",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@kevisual/center-components": "workspace:*",
"@kevisual/router": "^0.0.9",
"@kevisual/store": "^0.0.2",
"@mui/material": "^6.4.8",
"@types/lodash-es": "^4.17.12",
"@types/nprogress": "^0.2.3",
"@uiw/codemirror-theme-duotone": "^4.23.10",
"@uiw/codemirror-theme-vscode": "^4.23.10",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"codemirror": "^6.0.1",
"dayjs": "^1.11.13",
"highlight.js": "^11.11.1",
"immer": "^10.1.1",
"lodash-es": "^4.17.21",
"lucide-react": "^0.483.0",
"marked": "^15.0.7",
"marked-highlight": "^2.2.1",
"nanoid": "^5.1.5",
"nprogress": "^0.2.0",
"prettier": "^3.5.3",
"pretty-bytes": "^6.1.1",
"react": "19.0.0",
"react-datepicker": "^8.2.1",
"react-dom": "19.0.0",
"react-dropzone": "^14.3.8",
"react-toastify": "^11.0.5",
"zustand": "^5.0.3"
},
"devDependencies": {
"@kevisual/types": "^0.0.6"
},
"exports": {
".": "./src/index.tsx",
"./*": "./src/*"
}
}

View File

@@ -0,0 +1,3 @@
.cm-editor {
height: 100%;
}

View File

@@ -0,0 +1,120 @@
import { EditorView, keymap } from '@codemirror/view';
import { javascript } from '@codemirror/lang-javascript';
import { markdown } from '@codemirror/lang-markdown';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { json } from '@codemirror/lang-json';
import { yaml } from '@codemirror/lang-yaml';
import { history } from '@codemirror/history';
import { vscodeLight } from '@uiw/codemirror-theme-vscode';
import { formatKeymap } from './modules/keymap';
import { Compartment, EditorState, Extension } from '@codemirror/state';
import { defaultKeymap } from '@codemirror/commands';
import { autocompletion, Completion } from '@codemirror/autocomplete';
import { getFileType } from './utils/get-file-type';
type BaseEditorOpts = {
language?: string;
filename: string;
autoComplete?: {
props?: any;
open?: boolean;
overrideList?: Completion[];
};
};
export class BaseEditor {
editor?: EditorView;
onChangeCompartment?: Compartment;
language: string;
filename: string;
el?: HTMLElement;
autoComplete?: {
props?: any;
open?: boolean;
overrideList?: Completion[];
};
constructor(opts?: BaseEditorOpts) {
this.filename = opts?.filename || '';
this.language = opts?.language || 'typescript';
if (this.filename && !opts?.language) {
// 根据文件名自动判断语言
const fileType = getFileType(this.filename);
this.language = fileType || 'typescript';
}
this.autoComplete = opts?.autoComplete || { open: true, overrideList: [] };
this.onChangeCompartment = new Compartment(); // 创建一个用于 onChange 的独立扩展
}
onEditorChange(view: EditorView) {
console.log('onChange', view);
}
getFileType(filename: string) {
return getFileType(filename);
}
createEditor(el: HTMLElement) {
this.el = el;
const onChangeCompartment = this.onChangeCompartment!;
const language = this.language;
const extensions: Extension[] = [
vscodeLight,
formatKeymap,
keymap.of(defaultKeymap), //
// history(),
];
if (this.autoComplete?.open) {
extensions.push(
autocompletion({
activateOnTyping: true, // 输入时触发补全
closeOnBlur: true, // 失去焦点时关闭补全
activateOnTypingDelay: 2000, // 输入时触发补全
override: this.autoComplete?.overrideList || [],
...this.autoComplete?.props,
}),
);
}
if (language === 'typescript') {
extensions.push(javascript({ typescript: true }));
} else if (language === 'javascript') {
extensions.push(javascript());
} else if (language === 'markdown') {
extensions.push(markdown());
} else if (language === 'html') {
extensions.push(html());
} else if (language === 'css') {
extensions.push(css());
} else if (language === 'json') {
extensions.push(json());
} else if (language === 'yaml') {
extensions.push(yaml());
}
this.editor = new EditorView({
parent: el,
extensions: [
...extensions, //
onChangeCompartment.of([]),
],
});
}
resetEditor(el?: HTMLElement) {
this.editor?.destroy?.();
this.createEditor(el || (this.el as HTMLElement));
}
setLanguage(language: string, el?: HTMLElement) {
this.language = language;
this.editor?.destroy?.();
this.createEditor(el || (this.el as HTMLElement));
}
setContent(content: string) {
this.editor?.dispatch({
changes: { from: 0, to: this.editor?.state.doc.length, insert: content },
});
}
getContent() {
return this.editor?.state.doc.toString();
}
destroyEditor() {
this.editor?.destroy();
}
}
export default BaseEditor;

View File

@@ -0,0 +1,94 @@
import { CompletionResult, CompletionSource } from '@codemirror/autocomplete';
import { Transaction } from '@codemirror/state';
export async function fetchAICompletions(context) {
const userInput = context.matchBefore(/\w*/);
if (!userInput) return null;
const docText = context.state.doc.toString(); // 获取完整文档内容
const cursorPos = context.pos; // 获取光标位置
// 添加提示词
const promptText = `请根据以下代码和光标位置提供代码补全建议:
代码:
${docText}
光标位置:${cursorPos}
请提供适当的代码补全。`;
const aiResponse = await fetch('http://192.168.31.220:11434/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: promptText,
// 如果 Ollama 支持,可以传递其他参数,如模型名称
// model: 'your-model-name',
model: 'qwen2.5-coder:14b',
// model: 'qwen2.5:14b',
stream: false,
}),
});
const result = await aiResponse.json();
const data = result;
// 提取字段
const createdAt = new Date(data.created_at); // 转换为日期对象
const isDone = data.done;
const doneReason = data.done_reason;
const modelName = data.model;
const responseText = data.response;
const matchCodeBlock = responseText.match(/```javascript\n([\s\S]*?)\n```/);
let codeBlock = '';
if (matchCodeBlock) {
const codeBlock = matchCodeBlock[1]; // 提取代码块
console.log('补全代码:', codeBlock);
}
const suggestions = [
{
label: codeBlock,
type: 'text',
},
];
return {
from: userInput.from,
options: suggestions,
validFor: /^\w*$/,
};
}
// type AiCompletion = Readonly<CompletionSource>;
export const testAiCompletion = async (context: any): Promise<CompletionResult | null> => {
const userInput = context.matchBefore(/\w*/);
if (!userInput) return null;
const docText = context.state.doc.toString(); // 获取完整文档内容
const cursorPos = context.pos; // 获取光标位置
console.log('testAiCompletion', userInput, docText, cursorPos);
return {
from: userInput.from,
options: [
{
label: '123 测试 skflskdf ',
type: 'text',
apply: (state, completion, from, to) => {
console.log('apply', state, completion, from, to);
const newText = '123 测试 skflskdf ';
state.dispatch({
changes: { from, to, insert: newText },
});
},
info: '这是一个长文本上的反馈是冷酷的父母离开时的父母 这是一个长文本上的反馈是冷酷的父母离开时的父母 这是一个长文本上的反馈是冷酷的父母离开时的父母这是一个长文本上的反馈是冷酷的父母离开时的父母 这是一个长文本上的反馈是冷酷的父母离开时的父母这是一个长文本上的反馈是冷酷的父母离开时的父母 这是一个长文本上的反馈是冷酷的父母离开时的父母',
},
{
label: '456',
type: 'text',
},
],
validFor: /^\w*$/,
};
};

View File

@@ -0,0 +1,92 @@
import { EditorView, keymap } from '@codemirror/view';
import { defaultKeymap, indentSelection, indentWithTab, undo, history } from '@codemirror/commands';
import { indentUnit } from '@codemirror/language';
import prettier from 'prettier';
// import parserBabel from 'prettier/plugins/babel';
import parserEstree from 'prettier/plugins/estree';
// import parserHtml from 'prettier/plugins/html';
import parserTypescript from 'prettier/plugins/typescript';
import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
// 格式化函数
// Function to format the code using Prettier
async function formatCode(view: EditorView) {
const editor = view;
const code = editor.state.doc.toString();
try {
const formattedCode = await prettier.format(code, {
parser: 'typescript',
plugins: [parserEstree, parserTypescript],
});
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: formattedCode.trim(),
},
});
} catch (error) {
console.error('Error formatting code:', error);
}
}
// export const TabFn = (view: EditorView) => {
// const { state } = view;
// const selection = state.selection.main;
// // 判断是否存在补全上下文
// if (state.field(autocompletion).active?.length) {
// // 插入当前选中的补全内容
// const active = state.field(autocompletion).active[0];
// const selected = active?.options[active.selected]?.label;
// if (selected) {
// view.dispatch({
// changes: { from: selection.from, to: selection.to, insert: selected },
// });
// return true;
// }
// }
// return false;
// };
// 自定义 Tab 键行为
function customIndentWithTab(view: EditorView) {
const { state, dispatch } = view;
const { from, to } = state.selection.main;
const _indentUnit = state.facet(indentUnit) as string;
// 计算插入的缩进
const indent = _indentUnit.repeat(1);
dispatch({
changes: { from, to, insert: indent },
selection: { anchor: from + indent.length },
});
return true; // 表示按键事件被处理
}
export const formatKeymap = keymap.of([
{
// bug, 必须小写
key: 'alt-shift-f', // 快捷键绑定
// mac: 'cmd-shift-f',
run: (view) => {
formatCode(view);
return true; // 表示按键事件被处理
},
},
{
key: 'Tab',
run: customIndentWithTab, // 使用自定义的缩进函数
},
{
key: 'Ctrl-z', // Windows 撤销快捷键
// key: 'Alt-z',
mac: 'Cmd-z', // Mac 撤销快捷键
run: (view) => {
console.log('undo', view);
return undo(view); // 表示按键事件被处理
},
},
...defaultKeymap, // 默认快捷键
]);
// console.log('formatKeymap', defaultKeymap);

View File

@@ -0,0 +1,31 @@
export const getExtension = (filename: string) => {
const extension = filename.split('.').pop();
if (!extension) return '';
return extension;
};
export const supportedExtensions = ['ts', 'js', 'md', 'json', 'yaml', 'yml', 'css', 'html'];
export function getFileType(filename: string) {
const extension = getExtension(filename);
if (!supportedExtensions.includes(extension)) return '';
switch (extension) {
case 'ts':
return 'typescript';
case 'js':
return 'javascript';
case 'md':
return 'markdown';
case 'json':
return 'json';
case 'yaml':
return 'yaml';
case 'yml':
return 'yaml';
case 'css':
return 'css';
case 'html':
return 'html';
default:
return '';
}
}

View File

@@ -0,0 +1,3 @@
@import 'tailwindcss';
@import '@kevisual/center-components/theme/wind-theme.css';
@import './style.css';

View File

@@ -0,0 +1,4 @@
import { render } from './pages/Bootstrap';
import './styles.css';
render('#ai-root');

View File

@@ -0,0 +1,40 @@
import { useMemo } from 'react';
import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx';
import { ToastContainer } from 'react-toastify';
import { FileEditor } from './file-editor/FileEditor';
export const InitProvider = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
type AppProvider = {
key: string;
use: boolean;
};
export type AppProps = {
providers?: AppProvider[];
noProvider?: boolean;
/**
* 是否是单个应用
* 默认是单个应用模块。
*/
isSingleApp?: boolean;
};
export const App = ({ providers, noProvider, isSingleApp = true }: AppProps) => {
const children = useMemo(() => {
return (
<InitProvider>
<FileEditor />
</InitProvider>
);
}, []);
if (noProvider) {
return <>{children}</>;
}
return (
<CustomThemeProvider>
{children}
<ToastContainer />
</CustomThemeProvider>
);
};

View File

@@ -0,0 +1,88 @@
import { createRoot } from 'react-dom/client';
import { App, AppProps } from './App';
import React from 'react';
export class ReactRenderer {
component: any;
element: HTMLElement;
ref: React.RefObject<any>;
props: any;
root: any;
constructor(component: any, { props, className }: any) {
this.component = component;
const el = document.createElement('div');
this.element = el;
this.ref = React.createRef();
this.props = {
...props,
ref: this.ref,
};
el.className = className;
this.root = createRoot(this.element);
this.render();
}
updateProps(props: any) {
this.props = {
...this.props,
...props,
};
this.render();
}
render() {
this.root.render(React.createElement(this.component, this.props));
}
destroy() {
this.root.unmount();
}
}
export default ReactRenderer;
export const randomId = () => {
return Math.random().toString(36).substring(2, 15);
};
export const render = (el: HTMLElement | string, props?: AppProps) => {
const root = typeof el === 'string' ? document.querySelector(el) : el;
if (!root) {
console.error('root not found');
return;
}
const hasResourceApp = window.context?.codemirrorApp;
if (hasResourceApp) {
const render = hasResourceApp as ReactRenderer;
render.updateProps({
props: { t: randomId(), ...props },
});
root.innerHTML = '';
root.appendChild(render.element);
} else {
const renderer = new ReactRenderer(App, {
props: { ...props },
className: 'codemirror-root w-full h-full',
});
if (window.context) {
window.context.codemirrorApp = renderer;
} else {
window.context = {
codemirrorApp: renderer,
};
}
root.appendChild(renderer.element);
}
};
export const unmount = (el: HTMLElement | string) => {
const root = typeof el === 'string' ? document.querySelector(el) : el;
if (!root) {
console.error('root not found');
return;
}
const hasResourceApp = window.context?.codemirrorApp;
if (hasResourceApp) {
const render = hasResourceApp as ReactRenderer;
render.destroy();
window.context.codemirrorApp = null;
}
};

View File

@@ -0,0 +1,17 @@
import { useEffect, useRef } from 'react';
import { BaseEditor } from '../../editor/editor';
export const FileEditor = () => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const editor = new BaseEditor();
if (containerRef.current) {
editor.createEditor(containerRef.current);
}
return () => {
editor.destroyEditor();
};
}, []);
return <div ref={containerRef} className='w-full h-full'></div>;
};

View File

@@ -0,0 +1,3 @@
.cm-editor {
height: 100%;
}

View File

@@ -0,0 +1,39 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
"node_modules/@kevisual/types",
],
"paths": {
"@kevisual/codemirror/*": [
"src/*"
]
},
/* Linting */
"strict": true,
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": [
"src",
]
}

View File

@@ -0,0 +1,83 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import tailwindcss from '@tailwindcss/vite';
import basicSsl from '@vitejs/plugin-basic-ssl';
const plugins = [basicSsl()];
// const plugins = [];
plugins.push(tailwindcss());
let proxy = {};
if (true) {
proxy = {
'/api': {
target: 'https://kevisual.silkyai.cn',
changeOrigin: true,
ws: true,
cookieDomainRewrite: 'localhost',
rewrite: (path) => path.replace(/^\/api/, '/api'),
},
'/api/router': {
target: 'wss://kevisual.silkyai.cn',
changeOrigin: true,
ws: true,
rewriteWsOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'),
},
'/user/login': {
target: 'https://kevisual.silkyai.cn',
changeOrigin: true,
cookieDomainRewrite: 'localhost',
rewrite: (path) => path.replace(/^\/user/, '/user'),
},
};
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), ...plugins],
css: {
postcss: {},
},
resolve: {
alias: {
'@kevisual/codemirror': path.resolve(__dirname, './src'),
},
},
define: {
DEV_SERVER: JSON.stringify(process.env.NODE_ENV === 'development'),
BASE_NAME: JSON.stringify('/root/codemirror/'),
},
base: './',
// base: isDev ? '/' : '/root/codemirror/',
build: {
rollupOptions: {
// external: ['react', 'react-dom'],
},
},
server: {
port: 6022,
host: '0.0.0.0',
proxy: {
'/system/lib': {
target: 'https://kevisual.xiongxiao.me',
changeOrigin: true,
},
'/api': {
target: 'http://localhost:4005',
changeOrigin: true,
ws: true,
cookieDomainRewrite: 'localhost',
rewrite: (path) => path.replace(/^\/api/, '/api'),
},
'/api/router': {
target: 'ws://localhost:4005',
changeOrigin: true,
ws: true,
rewriteWsOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'),
},
...proxy,
},
},
});

View File

@@ -17,7 +17,8 @@
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.7",
"react": "19.0.0",
"react-dom": "19.0.0"
"react-dom": "19.0.0",
"react-hook-form": "^7.54.2"
},
"exports": {
".": "./src/index.tsx",

View File

@@ -0,0 +1,47 @@
import { FormControlLabel, TextField } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
export const InputControl = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
return (
<TextField
variant='outlined'
size='small'
name={name}
value={value || ''}
onChange={(e) => onChange?.(e.target.value)}
sx={{
width: '100%',
marginBottom: '16px',
}}
/>
);
};
type FormProps = {
onSubmit?: (data: any) => void;
children?: React.ReactNode;
};
export const FormDemo = (props: FormProps) => {
const { control, handleSubmit } = useForm();
const { onSubmit = () => {}, children } = props;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name='name'
control={control}
defaultValue=''
rules={{ required: 'Name is required' }}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label='Name'
variant='outlined'
margin='normal'
fullWidth //
error={!!error}
helperText={<>{error?.message}</>}
/>
)}
/>
</form>
);
};

View File

@@ -0,0 +1,91 @@
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material';
import { useRef, useState } from 'react';
export const Confirm = ({
open,
onClose,
title,
content,
onConfirm,
confirmText = '确认',
cancelText = '取消',
}: {
open: boolean;
onClose: () => void;
title: string;
content: string;
onConfirm?: () => void;
confirmText?: string;
cancelText?: string;
}) => {
return (
<Dialog open={open} onClose={onClose} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
<DialogTitle id='alert-dialog-title' className='text-secondary min-w-[300px]'>
{title}
</DialogTitle>
<DialogContent>
<DialogContentText id='alert-dialog-description'>{content}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color='primary'>
{cancelText || '取消'}
</Button>
<Button onClick={onConfirm} variant='contained' color='primary' autoFocus>
{confirmText || '确认'}
</Button>
</DialogActions>
</Dialog>
);
};
type Fn = () => void;
export const useConfirm = () => {
const [open, setOpen] = useState(false);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const fns = useRef<{
onConfirm: Fn;
onCancel: Fn;
confirmText: string;
cancelText: string;
}>({
onConfirm: () => {},
onCancel: () => {},
confirmText: '确认',
cancelText: '取消',
});
return {
contextHolder: (
<Confirm
open={open}
confirmText={fns.current.confirmText}
cancelText={fns.current.cancelText}
onClose={() => {
setOpen(false);
fns.current.onCancel();
}}
title={title}
content={content}
onConfirm={fns.current.onConfirm}
/>
),
confirm: (
title: string,
content: string,
opts?: {
onConfirm: () => void;
confirmText?: string;
cancelText?: string;
onCancel?: () => void;
},
) => {
setOpen(true);
setTitle(title);
setContent(content);
fns.current.onConfirm = opts?.onConfirm || (() => {});
fns.current.onCancel = opts?.onCancel || (() => {});
fns.current.confirmText = opts?.confirmText || '确认';
fns.current.cancelText = opts?.cancelText || '取消';
},
};
};

View File

@@ -5,4 +5,67 @@
--color-secondary: #ffa000;
--color-success: #28a745;
--scrollbar-color: #ffc107; /* 滚动条颜色 */
}
}
html,
body {
width: 100%;
height: 100%;
font-size: 16px;
font-family: 'Montserrat', sans-serif;
}
/* font-family */
@utility font-family-mon {
font-family: 'Montserrat', sans-serif;
}
@utility font-family-rob {
font-family: 'Roboto', sans-serif;
}
@utility font-family-int {
font-family: 'Inter', sans-serif;
}
@utility font-family-orb {
font-family: 'Orbitron', sans-serif;
}
@utility font-family-din {
font-family: 'DIN', sans-serif;
}
@utility flex-row-center {
@apply flex flex-row items-center justify-center;
}
@utility flex-col-center {
@apply flex flex-col items-center justify-center;
}
@utility scrollbar {
overflow: auto;
/* 整个滚动条 */
&::-webkit-scrollbar {
width: 3px;
height: 3px;
}
/* 滚动条有滑块的轨道部分 */
&::-webkit-scrollbar-track-piece {
background-color: transparent;
border-radius: 1px;
}
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
&::-webkit-scrollbar-thumb {
cursor: pointer;
background-color: #c1c1c1;
border-radius: 5px;
}
/* 滚动条滑块hover */
&::-webkit-scrollbar-thumb:hover {
background-color: #999999;
}
/* 同时有垂直和水平滚动条时交汇的部分 */
&::-webkit-scrollbar-corner {
display: block; /* 修复交汇时出现的白块 */
}
}

View File

@@ -3,7 +3,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Apps</title>
<link rel="stylesheet" href="./src/assets/index.css">
<link rel="stylesheet" href="./src/global.css">
<style>
html,
body {

View File

@@ -1,67 +0,0 @@
@import 'tailwindcss';
@import '@kevisual/center-components/theme/wind-theme.css';
@layer components {
.test-loading {
@apply w-20 h-20 bg-gray-300 rounded-full animate-spin;
}
}
:root {
--scrollbar-color: #ffbf00;
--primary-color: #ffc107;
--secondary-color: #ffa000;
}
#root {
width: 100%;
height: 100%;
}
#ai-bot-root {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: -100px;
z-index: 9999;
pointer-events: none;
}
.scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-color) #fff;
}
.scrollbar::-webkit-scrollbar {
height: 4px;
width: 4px;
}
.scrollbar::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-color);
border-radius: 10px;
}
.scrollbar::-webkit-scrollbar-track {
background: #fff;
}
.ant-select-outlined.ant-select-multiple .ant-select-selection-item {
background: var(--secondary-color);
color: white;
svg {
color: white;
}
}
.ant-select-selection-item {
color: var(--primary-color);
}
.ant-select {
.ant-select-arrow {
display: none !important;
}
}
.ant-picker-input {
.ant-picker-suffix,
.ant-picker-clear {
color: var(--primary-color);
}
}

View File

@@ -0,0 +1,4 @@
@import 'tailwindcss';
@import '@kevisual/center-components/theme/wind-theme.css';
@import './style.css';

View File

@@ -1,6 +1,5 @@
import { useEffect, useMemo } from 'react';
import { theme } from '@kevisual/center-components/theme/index.tsx';
import { ThemeProvider } from '@mui/material/styles';
import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx';
import { Left } from './layout/Left';
import { Main } from './main/index';
import { ToastContainer } from 'react-toastify';
@@ -120,11 +119,11 @@ export const App = ({ providers, noProvider, isSingleApp = true }: AppProps) =>
return <>{children}</>;
}
return (
<ThemeProvider theme={theme}>
<CustomThemeProvider>
<AntdConfigProvider>
{children}
<ToastContainer />
</AntdConfigProvider>
</ThemeProvider>
</CustomThemeProvider>
);
};

View File

@@ -6,107 +6,116 @@ import { getIcon } from '../FileIcon';
import { Download, Trash } from 'lucide-react';
import clsx from 'clsx';
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
import { useConfirm } from '@kevisual/center-components/modal/Confirm.tsx';
export const FileTable = () => {
const { list, prefix, download, onOpenPrefix, deleteFile } = useResourceStore();
const { setOpenDrawer, setPrefix } = useResourceFileStore();
const { confirm, contextHolder } = useConfirm();
return (
<TableContainer
className='scrollbar'
sx={{
'&': {
// scrollbarWidth: 'none',
// scrollbarColor: '#888 #fff',
},
'&::-webkit-scrollbar': {
width: '4px !important',
height: '4px !important',
background: '#fff',
},
'&::-webkit-scrollbar-thumb': {
background: '#888',
borderRadius: '2px',
},
}}
component={Paper}>
<Table
<>
{contextHolder}
<TableContainer
className='scrollbar'
sx={{
minWidth: 650,
'&': {
// scrollbarWidth: 'none',
// scrollbarColor: '#888 #fff',
},
'&::-webkit-scrollbar': {
width: '4px !important',
height: '4px !important',
background: '#fff',
},
'&::-webkit-scrollbar-thumb': {
background: '#888',
borderRadius: '2px',
},
}}
aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell sx={{ minWidth: 100 }}>Size</TableCell>
<TableCell sx={{ minWidth: 180 }}>Last Modified</TableCell>
<TableCell sx={{ minWidth: 110 }}>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{list.map((row) => {
const isFile = !!row.name;
return (
<TableRow key={row.etag || row.name || row.prefix}>
<TableCell>
<div
className={clsx('flex items-center gap-2 max-w-[300px] line-clamp-2 text-ellipsis', {
'cursor-pointer': true,
})}
onClick={(e) => {
if (!row.name) {
onOpenPrefix(row.prefix || '');
} else {
setPrefix(row.name || '');
setOpenDrawer(true);
}
e.stopPropagation();
}}>
<div className='shrink-0'>{getIcon(row.name)}</div>
{row.name ? row.name.replace(prefix, '') : row.prefix?.replace?.(prefix, '')}
</div>
</TableCell>
<TableCell>{row.size ? prettyBytes(row.size) : ''}</TableCell>
<TableCell>{row.lastModified ? dayjs(row.lastModified).format('YYYY-MM-DD HH:mm:ss') : ''}</TableCell>
<TableCell>
{isFile && (
<Button
variant='contained'
color='primary'
component={Paper}>
<Table
sx={{
minWidth: 650,
}}
aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell sx={{ minWidth: 100 }}>Size</TableCell>
<TableCell sx={{ minWidth: 180 }}>Last Modified</TableCell>
<TableCell sx={{ minWidth: 110 }}>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{list.map((row) => {
const isFile = !!row.name;
return (
<TableRow key={row.etag || row.name || row.prefix}>
<TableCell>
<div
className={clsx('flex items-center gap-2 max-w-[300px] line-clamp-2 text-ellipsis', {
'cursor-pointer': true,
})}
onClick={(e) => {
if (!row.name) {
onOpenPrefix(row.prefix || '');
} else {
setPrefix(row.name || '');
setOpenDrawer(true);
}
e.stopPropagation();
download(row);
}}
sx={{
color: 'white',
minWidth: '32px',
padding: '4px',
}}>
<Download />
</Button>
)}
{isFile && (
<Button
variant='contained'
color='error'
className='ml-2!'
onClick={(e) => {
e.stopPropagation();
deleteFile(row);
}}
sx={{
color: 'white',
minWidth: '32px',
padding: '4px',
}}>
<Trash />
</Button>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<div className='shrink-0'>{getIcon(row.name)}</div>
{row.name ? row.name.replace(prefix, '') : row.prefix?.replace?.(prefix, '')}
</div>
</TableCell>
<TableCell>{row.size ? prettyBytes(row.size) : ''}</TableCell>
<TableCell>{row.lastModified ? dayjs(row.lastModified).format('YYYY-MM-DD HH:mm:ss') : ''}</TableCell>
<TableCell>
{isFile && (
<Button
variant='contained'
color='primary'
onClick={(e) => {
e.stopPropagation();
download(row);
}}
sx={{
color: 'white',
minWidth: '32px',
padding: '4px',
}}>
<Download />
</Button>
)}
{isFile && (
<Button
variant='contained'
color='error'
className='ml-2!'
onClick={(e) => {
e.stopPropagation();
confirm('删除文件', '确定删除该文件吗?', {
onConfirm: () => {
deleteFile(row);
},
});
}}
sx={{
color: 'white',
minWidth: '32px',
padding: '4px',
}}>
<Trash />
</Button>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</>
);
};

View File

@@ -1,37 +1,16 @@
import React, { Fragment, useEffect, useMemo } from 'react';
import {
AppBar,
Box,
CssBaseline,
Divider,
List,
ListItem,
ListItemButton,
ListItemText,
Toolbar,
Typography,
Button,
useTheme,
ListItemIcon,
Tooltip,
Container,
} from '@mui/material';
import { Box, Divider, List, ListItem, ListItemButton, ListItemText, useTheme, ListItemIcon, Tooltip, Container } from '@mui/material';
import { ActiveMenu, useLayoutStore } from '../store/layout';
import { activeMenuList } from '../modules/MenuList';
import { amber } from '@mui/material/colors';
import { lighten, rgbToHex } from '@mui/material/styles';
import { SquareMenu } from 'lucide-react';
const drawerWidth = 240;
export type LeftProps = {
children?: React.ReactNode;
};
export const Left = ({ children }: LeftProps) => {
const { setActiveMenu, init, showLabel, setShowLabel } = useLayoutStore();
const theme = useTheme();
// console.log(theme.palette.primary.main, rgbToHex(lighten(theme.palette.primary.main, 0.4)));
// console.log(theme.palette.divider);
useEffect(() => {
init();
}, []);
@@ -49,23 +28,24 @@ export const Left = ({ children }: LeftProps) => {
}, [activeMenuList]);
return (
<Box sx={{ display: 'flex', backgroundColor: amber[50] }}>
<Box sx={{ height: '100%', display: 'flex', backgroundColor: amber[50] }}>
<Box component='nav' sx={{ flexShrink: { sm: 0 } }}>
<Box
sx={{
flexShrink: 0,
'& .MuiDrawer-paper': { boxSizing: 'border-box' },
height: '100%',
}}>
<Box
sx={{
textAlign: 'center',
height: '100vh',
borderRight: 1,
borderColor: theme.palette.divider,
display: 'flex',
height: '100%',
flexDirection: 'column',
}}>
<List sx={{ flexGrow: 1 }} key={list.length}>
<List sx={{ flexGrow: 1, height: '100%' }} key={list.length}>
{list.map((item) => (
<Fragment key={item.value}>
<ListItem disablePadding>
@@ -106,7 +86,7 @@ export const Left = ({ children }: LeftProps) => {
</Box>
</Box>
</Box>
<Box sx={{ flexGrow: 1, height: '100vh', overflow: 'hidden' }}>
<Box sx={{ flexGrow: 1, height: '100%', overflow: 'hidden' }}>
<Container sx={{ width: '100%', height: '100%' }}>{children}</Container>
</Box>
</Box>

View File

@@ -18,5 +18,5 @@ export const Main = () => {
if (activeMenu === ActiveMenu.Statistic) {
return <Statistic />;
}
return <div className='h-full'>{activeMenu}</div>;
return <div className='h-full overflow-hidden'>{activeMenu}</div>;
};

View File

@@ -9,10 +9,12 @@ type ConvertOpts = {
version?: string;
username?: string;
directory?: string;
isPublic?: boolean;
filename?: string;
};
export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
const { directory, appKey, version, username } = opts;
const { directory, appKey, version, username, isPublic } = opts;
return new Promise(async (resolve, reject) => {
const token = localStorage.getItem('token');
if (!token) {
@@ -21,11 +23,16 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
}
const taskId = nanoid();
const filename = file.name;
const filename = opts.filename || file.name;
const load = toast.loading(`${filename} 上传中...`);
NProgress.start();
// const eventSource = new EventSource('http://49.232.155.236:11015/api/s1/events?taskId=' + taskId);
const eventSource = new EventSource('/api/s1/events?taskId=' + taskId);
const searchParams = new URLSearchParams();
searchParams.set('taskId', taskId);
if (isPublic) {
searchParams.set('public', 'true');
}
const eventSource = new EventSource('/api/s1/events?' + searchParams.toString());
// 监听服务器推送的进度更新
eventSource.onmessage = function (event) {
console.log('Progress update:', event.data);
@@ -60,9 +67,10 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk, file.name);
formData.append('file', chunk, filename);
formData.append('chunkIndex', currentChunk.toString());
formData.append('totalChunks', totalChunks.toString());
const isLast = currentChunk === totalChunks - 1;
if (directory) {
formData.append('directory', directory);
}
@@ -83,10 +91,12 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
},
}).then((response) => response.json());
fetch('/api/s1/events/close?taskId=' + taskId);
eventSource.close();
NProgress.done();
toast.dismiss(load);
resolve(res);
if (isLast) {
NProgress.done();
eventSource.close();
toast.dismiss(load);
resolve(res);
}
// console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res);
} catch (error) {
console.log('Error uploading chunk', error);

View File

@@ -1,3 +1,22 @@
:root {
--scrollbar-color: #ffbf00;
--primary-color: #ffc107;
--secondary-color: #ffa000;
}
#root {
width: 100%;
height: 100%;
}
#ai-bot-root {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: -100px;
z-index: 9999;
pointer-events: none;
}
.scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-color) #fff;
@@ -15,5 +34,4 @@
.scrollbar::-webkit-scrollbar-track {
background: #fff;
}
}