Files
cc-switch/src/components/prompts/PromptFormModal.tsx
YoVinchen 155532ea8c 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.
2025-11-12 16:41:41 +08:00

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;