* 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.
161 lines
4.5 KiB
TypeScript
161 lines
4.5 KiB
TypeScript
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;
|