Merge branch 'refs/heads/release/v0.2.3' into fix/release_memory_bug
This commit is contained in:
@@ -195,6 +195,11 @@ def update_config(
|
|||||||
api_logger.warning(f"用户 {current_user.username} 尝试更新配置但未选择工作空间")
|
api_logger.warning(f"用户 {current_user.username} 尝试更新配置但未选择工作空间")
|
||||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||||
|
|
||||||
|
# 校验至少有一个字段需要更新
|
||||||
|
if payload.config_name is None and payload.config_desc is None and payload.scene_id is None:
|
||||||
|
api_logger.warning(f"用户 {current_user.username} 尝试更新配置但未提供任何更新字段")
|
||||||
|
return fail(BizCode.INVALID_PARAMETER, "请至少提供一个需要更新的字段", "config_name, config_desc, scene_id 均为空")
|
||||||
|
|
||||||
api_logger.info(f"用户 {current_user.username} 在工作空间 {workspace_id} 请求更新配置: {payload.config_id}")
|
api_logger.info(f"用户 {current_user.username} 在工作空间 {workspace_id} 请求更新配置: {payload.config_id}")
|
||||||
try:
|
try:
|
||||||
svc = DataConfigService(db)
|
svc = DataConfigService(db)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from app.services.ontology_service import OntologyService
|
|||||||
from app.core.memory.llm_tools.openai_client import OpenAIClient
|
from app.core.memory.llm_tools.openai_client import OpenAIClient
|
||||||
from app.core.memory.utils.validation.owl_validator import OWLValidator
|
from app.core.memory.utils.validation.owl_validator import OWLValidator
|
||||||
from app.services.model_service import ModelConfigService
|
from app.services.model_service import ModelConfigService
|
||||||
|
from app.repositories.ontology_scene_repository import OntologySceneRepository
|
||||||
|
|
||||||
|
|
||||||
api_logger = get_api_logger()
|
api_logger = get_api_logger()
|
||||||
@@ -766,6 +767,46 @@ async def delete_scene(
|
|||||||
return fail(BizCode.INTERNAL_ERROR, "场景删除失败", str(e))
|
return fail(BizCode.INTERNAL_ERROR, "场景删除失败", str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/scenes/simple", response_model=ApiResponse)
|
||||||
|
async def get_scenes_simple(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""获取场景简单列表(轻量级,用于下拉选择)
|
||||||
|
|
||||||
|
仅返回 scene_id 和 scene_name,不加载关联数据,响应速度快。
|
||||||
|
适用于前端下拉选择场景的场景。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: 数据库会话
|
||||||
|
current_user: 当前用户
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ApiResponse: 包含场景简单列表
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
GET /scenes/simple
|
||||||
|
返回: {"data": [{"scene_id": "xxx", "scene_name": "场景1"}, ...]}
|
||||||
|
"""
|
||||||
|
api_logger.info(f"Simple scene list requested by user {current_user.id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
workspace_id = current_user.current_workspace_id
|
||||||
|
if not workspace_id:
|
||||||
|
api_logger.warning(f"User {current_user.id} has no current workspace")
|
||||||
|
return fail(BizCode.BAD_REQUEST, "请求参数无效", "当前用户没有工作空间")
|
||||||
|
|
||||||
|
repo = OntologySceneRepository(db)
|
||||||
|
scenes = repo.get_simple_list(workspace_id)
|
||||||
|
|
||||||
|
api_logger.info(f"Simple scene list retrieved: {len(scenes)} scenes")
|
||||||
|
return success(data=scenes, msg="查询成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
api_logger.error(f"Failed to get simple scene list: {str(e)}", exc_info=True)
|
||||||
|
return fail(BizCode.INTERNAL_ERROR, "查询失败", str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scenes", response_model=ApiResponse)
|
@router.get("/scenes", response_model=ApiResponse)
|
||||||
async def get_scenes(
|
async def get_scenes(
|
||||||
workspace_id: Optional[str] = None,
|
workspace_id: Optional[str] = None,
|
||||||
|
|||||||
@@ -279,6 +279,9 @@ class MemoryConfigRepository:
|
|||||||
if update.config_desc is not None:
|
if update.config_desc is not None:
|
||||||
db_config.config_desc = update.config_desc
|
db_config.config_desc = update.config_desc
|
||||||
has_update = True
|
has_update = True
|
||||||
|
if update.scene_id is not None:
|
||||||
|
db_config.scene_id = update.scene_id
|
||||||
|
has_update = True
|
||||||
|
|
||||||
if not has_update:
|
if not has_update:
|
||||||
raise ValueError("No fields to update")
|
raise ValueError("No fields to update")
|
||||||
@@ -650,28 +653,32 @@ class MemoryConfigRepository:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all(db: Session, workspace_id: Optional[uuid.UUID] = None) -> List[MemoryConfig]:
|
def get_all(db: Session, workspace_id: Optional[uuid.UUID] = None) -> List[Tuple[MemoryConfig, Optional[str]]]:
|
||||||
"""获取所有配置参数
|
"""获取所有配置参数,包含关联的场景名称
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: 数据库会话
|
db: 数据库会话
|
||||||
workspace_id: 工作空间ID,用于过滤查询结果
|
workspace_id: 工作空间ID,用于过滤查询结果
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[MemoryConfig]: 配置列表
|
List[Tuple[MemoryConfig, Optional[str]]]: 配置列表,每项为 (配置对象, 场景名称)
|
||||||
"""
|
"""
|
||||||
|
from app.models.ontology_scene import OntologyScene
|
||||||
|
|
||||||
db_logger.debug(f"查询所有配置: workspace_id={workspace_id}")
|
db_logger.debug(f"查询所有配置: workspace_id={workspace_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = db.query(MemoryConfig)
|
query = db.query(MemoryConfig, OntologyScene.scene_name).outerjoin(
|
||||||
|
OntologyScene, MemoryConfig.scene_id == OntologyScene.scene_id
|
||||||
|
)
|
||||||
|
|
||||||
if workspace_id:
|
if workspace_id:
|
||||||
query = query.filter(MemoryConfig.workspace_id == workspace_id)
|
query = query.filter(MemoryConfig.workspace_id == workspace_id)
|
||||||
|
|
||||||
configs = query.order_by(desc(MemoryConfig.updated_at)).all()
|
results = query.order_by(desc(MemoryConfig.updated_at)).all()
|
||||||
|
|
||||||
db_logger.debug(f"配置列表查询成功: 数量={len(configs)}")
|
db_logger.debug(f"配置列表查询成功: 数量={len(results)}")
|
||||||
return configs
|
return results
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db_logger.error(f"查询所有配置失败: workspace_id={workspace_id} - {str(e)}")
|
db_logger.error(f"查询所有配置失败: workspace_id={workspace_id} - {str(e)}")
|
||||||
|
|||||||
@@ -392,3 +392,48 @@ class OntologySceneRepository:
|
|||||||
exc_info=True
|
exc_info=True
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def get_simple_list(self, workspace_id: UUID) -> List[dict]:
|
||||||
|
"""获取场景简单列表(仅包含scene_id和scene_name,用于下拉选择)
|
||||||
|
|
||||||
|
这是一个轻量级查询,不加载关联的classes,响应速度快。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace_id: 工作空间ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[dict]: 场景简单列表,每项包含scene_id和scene_name
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> repo = OntologySceneRepository(db)
|
||||||
|
>>> scenes = repo.get_simple_list(workspace_id)
|
||||||
|
>>> # [{"scene_id": "xxx", "scene_name": "场景1"}, ...]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.debug(f"Getting simple scene list for workspace: {workspace_id}")
|
||||||
|
|
||||||
|
# 只查询需要的字段,不加载关联数据
|
||||||
|
results = self.db.query(
|
||||||
|
OntologyScene.scene_id,
|
||||||
|
OntologyScene.scene_name
|
||||||
|
).filter(
|
||||||
|
OntologyScene.workspace_id == workspace_id
|
||||||
|
).order_by(
|
||||||
|
OntologyScene.updated_at.desc()
|
||||||
|
).all()
|
||||||
|
|
||||||
|
scenes = [
|
||||||
|
{"scene_id": str(r.scene_id), "scene_name": r.scene_name}
|
||||||
|
for r in results
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(f"Found {len(scenes)} scenes (simple list) in workspace {workspace_id}")
|
||||||
|
|
||||||
|
return scenes
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to get simple scene list: {str(e)}",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|||||||
@@ -248,8 +248,9 @@ class ConfigParamsDelete(BaseModel): # 删除配置参数模型(请求体)
|
|||||||
|
|
||||||
class ConfigUpdate(BaseModel): # 更新记忆萃取引擎配置参数时使用的模型
|
class ConfigUpdate(BaseModel): # 更新记忆萃取引擎配置参数时使用的模型
|
||||||
config_id: Union[uuid.UUID, int, str] = None
|
config_id: Union[uuid.UUID, int, str] = None
|
||||||
config_name: str = Field("配置名称", description="配置名称(字符串)")
|
config_name: Optional[str] = Field(None, description="配置名称(字符串)")
|
||||||
config_desc: str = Field("配置描述", description="配置描述(字符串)")
|
config_desc: Optional[str] = Field(None, description="配置描述(字符串)")
|
||||||
|
scene_id: Optional[uuid.UUID] = Field(None, description="本体场景ID")
|
||||||
|
|
||||||
|
|
||||||
class ConfigUpdateExtracted(BaseModel): # 更新记忆萃取引擎配置参数时使用的模型
|
class ConfigUpdateExtracted(BaseModel): # 更新记忆萃取引擎配置参数时使用的模型
|
||||||
|
|||||||
@@ -183,11 +183,11 @@ class DataConfigService: # 数据配置服务类(PostgreSQL)
|
|||||||
|
|
||||||
# --- Read All ---
|
# --- Read All ---
|
||||||
def get_all(self, workspace_id = None) -> List[Dict[str, Any]]: # 获取所有配置参数
|
def get_all(self, workspace_id = None) -> List[Dict[str, Any]]: # 获取所有配置参数
|
||||||
configs = MemoryConfigRepository.get_all(self.db, workspace_id)
|
results = MemoryConfigRepository.get_all(self.db, workspace_id)
|
||||||
|
|
||||||
# 将 ORM 对象转换为字典列表
|
# 将 ORM 对象转换为字典列表
|
||||||
data_list = []
|
data_list = []
|
||||||
for config in configs:
|
for config, scene_name in results:
|
||||||
# 安全地转换 user_id 为 int
|
# 安全地转换 user_id 为 int
|
||||||
config_id_old = None
|
config_id_old = None
|
||||||
if config.config_id_old:
|
if config.config_id_old:
|
||||||
@@ -209,7 +209,8 @@ class DataConfigService: # 数据配置服务类(PostgreSQL)
|
|||||||
"end_user_id": config.end_user_id,
|
"end_user_id": config.end_user_id,
|
||||||
"config_id_old": config_id_old,
|
"config_id_old": config_id_old,
|
||||||
"apply_id": config.apply_id,
|
"apply_id": config.apply_id,
|
||||||
"scene_id": config.scene_id,
|
"scene_id": str(config.scene_id) if config.scene_id else None,
|
||||||
|
"scene_name": scene_name, # 新增:场景名称
|
||||||
"llm_id": config.llm_id,
|
"llm_id": config.llm_id,
|
||||||
"embedding_id": config.embedding_id,
|
"embedding_id": config.embedding_id,
|
||||||
"rerank_id": config.rerank_id,
|
"rerank_id": config.rerank_id,
|
||||||
@@ -635,10 +636,9 @@ async def analytics_recent_activity_stats() -> Dict[str, Any]:
|
|||||||
if m < 1:
|
if m < 1:
|
||||||
latest_relative = "刚刚"
|
latest_relative = "刚刚"
|
||||||
elif m < 60:
|
elif m < 60:
|
||||||
latest_relative = f"{m}分钟前"
|
latest_relative = "一会前"
|
||||||
else:
|
else:
|
||||||
h = int(m // 60)
|
latest_relative = "较早前"
|
||||||
latest_relative = f"{h}小时前" if h < 24 else f"{int(h // 24)}天前"
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,14 @@
|
|||||||
"@antv/layout": "^1.2.14-beta.8",
|
"@antv/layout": "^1.2.14-beta.8",
|
||||||
"@antv/x6": "^3.0.1",
|
"@antv/x6": "^3.0.1",
|
||||||
"@antv/x6-react-shape": "^3.0.1",
|
"@antv/x6-react-shape": "^3.0.1",
|
||||||
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
|
"@codemirror/lang-java": "^6.0.2",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/lang-rust": "^6.0.2",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@codemirror/view": "^6.39.12",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -25,6 +33,7 @@
|
|||||||
"antd": "^5.27.4",
|
"antd": "^5.27.4",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
@@ -55,6 +64,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.14",
|
"@tailwindcss/postcss": "^4.1.14",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@types/codemirror": "^5.60.17",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^24.6.0",
|
"@types/node": "^24.6.0",
|
||||||
|
|||||||
150
web/src/components/CodeMirrorEditor/index.tsx
Normal file
150
web/src/components/CodeMirrorEditor/index.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-02-04 17:20:52
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-02-04 17:20:52
|
||||||
|
*/
|
||||||
|
import { useEffect, useRef, useMemo } from 'react';
|
||||||
|
import { EditorView, basicSetup } from 'codemirror';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { python } from '@codemirror/lang-python';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { java } from '@codemirror/lang-java';
|
||||||
|
import { cpp } from '@codemirror/lang-cpp';
|
||||||
|
import { rust } from '@codemirror/lang-rust';
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the CodeMirrorEditor component
|
||||||
|
* @property {string} value - The initial code content to display in the editor
|
||||||
|
* @property {string} language - Programming language for syntax highlighting (python, python3, javascript, typescript, java, cpp, c, rust)
|
||||||
|
* @property {function} onChange - Callback function triggered when editor content changes, receives the new code value
|
||||||
|
* @property {string} theme - Editor theme, either 'light' or 'dark'
|
||||||
|
* @property {boolean} readOnly - Whether the editor is read-only
|
||||||
|
* @property {string} height - Custom height for the editor
|
||||||
|
* @property {string} size - Predefined size preset: 'default' (120px min-height, 14px font) or 'small' (60px min-height, 12px font)
|
||||||
|
*/
|
||||||
|
interface CodeMirrorEditorProps {
|
||||||
|
value?: string;
|
||||||
|
language?: 'python' | 'python3' | 'javascript' | 'typescript' | 'java' | 'cpp' | 'c' | 'rust';
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
theme?: 'light' | 'dark';
|
||||||
|
readOnly?: boolean;
|
||||||
|
height?: string;
|
||||||
|
size?: 'default' | 'small';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of language identifiers to their corresponding CodeMirror language extensions
|
||||||
|
* Supports multiple programming languages with syntax highlighting
|
||||||
|
*/
|
||||||
|
const languageExtensions: Record<string, any> = {
|
||||||
|
python: python(),
|
||||||
|
python3: python(),
|
||||||
|
javascript: javascript(),
|
||||||
|
typescript: javascript({ typescript: true }),
|
||||||
|
java: java(),
|
||||||
|
cpp: cpp(),
|
||||||
|
c: cpp(),
|
||||||
|
rust: rust(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodeMirrorEditor - A React wrapper component for CodeMirror 6 editor
|
||||||
|
* Provides a code editor with syntax highlighting, theme support, and customizable sizing
|
||||||
|
* Used in workflow code execution nodes for editing Python and JavaScript code
|
||||||
|
*/
|
||||||
|
const CodeMirrorEditor = ({
|
||||||
|
value = '',
|
||||||
|
language = 'javascript',
|
||||||
|
onChange,
|
||||||
|
theme = 'light',
|
||||||
|
readOnly = false,
|
||||||
|
size,
|
||||||
|
}: CodeMirrorEditorProps) => {
|
||||||
|
// Reference to the DOM element that will contain the editor
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
// Reference to the CodeMirror EditorView instance
|
||||||
|
const viewRef = useRef<EditorView | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize CodeMirror editor when component mounts or when language/theme/readOnly changes
|
||||||
|
* Sets up extensions for syntax highlighting, change listeners, and theme
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
|
||||||
|
// Get the appropriate language extension, fallback to JavaScript if not found
|
||||||
|
const langExtension = languageExtensions[language] || languageExtensions.javascript;
|
||||||
|
|
||||||
|
// Configure editor extensions
|
||||||
|
const extensions = [
|
||||||
|
basicSetup, // Basic editor features (line numbers, bracket matching, etc.)
|
||||||
|
langExtension, // Language-specific syntax highlighting
|
||||||
|
// Listen for document changes and trigger onChange callback
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged && onChange) {
|
||||||
|
onChange(update.state.doc.toString());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
EditorState.readOnly.of(readOnly), // Set read-only mode
|
||||||
|
];
|
||||||
|
|
||||||
|
// Apply dark theme if specified
|
||||||
|
if (theme === 'dark') {
|
||||||
|
extensions.push(oneDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create editor state with initial value and extensions
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc: value,
|
||||||
|
extensions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create and mount the editor view
|
||||||
|
viewRef.current = new EditorView({
|
||||||
|
state,
|
||||||
|
parent: editorRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup: destroy editor instance when component unmounts or dependencies change
|
||||||
|
return () => {
|
||||||
|
viewRef.current?.destroy();
|
||||||
|
};
|
||||||
|
}, [language, theme, readOnly]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update editor content when the value prop changes externally
|
||||||
|
* Only updates if the new value differs from current editor content
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
|
||||||
|
viewRef.current.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: viewRef.current.state.doc.length,
|
||||||
|
insert: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
// Calculate minimum height based on size prop: small (60px) or default (120px)
|
||||||
|
const minHeight = useMemo(() => {
|
||||||
|
return `${size === 'small' ? 60 : 120}px`
|
||||||
|
}, [size])
|
||||||
|
|
||||||
|
// Calculate font size based on size prop: small (12px) or default (14px)
|
||||||
|
const fontSize = useMemo(() => {
|
||||||
|
return `${size === 'small' ? 12 : 14}px`
|
||||||
|
}, [size])
|
||||||
|
|
||||||
|
// Calculate line height based on size prop: small (16px) or default (20px)
|
||||||
|
const lineHeight = useMemo(() => {
|
||||||
|
return `${size === 'small' ? 16 : 20}px`
|
||||||
|
}, [size])
|
||||||
|
|
||||||
|
return <div ref={editorRef} style={{ minHeight, fontSize, lineHeight }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeMirrorEditor;
|
||||||
@@ -180,4 +180,9 @@ body {
|
|||||||
.x6-node foreignObject > body {
|
.x6-node foreignObject > body {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ͼ2 .cm-gutters {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -15,8 +15,6 @@ import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
|||||||
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||||
import CommandPlugin from './plugin/CommandPlugin';
|
import CommandPlugin from './plugin/CommandPlugin';
|
||||||
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||||
import Python3HighlightPlugin from './plugin/Python3HighlightPlugin';
|
|
||||||
import JavaScriptHighlightPlugin from './plugin/JavaScriptHighlightPlugin';
|
|
||||||
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
||||||
import BlurPlugin from './plugin/BlurPlugin';
|
import BlurPlugin from './plugin/BlurPlugin';
|
||||||
import { VariableNode } from './nodes/VariableNode'
|
import { VariableNode } from './nodes/VariableNode'
|
||||||
@@ -32,7 +30,7 @@ export interface LexicalEditorProps {
|
|||||||
lineHeight?: number;
|
lineHeight?: number;
|
||||||
size?: 'default' | 'small';
|
size?: 'default' | 'small';
|
||||||
type?: 'input' | 'textarea',
|
type?: 'input' | 'textarea',
|
||||||
language?: 'string' | 'jinja2' | 'python3' | 'javascript'
|
language?: 'string' | 'jinja2'
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
@@ -67,7 +65,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const needsLineNumbers = language === 'jinja2' || language === 'python3' || language === 'javascript';
|
const needsLineNumbers = language === 'jinja2';
|
||||||
setEnableJinja2(language === 'jinja2');
|
setEnableJinja2(language === 'jinja2');
|
||||||
setEnableLineNumbers(needsLineNumbers);
|
setEnableLineNumbers(needsLineNumbers);
|
||||||
|
|
||||||
@@ -237,13 +235,11 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
{language === 'jinja2' && <Jinja2HighlightPlugin />}
|
{language === 'jinja2' && <Jinja2HighlightPlugin />}
|
||||||
{language === 'python3' && <Python3HighlightPlugin />}
|
|
||||||
{language === 'javascript' && <JavaScriptHighlightPlugin />}
|
|
||||||
{enableLineNumbers && <LineNumberPlugin />}
|
{enableLineNumbers && <LineNumberPlugin />}
|
||||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||||
<InitialValuePlugin key={language} value={value} options={options} enableLineNumbers={enableLineNumbers} />
|
<InitialValuePlugin value={value} options={options} enableLineNumbers={enableLineNumbers} />
|
||||||
{enableLineNumbers && <BlurPlugin />}
|
{enableJinja2 && <BlurPlugin />}
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
||||||
import { TextNode, $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND } from 'lexical';
|
|
||||||
|
|
||||||
const JS_KEYWORDS = new Set([
|
|
||||||
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
|
|
||||||
'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import',
|
|
||||||
'in', 'instanceof', 'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try',
|
|
||||||
'typeof', 'var', 'void', 'while', 'with', 'yield', 'true', 'false', 'null', 'undefined'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const JavaScriptHighlightPlugin = () => {
|
|
||||||
const [editor] = useLexicalComposerContext();
|
|
||||||
const isPastingRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return editor.registerCommand(
|
|
||||||
PASTE_COMMAND,
|
|
||||||
() => {
|
|
||||||
isPastingRef.current = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
isPastingRef.current = false;
|
|
||||||
}, 100);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_LOW
|
|
||||||
);
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
|
||||||
if (isPastingRef.current) return;
|
|
||||||
|
|
||||||
const text = textNode.getTextContent();
|
|
||||||
|
|
||||||
if (textNode.hasFormat('code')) return;
|
|
||||||
if (!needsHighlight(text)) return;
|
|
||||||
if (textNode.getStyle()) return;
|
|
||||||
|
|
||||||
const parent = textNode.getParent();
|
|
||||||
if (!parent) return;
|
|
||||||
|
|
||||||
const selection = $getSelection();
|
|
||||||
let selectionOffset = null;
|
|
||||||
if ($isRangeSelection(selection)) {
|
|
||||||
const anchor = selection.anchor;
|
|
||||||
if (anchor.getNode() === textNode) {
|
|
||||||
selectionOffset = anchor.offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = tokenizeJavaScript(text);
|
|
||||||
if (tokens.length <= 1) return;
|
|
||||||
|
|
||||||
const newNodes = tokens.map(token => {
|
|
||||||
const newNode = $createTextNode(token.text);
|
|
||||||
newNode.toggleFormat('code');
|
|
||||||
|
|
||||||
switch (token.type) {
|
|
||||||
case 'keyword':
|
|
||||||
newNode.setStyle('color: #d73a49; font-weight: 600;');
|
|
||||||
break;
|
|
||||||
case 'string':
|
|
||||||
newNode.setStyle('color: #032f62;');
|
|
||||||
break;
|
|
||||||
case 'comment':
|
|
||||||
newNode.setStyle('color: #6a737d; font-style: italic;');
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
newNode.setStyle('color: #005cc5; font-weight: 500;');
|
|
||||||
break;
|
|
||||||
case 'function':
|
|
||||||
newNode.setStyle('color: #6f42c1; font-weight: 500;');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newNode;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newNodes.length > 1) {
|
|
||||||
textNode.replace(newNodes[0]);
|
|
||||||
for (let i = 1; i < newNodes.length; i++) {
|
|
||||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectionOffset !== null && $isRangeSelection(selection)) {
|
|
||||||
let currentOffset = 0;
|
|
||||||
for (const node of newNodes) {
|
|
||||||
const nodeLength = node.getTextContent().length;
|
|
||||||
if (currentOffset + nodeLength >= selectionOffset) {
|
|
||||||
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
currentOffset += nodeLength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function needsHighlight(text: string): boolean {
|
|
||||||
return /[a-zA-Z0-9_/"'`]/.test(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizeJavaScript(text: string): Array<{text: string, type: string}> {
|
|
||||||
const tokens: Array<{text: string, type: string}> = [];
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (i < text.length) {
|
|
||||||
// Single-line comments
|
|
||||||
if (text.slice(i, i + 2) === '//') {
|
|
||||||
let start = i;
|
|
||||||
while (i < text.length && text[i] !== '\n') i++;
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-line comments
|
|
||||||
if (text.slice(i, i + 2) === '/*') {
|
|
||||||
let start = i;
|
|
||||||
i += 2;
|
|
||||||
while (i < text.length && text.slice(i, i + 2) !== '*/') i++;
|
|
||||||
if (i < text.length) i += 2;
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strings
|
|
||||||
if (text[i] === '"' || text[i] === "'" || text[i] === '`') {
|
|
||||||
const quote = text[i];
|
|
||||||
let start = i++;
|
|
||||||
|
|
||||||
while (i < text.length) {
|
|
||||||
if (text[i] === quote && text[i - 1] !== '\\') {
|
|
||||||
i++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'string' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numbers
|
|
||||||
if (/\d/.test(text[i])) {
|
|
||||||
let start = i;
|
|
||||||
while (i < text.length && /[\d.]/.test(text[i])) i++;
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'number' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keywords and identifiers
|
|
||||||
if (/[a-zA-Z_$]/.test(text[i])) {
|
|
||||||
let start = i;
|
|
||||||
while (i < text.length && /[a-zA-Z0-9_$]/.test(text[i])) i++;
|
|
||||||
const word = text.slice(start, i);
|
|
||||||
|
|
||||||
if (JS_KEYWORDS.has(word)) {
|
|
||||||
tokens.push({ text: word, type: 'keyword' });
|
|
||||||
} else if (i < text.length && text[i] === '(') {
|
|
||||||
tokens.push({ text: word, type: 'function' });
|
|
||||||
} else {
|
|
||||||
tokens.push({ text: word, type: 'text' });
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other characters
|
|
||||||
let start = i;
|
|
||||||
while (i < text.length && !/[a-zA-Z0-9_$/"'`]/.test(text[i])) i++;
|
|
||||||
if (start < i) {
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'text' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default JavaScriptHighlightPlugin;
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
||||||
import { TextNode, $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND } from 'lexical';
|
|
||||||
|
|
||||||
const PYTHON_KEYWORDS = new Set([
|
|
||||||
'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue',
|
|
||||||
'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import',
|
|
||||||
'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while',
|
|
||||||
'with', 'yield'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const Python3HighlightPlugin = () => {
|
|
||||||
const [editor] = useLexicalComposerContext();
|
|
||||||
const isPastingRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return editor.registerCommand(
|
|
||||||
PASTE_COMMAND,
|
|
||||||
() => {
|
|
||||||
isPastingRef.current = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
isPastingRef.current = false;
|
|
||||||
}, 100);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_LOW
|
|
||||||
);
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
|
||||||
if (isPastingRef.current) return;
|
|
||||||
|
|
||||||
const text = textNode.getTextContent();
|
|
||||||
|
|
||||||
if (textNode.hasFormat('code')) return;
|
|
||||||
if (textNode.getStyle()) return;
|
|
||||||
if (!needsHighlight(text)) return;
|
|
||||||
|
|
||||||
const parent = textNode.getParent();
|
|
||||||
if (!parent) return;
|
|
||||||
|
|
||||||
const selection = $getSelection();
|
|
||||||
let selectionOffset = null;
|
|
||||||
if ($isRangeSelection(selection)) {
|
|
||||||
const anchor = selection.anchor;
|
|
||||||
if (anchor.getNode() === textNode) {
|
|
||||||
selectionOffset = anchor.offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = tokenizePython(text);
|
|
||||||
if (tokens.length <= 1) return;
|
|
||||||
|
|
||||||
const newNodes = tokens.map(token => {
|
|
||||||
const newNode = $createTextNode(token.text);
|
|
||||||
newNode.toggleFormat('code');
|
|
||||||
|
|
||||||
switch (token.type) {
|
|
||||||
case 'keyword':
|
|
||||||
newNode.setStyle('color: #d73a49; font-weight: 600;');
|
|
||||||
break;
|
|
||||||
case 'string':
|
|
||||||
newNode.setStyle('color: #032f62;');
|
|
||||||
break;
|
|
||||||
case 'comment':
|
|
||||||
newNode.setStyle('color: #6a737d; font-style: italic;');
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
newNode.setStyle('color: #005cc5; font-weight: 500;');
|
|
||||||
break;
|
|
||||||
case 'function':
|
|
||||||
newNode.setStyle('color: #6f42c1; font-weight: 500;');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newNode;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newNodes.length > 1) {
|
|
||||||
textNode.replace(newNodes[0]);
|
|
||||||
for (let i = 1; i < newNodes.length; i++) {
|
|
||||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectionOffset !== null && $isRangeSelection(selection)) {
|
|
||||||
let currentOffset = 0;
|
|
||||||
for (const node of newNodes) {
|
|
||||||
const nodeLength = node.getTextContent().length;
|
|
||||||
if (currentOffset + nodeLength >= selectionOffset) {
|
|
||||||
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
currentOffset += nodeLength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function needsHighlight(text: string): boolean {
|
|
||||||
return /[a-zA-Z0-9_#"']/.test(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizePython(text: string): Array<{text: string, type: string}> {
|
|
||||||
const tokens: Array<{text: string, type: string}> = [];
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (i < text.length) {
|
|
||||||
// Comments
|
|
||||||
if (text[i] === '#') {
|
|
||||||
let start = i;
|
|
||||||
while (i < text.length && text[i] !== '\n') i++;
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strings
|
|
||||||
if (text[i] === '"' || text[i] === "'") {
|
|
||||||
const quote = text[i];
|
|
||||||
let start = i++;
|
|
||||||
const isTriple = text.slice(start, start + 3) === quote.repeat(3);
|
|
||||||
if (isTriple) i += 2;
|
|
||||||
|
|
||||||
while (i < text.length) {
|
|
||||||
if (isTriple && text.slice(i, i + 3) === quote.repeat(3)) {
|
|
||||||
i += 3;
|
|
||||||
break;
|
|
||||||
} else if (!isTriple && text[i] === quote && text[i - 1] !== '\\') {
|
|
||||||
i++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'string' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numbers
|
|
||||||
if (/\d/.test(text[i])) {
|
|
||||||
let start = i;
|
|
||||||
while (i < text.length && /[\d.]/.test(text[i])) i++;
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'number' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keywords and identifiers
|
|
||||||
if (/[a-zA-Z_]/.test(text[i])) {
|
|
||||||
let start = i;
|
|
||||||
while (i < text.length && /[a-zA-Z0-9_]/.test(text[i])) i++;
|
|
||||||
const word = text.slice(start, i);
|
|
||||||
|
|
||||||
if (PYTHON_KEYWORDS.has(word)) {
|
|
||||||
tokens.push({ text: word, type: 'keyword' });
|
|
||||||
} else if (i < text.length && text[i] === '(') {
|
|
||||||
tokens.push({ text: word, type: 'function' });
|
|
||||||
} else {
|
|
||||||
tokens.push({ text: word, type: 'text' });
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other characters
|
|
||||||
let start = i;
|
|
||||||
while (i < text.length && !/[a-zA-Z0-9_#"']/.test(text[i])) i++;
|
|
||||||
if (start < i) {
|
|
||||||
tokens.push({ text: text.slice(start, i), type: 'text' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Python3HighlightPlugin;
|
|
||||||
@@ -5,8 +5,8 @@ import { Node } from '@antv/x6'
|
|||||||
|
|
||||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
import MappingList from '../MappingList'
|
import MappingList from '../MappingList'
|
||||||
import Editor from '../../Editor'
|
|
||||||
import OutputList from './OutputList'
|
import OutputList from './OutputList'
|
||||||
|
import CodeMirrorEditor from '@/components/CodeMirrorEditor';
|
||||||
|
|
||||||
interface MappingItem {
|
interface MappingItem {
|
||||||
name?: string
|
name?: string
|
||||||
@@ -110,7 +110,10 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
|||||||
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.language !== curr.language}>
|
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.language !== curr.language}>
|
||||||
{() => (
|
{() => (
|
||||||
<Form.Item name="code" noStyle>
|
<Form.Item name="code" noStyle>
|
||||||
<Editor size="small" language={form.getFieldValue('language')} />
|
<CodeMirrorEditor
|
||||||
|
language={form.getFieldValue('language')}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user