feat(prompts+i18n): add prompt management and improve prompt editor i18n (#193)
* 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.
This commit is contained in:
160
src/components/prompts/PromptFormModal.tsx
Normal file
160
src/components/prompts/PromptFormModal.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import MarkdownEditor from "@/components/MarkdownEditor";
|
||||
import type { Prompt, AppId } from "@/lib/api";
|
||||
|
||||
interface PromptFormModalProps {
|
||||
appId: AppId;
|
||||
editingId?: string;
|
||||
initialData?: Prompt;
|
||||
onSave: (id: string, prompt: Prompt) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PromptFormModal: React.FC<PromptFormModalProps> = ({
|
||||
appId,
|
||||
editingId,
|
||||
initialData,
|
||||
onSave,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const appName = t(`apps.${appId}`);
|
||||
const filenameMap: Record<AppId, string> = {
|
||||
claude: "CLAUDE.md",
|
||||
codex: "AGENTS.md",
|
||||
gemini: "GEMINI.md",
|
||||
};
|
||||
const filename = filenameMap[appId];
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 检测初始暗色模式状态
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
|
||||
// 监听 html 元素的 class 变化以实时响应主题切换
|
||||
const observer = new MutationObserver(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setName(initialData.name);
|
||||
setDescription(initialData.description || "");
|
||||
setContent(initialData.content);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim() || !content.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const id = editingId || `prompt-${Date.now()}`;
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const prompt: Prompt = {
|
||||
id,
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
content: content.trim(),
|
||||
enabled: initialData?.enabled || false,
|
||||
createdAt: initialData?.createdAt || timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
await onSave(id, prompt);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
// Error handled by hook
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingId
|
||||
? t("prompts.editTitle", { appName })
|
||||
: t("prompts.addTitle", { appName })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-4 px-6 py-4">
|
||||
<div>
|
||||
<Label htmlFor="name">{t("prompts.name")}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t("prompts.namePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">{t("prompts.description")}</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t("prompts.descriptionPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="content" className="mb-2 block">
|
||||
{t("prompts.content")}
|
||||
</Label>
|
||||
<MarkdownEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
placeholder={t("prompts.contentPlaceholder", { filename })}
|
||||
darkMode={isDarkMode}
|
||||
minHeight="300px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={!name.trim() || !content.trim() || saving}
|
||||
>
|
||||
{saving ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptFormModal;
|
||||
Reference in New Issue
Block a user