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,
},
},
});