diff --git a/frontend/src/agents/NewAgent.tsx b/frontend/src/agents/NewAgent.tsx index e8593caa..e13b0e6c 100644 --- a/frontend/src/agents/NewAgent.tsx +++ b/frontend/src/agents/NewAgent.tsx @@ -1523,7 +1523,7 @@ function AddPromptModal({ onClose(); setNewPromptName(''); setNewPromptContent(''); - onSelect?.(newPromptName, newPrompt.id, newPromptContent); + onSelect?.(newPromptName, newPrompt.id, 'private'); } catch (error) { console.error('Error adding prompt:', error); } diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index 83f782fa..527e5179 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -481,6 +481,11 @@ "addDescription": "Füge deinen benutzerdefinierten Prompt hinzu und speichere ihn in DocsGPT", "editPrompt": "Prompt bearbeiten", "editDescription": "Bearbeite deinen benutzerdefinierten Prompt und speichere ihn in DocsGPT", + "viewPrompt": "Prompt ansehen", + "viewDescription": "Integrierte Prompts können nicht bearbeitet werden. Dupliziere ihn, um eine eigene bearbeitbare Kopie zu erstellen.", + "duplicate": "Duplizieren", + "duplicatePrompt": "Prompt duplizieren", + "duplicateDescription": "Du bearbeitest eine Kopie von „{{name}}“ – speichere sie als eigenen Prompt", "promptName": "Prompt-Name", "promptText": "Prompt-Text", "save": "Speichern", diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 8d91f741..2007d474 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -574,6 +574,11 @@ "addDescription": "Add your custom prompt and save it to DocsGPT", "editPrompt": "Edit Prompt", "editDescription": "Edit your custom prompt and save it to DocsGPT", + "viewPrompt": "View Prompt", + "viewDescription": "Built-in prompts can't be edited. Duplicate to create your own editable copy.", + "duplicate": "Duplicate", + "duplicatePrompt": "Duplicate Prompt", + "duplicateDescription": "You're editing a copy of \"{{name}}\" — save it as your own prompt", "promptName": "Prompt Name", "promptText": "Prompt Text", "save": "Save", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 4f06f46c..69a0458c 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -499,6 +499,11 @@ "addDescription": "Agrega tu prompt personalizado y guárdalo en DocsGPT", "editPrompt": "Editar Prompt", "editDescription": "Edita tu prompt personalizado y guárdalo en DocsGPT", + "viewPrompt": "Ver Prompt", + "viewDescription": "Los prompts integrados no se pueden editar. Duplícalo para crear tu propia copia editable.", + "duplicate": "Duplicar", + "duplicatePrompt": "Duplicar Prompt", + "duplicateDescription": "Estás editando una copia de \"{{name}}\"; guárdala como tu propio prompt", "promptName": "Nombre del Prompt", "promptText": "Texto del Prompt", "save": "Guardar", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index b4d0e1e5..3a476028 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -499,6 +499,11 @@ "addDescription": "カスタムプロンプトを追加して DocsGPT に保存します", "editPrompt": "プロンプトを編集", "editDescription": "カスタムプロンプトを編集して DocsGPT に保存します", + "viewPrompt": "プロンプトを表示", + "viewDescription": "組み込みプロンプトは編集できません。複製して編集可能なコピーを作成してください。", + "duplicate": "複製", + "duplicatePrompt": "プロンプトを複製", + "duplicateDescription": "「{{name}}」のコピーを編集中です。自分のプロンプトとして保存してください", "promptName": "プロンプト名", "promptText": "プロンプトのテキスト", "save": "保存", diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 951e52a0..ce951ec2 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -499,6 +499,11 @@ "addDescription": "Добавьте свой собственный промпт и сохраните его в DocsGPT", "editPrompt": "Редактировать промпт", "editDescription": "Отредактируйте свой промпт и сохраните его в DocsGPT", + "viewPrompt": "Просмотр промпта", + "viewDescription": "Встроенные промпты нельзя редактировать. Дублируйте, чтобы создать свою редактируемую копию.", + "duplicate": "Дублировать", + "duplicatePrompt": "Дублировать промпт", + "duplicateDescription": "Вы редактируете копию «{{name}}» — сохраните её как свой промпт", "promptName": "Название промпта", "promptText": "Текст промпта", "save": "Сохранить", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index 2da96709..61ece2d4 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -499,6 +499,11 @@ "addDescription": "新增自定義提示並儲存到 DocsGPT", "editPrompt": "編輯提示", "editDescription": "編輯自定義提示並儲存到 DocsGPT", + "viewPrompt": "檢視提示", + "viewDescription": "內建提示無法編輯。建立副本後即可自行編輯。", + "duplicate": "建立副本", + "duplicatePrompt": "建立提示副本", + "duplicateDescription": "正在編輯「{{name}}」的副本,儲存後即為您自己的提示", "promptName": "提示名稱", "promptText": "提示文字", "save": "儲存", diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index e51cc953..d0d51153 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -499,6 +499,11 @@ "addDescription": "添加自定义提示并保存到 DocsGPT", "editPrompt": "编辑提示", "editDescription": "编辑自定义提示并保存到 DocsGPT", + "viewPrompt": "查看提示", + "viewDescription": "内置提示无法编辑。复制后可创建您自己的可编辑副本。", + "duplicate": "创建副本", + "duplicatePrompt": "创建提示副本", + "duplicateDescription": "正在编辑“{{name}}”的副本,保存后即为您自己的提示", "promptName": "提示名称", "promptText": "提示文本", "save": "保存", diff --git a/frontend/src/preferences/PromptsModal.tsx b/frontend/src/preferences/PromptsModal.tsx index 2fde03f0..efb34a45 100644 --- a/frontend/src/preferences/PromptsModal.tsx +++ b/frontend/src/preferences/PromptsModal.tsx @@ -175,6 +175,7 @@ type PromptTextareaProps = { value: string; onChange: (event: React.ChangeEvent) => void; ariaLabel: string; + readOnly?: boolean; }; function PromptTextarea({ @@ -182,6 +183,7 @@ function PromptTextarea({ value, onChange, ariaLabel, + readOnly = false, }: PromptTextareaProps) { const [scrollOffsets, setScrollOffsets] = React.useState({ top: 0, left: 0 }); const highlightedValue = React.useMemo( @@ -220,6 +222,7 @@ function PromptTextarea({ onScroll={handleScroll} placeholder=" " aria-label={ariaLabel} + readOnly={readOnly} /> ); @@ -298,6 +301,7 @@ function AddPrompt({ newPromptContent, setNewPromptContent, disableSave, + duplicateSourceName, }: { setModalState: (state: ActiveState) => void; handleAddPrompt?: () => void; @@ -306,6 +310,7 @@ function AddPrompt({ newPromptContent: string; setNewPromptContent: (content: string) => void; disableSave: boolean; + duplicateSourceName?: string | null; }) { const { t } = useTranslation(); const systemVariableOptions = React.useMemo( @@ -317,10 +322,16 @@ function AddPrompt({ return (

- {t('modals.prompts.addPrompt')} + {duplicateSourceName + ? t('modals.prompts.duplicatePrompt') + : t('modals.prompts.addPrompt')}

- {t('modals.prompts.addDescription')} + {duplicateSourceName + ? t('modals.prompts.duplicateDescription', { + name: duplicateSourceName, + }) + : t('modals.prompts.addDescription')}

void; handleEditPrompt?: (id: string, type: string) => void; @@ -442,6 +454,7 @@ function EditPrompt({ setEditPromptContent: (content: string) => void; currentPromptEdit: { name: string; id: string; type: string }; disableSave: boolean; + onDuplicate?: () => void; }) { const { t } = useTranslation(); const systemVariableOptions = React.useMemo( @@ -449,14 +462,23 @@ function EditPrompt({ [t], ); const toolVariables = useToolVariables(); + const isReadOnly = currentPromptEdit.type === 'public'; return (

- {t('modals.prompts.editPrompt')} + {t( + isReadOnly + ? 'modals.prompts.viewPrompt' + : 'modals.prompts.editPrompt', + )}

- {t('modals.prompts.editDescription')} + {t( + isReadOnly + ? 'modals.prompts.viewDescription' + : 'modals.prompts.editDescription', + )}

setEditPromptName(e.target.value)} labelBgClassName="bg-card" + disabled={isReadOnly} />
@@ -474,6 +497,7 @@ function EditPrompt({ value={editPromptContent} onChange={(e) => setEditPromptContent(e.target.value)} ariaLabel={t('prompts.textAriaLabel')} + readOnly={isReadOnly} />
-
-

- - {t('modals.prompts.variablesLabel')} - - - {t('modals.prompts.variablesDescription')} - -

+ {!isReadOnly && ( +
+

+ + {t('modals.prompts.variablesLabel')} + + + {t('modals.prompts.variablesDescription')} + +

-
- +
+ - + +
-
+ )}
- + {isReadOnly ? ( + onDuplicate && ( + + ) + ) : ( + + )}
@@ -588,6 +625,8 @@ export default function PromptsModal({ currentPromptEdit, handleAddPrompt, handleEditPrompt, + onDuplicate, + duplicateSourceName, }: { existingPrompts: { name: string; id: string; type: string }[]; modalState: ActiveState; @@ -609,53 +648,30 @@ export default function PromptsModal({ }; handleAddPrompt?: () => void; handleEditPrompt?: (id: string, type: string) => void; + onDuplicate?: () => void; + duplicateSourceName?: string | null; }) { - const [disableSave, setDisableSave] = React.useState(true); - const handlePromptNameChange = (edit: boolean, newName: string) => { - if (edit) { - const nameExists = existingPrompts.find( + const disableSave = React.useMemo(() => { + if (type === 'EDIT') { + const nameExists = existingPrompts.some( (prompt) => - newName === prompt.name && prompt.id !== currentPromptEdit.id, + prompt.name === editPromptName && prompt.id !== currentPromptEdit.id, ); - setDisableSave( - !( - newName && - !nameExists && - editPromptName && - editPromptContent.trim() !== '' - ), - ); - setEditPromptName(newName); - } else { - const nameExists = existingPrompts.find( - (prompt) => newName === prompt.name, - ); - setDisableSave( - !(newName && !nameExists && newPromptContent.trim() !== ''), - ); - setNewPromptName(newName); + return !editPromptName || nameExists || editPromptContent.trim() === ''; } - }; - - const handleContentChange = (edit: boolean, newContent: string) => { - if (edit) { - const nameValid = - editPromptName && - !existingPrompts.find( - (prompt) => - editPromptName === prompt.name && - prompt.id !== currentPromptEdit.id, - ); - setDisableSave(!(nameValid && newContent.trim() !== '')); - setEditPromptContent(newContent); - } else { - const nameValid = - newPromptName && - !existingPrompts.find((prompt) => newPromptName === prompt.name); - setDisableSave(!(nameValid && newContent.trim() !== '')); - setNewPromptContent(newContent); - } - }; + const nameExists = existingPrompts.some( + (prompt) => prompt.name === newPromptName, + ); + return !newPromptName || nameExists || newPromptContent.trim() === ''; + }, [ + type, + existingPrompts, + editPromptName, + editPromptContent, + currentPromptEdit.id, + newPromptName, + newPromptContent, + ]); let view; @@ -665,10 +681,11 @@ export default function PromptsModal({ setModalState={setModalState} handleAddPrompt={handleAddPrompt} newPromptName={newPromptName} - setNewPromptName={handlePromptNameChange.bind(null, false)} + setNewPromptName={setNewPromptName} newPromptContent={newPromptContent} - setNewPromptContent={handleContentChange.bind(null, false)} + setNewPromptContent={setNewPromptContent} disableSave={disableSave} + duplicateSourceName={duplicateSourceName} /> ); } else if (type === 'EDIT') { @@ -677,11 +694,12 @@ export default function PromptsModal({ setModalState={setModalState} handleEditPrompt={handleEditPrompt} editPromptName={editPromptName} - setEditPromptName={handlePromptNameChange.bind(null, true)} + setEditPromptName={setEditPromptName} editPromptContent={editPromptContent} - setEditPromptContent={handleContentChange.bind(null, true)} + setEditPromptContent={setEditPromptContent} currentPromptEdit={currentPromptEdit} disableSave={disableSave} + onDuplicate={onDuplicate} /> ); } else { @@ -701,7 +719,15 @@ export default function PromptsModal({ } }} hideTitle - title={type === 'ADD' ? 'Add Prompt' : 'Edit Prompt'} + title={ + type === 'ADD' + ? duplicateSourceName + ? 'Duplicate Prompt' + : 'Add Prompt' + : currentPromptEdit.type === 'public' + ? 'View Prompt' + : 'Edit Prompt' + } size="lg" mobileVariant="sheet" className="bg-card dark:bg-card w-[95vw] max-w-[650px] rounded-2xl px-4 py-4 sm:px-6 sm:py-6 md:max-w-[860px] md:px-8 md:py-6 lg:max-w-[980px]" diff --git a/frontend/src/settings/Prompts.tsx b/frontend/src/settings/Prompts.tsx index 55c14066..d920c983 100644 --- a/frontend/src/settings/Prompts.tsx +++ b/frontend/src/settings/Prompts.tsx @@ -1,4 +1,4 @@ -import { ChevronDown, Pencil, Trash2 } from 'lucide-react'; +import { ChevronDown, Copy, Eye, Pencil, Trash2 } from 'lucide-react'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -14,6 +14,7 @@ import { import { Button } from '../components/ui/button'; import { Popover, + PopoverAnchor, PopoverContent, PopoverTrigger, } from '../components/ui/popover'; @@ -57,6 +58,9 @@ export default function Prompts({ type: '', }); const [modalType, setModalType] = React.useState<'ADD' | 'EDIT'>('ADD'); + const [duplicateSource, setDuplicateSource] = React.useState( + null, + ); const [modalState, setModalState] = React.useState('INACTIVE'); const [open, setOpen] = React.useState(false); @@ -95,7 +99,7 @@ export default function Prompts({ ]); } setModalState('INACTIVE'); - onSelectPrompt(newPromptName, newPrompt.id, newPromptContent); + onSelectPrompt(newPromptName, newPrompt.id, 'private'); setNewPromptName(''); setNewPromptContent(''); } catch (error) { @@ -155,6 +159,62 @@ export default function Prompts({ } }; + const openEditModal = (prompt: { + id: string; + name: string; + type: string; + }) => { + setModalType('EDIT'); + setEditPromptName(prompt.name); + setEditPromptContent(''); + handleFetchPromptContent(prompt.id); + setCurrentPromptEdit({ + id: prompt.id, + name: prompt.name, + type: prompt.type, + }); + setModalState('ACTIVE'); + setOpen(false); + }; + + const generateCopyName = (baseName: string) => { + let candidate = `${baseName} copy`; + let counter = 2; + while (prompts.some((prompt) => prompt.name === candidate)) { + candidate = `${baseName} copy ${counter}`; + counter += 1; + } + return candidate; + }; + + const handleDuplicatePrompt = async (prompt: { + id: string; + name: string; + }) => { + try { + const response = await userService.getSinglePrompt(prompt.id, token); + if (!response.ok) { + throw new Error('Failed to fetch prompt content'); + } + const promptContent = await response.json(); + setModalType('ADD'); + setDuplicateSource(prompt.name); + setNewPromptName(generateCopyName(prompt.name)); + setNewPromptContent(promptContent.content); + setModalState('ACTIVE'); + setOpen(false); + } catch (error) { + console.error(error); + } + }; + + const handleDuplicateFromModal = () => { + setDuplicateSource(currentPromptEdit.name); + setNewPromptName(generateCopyName(currentPromptEdit.name)); + setNewPromptContent(editPromptContent); + setModalType('ADD'); + }; + const handleSaveChanges = (id: string, type: string) => { userService .updatePrompt( @@ -196,8 +256,8 @@ export default function Prompts({ }); }; - const triggerClassName = cn( - 'border-border bg-card text-foreground hover:bg-accent flex w-56 items-center justify-between rounded-3xl border px-5 py-3 text-sm', + const pillClassName = cn( + 'border-border bg-card text-foreground hover:bg-accent flex w-56 items-stretch rounded-3xl border text-sm transition-colors', dropdownProps.className, ); @@ -210,28 +270,54 @@ export default function Prompts({

- - + + {selectedPrompt?.id && selectedPrompt.type !== 'public' && ( + <> + + + No results found {prompts.map((prompt) => { const isActive = selectedPrompt?.id === prompt.id; - const canDelete = prompt.type !== 'public'; + const canModify = prompt.type !== 'public'; return ( {prompt.name}
- {prompt.type !== 'public' && ( - - )} - {canDelete && ( + + + {canModify && ( )}
@@ -311,6 +405,7 @@ export default function Prompts({ className="h-auto w-20 rounded-3xl border border-transparent py-3" onClick={() => { setModalType('ADD'); + setDuplicateSource(null); setModalState('ACTIVE'); }} > @@ -336,6 +431,8 @@ export default function Prompts({ currentPromptEdit={currentPromptEdit} handleAddPrompt={handleAddPrompt} handleEditPrompt={handleSaveChanges} + onDuplicate={handleDuplicateFromModal} + duplicateSourceName={duplicateSource} /> {promptToDelete && (