* feat(prompts): add prompt management across Tauri service and React UI
- backend: add commands/prompt.rs, services/prompt.rs, register in commands/mod.rs and lib.rs, refine app_config.rs
- frontend: add PromptPanel, PromptFormModal, PromptListItem, MarkdownEditor, usePromptActions, integrate in App.tsx
- api: add src/lib/api/prompts.ts
- i18n: update src/i18n/locales/{en,zh}.json
- build: update package.json and pnpm-lock.yaml
* feat(i18n): improve i18n for prompts and Markdown editor
- update src/i18n/locales/{en,zh}.json keys and strings
- apply i18n in PromptFormModal, PromptPanel, and MarkdownEditor
- align prompt text with src-tauri/src/services/prompt.rs
* feat(prompts): add enable/disable toggle and simplify panel UI
- Add PromptToggle component and integrate in prompt list items
- Implement toggleEnabled with optimistic update; enable via API, disable via upsert with enabled=false;
reload after success
- Simplify PromptPanel: remove file import and current-file preview to keep CRUD flow focused
- Tweak header controls style (use mcp variant) and minor copy: rename “Prompt Management” to “Prompts”
- i18n: add disableSuccess/disableFailed messages
- Backend (Tauri): prevent duplicate backups when importing original prompt content
* style: unify code formatting with trailing commas
* feat(prompts): add Gemini filename support to PromptFormModal
Update filename mapping to use Record<AppId, string> pattern, supporting
GEMINI.md alongside CLAUDE.md and AGENTS.md.
* fix(prompts): sync enabled prompt to file when updating
When updating a prompt that is currently enabled, automatically sync
the updated content to the corresponding live file (CLAUDE.md/AGENTS.md/GEMINI.md).
This ensures the active prompt file always reflects the latest content
when editing enabled prompts.
160 lines
4.1 KiB
TypeScript
160 lines
4.1 KiB
TypeScript
import React, { useRef, useEffect } from "react";
|
|
import { EditorView, basicSetup } from "codemirror";
|
|
import { markdown } from "@codemirror/lang-markdown";
|
|
import { oneDark } from "@codemirror/theme-one-dark";
|
|
import { EditorState } from "@codemirror/state";
|
|
import { placeholder as placeholderExt } from "@codemirror/view";
|
|
|
|
interface MarkdownEditorProps {
|
|
value: string;
|
|
onChange?: (value: string) => void;
|
|
placeholder?: string;
|
|
darkMode?: boolean;
|
|
readOnly?: boolean;
|
|
className?: string;
|
|
minHeight?: string;
|
|
maxHeight?: string;
|
|
}
|
|
|
|
const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
|
|
value,
|
|
onChange,
|
|
placeholder: placeholderText = "",
|
|
darkMode = false,
|
|
readOnly = false,
|
|
className = "",
|
|
minHeight = "300px",
|
|
maxHeight,
|
|
}) => {
|
|
const editorRef = useRef<HTMLDivElement>(null);
|
|
const viewRef = useRef<EditorView | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!editorRef.current) return;
|
|
|
|
// 定义基础主题
|
|
const baseTheme = EditorView.baseTheme({
|
|
"&": {
|
|
height: "100%",
|
|
minHeight,
|
|
maxHeight: maxHeight || "none",
|
|
},
|
|
".cm-scroller": {
|
|
overflow: "auto",
|
|
fontFamily:
|
|
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
fontSize: "14px",
|
|
},
|
|
"&light .cm-content, &dark .cm-content": {
|
|
padding: "12px 0",
|
|
},
|
|
"&light .cm-editor, &dark .cm-editor": {
|
|
backgroundColor: "transparent",
|
|
},
|
|
"&.cm-focused": {
|
|
outline: "none",
|
|
},
|
|
});
|
|
|
|
const extensions = [
|
|
basicSetup,
|
|
markdown(),
|
|
baseTheme,
|
|
EditorView.lineWrapping,
|
|
EditorState.readOnly.of(readOnly),
|
|
];
|
|
|
|
if (!readOnly) {
|
|
extensions.push(
|
|
placeholderExt(placeholderText),
|
|
EditorView.updateListener.of((update) => {
|
|
if (update.docChanged && onChange) {
|
|
onChange(update.state.doc.toString());
|
|
}
|
|
}),
|
|
);
|
|
} else {
|
|
// 只读模式下隐藏光标和高亮行
|
|
extensions.push(
|
|
EditorView.theme({
|
|
".cm-cursor, .cm-dropCursor": { border: "none" },
|
|
".cm-activeLine": { backgroundColor: "transparent !important" },
|
|
".cm-activeLineGutter": { backgroundColor: "transparent !important" },
|
|
}),
|
|
);
|
|
}
|
|
|
|
// 如果启用深色模式,添加深色主题
|
|
if (darkMode) {
|
|
extensions.push(oneDark);
|
|
} else {
|
|
// 浅色模式下的简单样式调整,使其更融入 UI
|
|
extensions.push(
|
|
EditorView.theme(
|
|
{
|
|
"&": {
|
|
backgroundColor: "transparent",
|
|
},
|
|
".cm-content": {
|
|
color: "#374151", // text-gray-700
|
|
},
|
|
".cm-gutters": {
|
|
backgroundColor: "#f9fafb", // bg-gray-50
|
|
color: "#9ca3af", // text-gray-400
|
|
borderRight: "1px solid #e5e7eb", // border-gray-200
|
|
},
|
|
".cm-activeLineGutter": {
|
|
backgroundColor: "#e5e7eb",
|
|
},
|
|
},
|
|
{ dark: false },
|
|
),
|
|
);
|
|
}
|
|
|
|
// 创建初始状态
|
|
const state = EditorState.create({
|
|
doc: value,
|
|
extensions,
|
|
});
|
|
|
|
// 创建编辑器视图
|
|
const view = new EditorView({
|
|
state,
|
|
parent: editorRef.current,
|
|
});
|
|
|
|
viewRef.current = view;
|
|
|
|
return () => {
|
|
view.destroy();
|
|
viewRef.current = null;
|
|
};
|
|
}, [darkMode, readOnly, minHeight, maxHeight, placeholderText]); // 添加 placeholderText 依赖以支持国际化切换
|
|
|
|
// 当 value 从外部改变时更新编辑器内容
|
|
useEffect(() => {
|
|
if (viewRef.current && viewRef.current.state.doc.toString() !== value) {
|
|
const transaction = viewRef.current.state.update({
|
|
changes: {
|
|
from: 0,
|
|
to: viewRef.current.state.doc.length,
|
|
insert: value,
|
|
},
|
|
});
|
|
viewRef.current.dispatch(transaction);
|
|
}
|
|
}, [value]);
|
|
|
|
return (
|
|
<div
|
|
ref={editorRef}
|
|
className={`border rounded-md overflow-hidden ${
|
|
darkMode ? "border-gray-800" : "border-gray-200"
|
|
} ${className}`}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default MarkdownEditor;
|