diff --git a/frontend/src/components/prompts/prompt-editor-drawer.tsx b/frontend/src/components/prompts/prompt-editor-drawer.tsx new file mode 100644 index 0000000..0f0ab63 --- /dev/null +++ b/frontend/src/components/prompts/prompt-editor-drawer.tsx @@ -0,0 +1,77 @@ +import { useTranslation } from 'react-i18next' +import type { SelectOption } from '../../features/options/options-types' +import type { PromptInput, PromptRecord } from '../../features/prompts/prompts-types' +import { Button } from '../ui/button' +import { InlineError } from '../ui/inline-error' +import { PromptForm } from './prompt-form' + +export type PromptEditorState = + | { mode: 'create' } + | { mode: 'edit'; prompt: PromptRecord } + | null + +type PromptEditorDrawerProps = { + state: PromptEditorState + isSaving: boolean + errorMessage?: string | null + modelOptions: SelectOption[] + categoryOptions: SelectOption[] + optionsLoading: boolean + onSubmit: (value: PromptInput) => Promise + onClose: () => void +} + +export function PromptEditorDrawer({ + state, + isSaving, + errorMessage, + modelOptions, + categoryOptions, + optionsLoading, + onSubmit, + onClose, +}: PromptEditorDrawerProps) { + const { t } = useTranslation() + + if (!state) return null + + const isEdit = state.mode === 'edit' + const title = isEdit ? t('prompts.edit') : t('prompts.create') + + return ( +
+ +
+ ) +} diff --git a/frontend/src/components/prompts/prompt-form.tsx b/frontend/src/components/prompts/prompt-form.tsx index 38021f5..b12b076 100644 --- a/frontend/src/components/prompts/prompt-form.tsx +++ b/frontend/src/components/prompts/prompt-form.tsx @@ -16,6 +16,7 @@ type PromptFormProps = { modelOptions: SelectOption[] categoryOptions: SelectOption[] optionsLoading: boolean + showCancel?: boolean onSubmit: (value: PromptInput) => Promise onCancelEdit: () => void } @@ -28,6 +29,7 @@ export function PromptForm({ modelOptions, categoryOptions, optionsLoading, + showCancel = false, onSubmit, onCancelEdit, }: PromptFormProps) { @@ -129,7 +131,7 @@ export function PromptForm({ - {isEdit ? ( + {isEdit || showCancel ? ( diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 5960bc2..089c0e4 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -172,6 +172,7 @@ export const en = { description: 'Save the prompts that worked, then find and reuse them faster.', edit: 'Edit prompt', create: 'Save a proven prompt', + newPrompt: 'New Prompt', listTitle: 'Saved prompts', listDescription: 'Open any card to review model, category, rating, and full prompt text.', dropdownError: 'Unable to load dropdown options', diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index e0356af..ba0a6e1 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -172,6 +172,7 @@ export const es = { description: 'Guarda los prompts que funcionaron, luego encuéntralos y reutilízalos más rápido.', edit: 'Editar prompt', create: 'Guardar un prompt probado', + newPrompt: 'Nuevo prompt', listTitle: 'Prompts guardados', listDescription: 'Abre cualquier tarjeta para revisar modelo, categoría, calificación y texto completo del prompt.', dropdownError: 'No se pudieron cargar las opciones desplegables', diff --git a/frontend/src/lib/validation/prompt-schemas.test.ts b/frontend/src/lib/validation/prompt-schemas.test.ts index dd6c928..23315fd 100644 --- a/frontend/src/lib/validation/prompt-schemas.test.ts +++ b/frontend/src/lib/validation/prompt-schemas.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { promptSchema } from './prompt-schemas' const validPrompt = { + title: 'Answer generator', model_name: 'gpt', prompt_text: 'Generate answer', category: 'qa', diff --git a/frontend/src/pages/prompts/prompts-page.tsx b/frontend/src/pages/prompts/prompts-page.tsx index 92eadb3..d977d89 100644 --- a/frontend/src/pages/prompts/prompts-page.tsx +++ b/frontend/src/pages/prompts/prompts-page.tsx @@ -1,12 +1,13 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { PromptEditorDrawer, type PromptEditorState } from '../../components/prompts/prompt-editor-drawer' +import { PromptList } from '../../components/prompts/prompt-list' +import { Button } from '../../components/ui/button' import { Card } from '../../components/ui/card' import { ConfirmDialog } from '../../components/ui/confirm-dialog' import { InlineError } from '../../components/ui/inline-error' import { PageHeader } from '../../components/ui/page-header' -import { PromptForm } from '../../components/prompts/prompt-form' -import { PromptList } from '../../components/prompts/prompt-list' import { useAuth } from '../../features/auth/auth-store' import { listCategoryOptions, @@ -27,7 +28,7 @@ export function PromptsPage() { const { t } = useTranslation() const { session } = useAuth() const queryClient = useQueryClient() - const [editingPrompt, setEditingPrompt] = useState(null) + const [editorState, setEditorState] = useState(null) const [promptToDelete, setPromptToDelete] = useState(null) const [error, setError] = useState(null) @@ -76,6 +77,7 @@ export function PromptsPage() { }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['prompts', userId] }) + setEditorState(null) setError(null) }, onError: (err) => { @@ -85,14 +87,14 @@ export function PromptsPage() { const updateMutation = useMutation({ mutationFn: async (value: PromptInput) => { - if (!token || userId === null || !editingPrompt) { + if (!token || userId === null || editorState?.mode !== 'edit') { throw new Error(t('prompts.errors.selectedUnavailable')) } - return updatePrompt(token, editingPrompt.id, userId, value) + return updatePrompt(token, editorState.prompt.id, userId, value) }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['prompts', userId] }) - setEditingPrompt(null) + setEditorState(null) setError(null) }, onError: (err) => { @@ -117,7 +119,7 @@ export function PromptsPage() { }) const handleSubmit = async (value: PromptInput) => { - if (editingPrompt) { + if (editorState?.mode === 'edit') { await updateMutation.mutateAsync(value) return } @@ -132,11 +134,27 @@ export function PromptsPage() { }) } + const openCreateDrawer = () => { + setError(null) + setEditorState({ mode: 'create' }) + } + + const openEditDrawer = (prompt: PromptRecord) => { + setError(null) + setEditorState({ mode: 'edit', prompt }) + } + + const closeEditorDrawer = () => { + setEditorState(null) + } + const prompts = promptsQuery.data ?? [] const averageRating = prompts.length ? prompts.reduce((total, prompt) => total + prompt.rate, 0) / prompts.length : 0 const categoryCount = new Set(prompts.map((prompt) => prompt.category)).size + const optionErrorMessage = categoriesQuery.error || modelsQuery.error ? t('prompts.dropdownError') : null + const drawerErrorMessage = error ?? optionErrorMessage return (
@@ -144,6 +162,11 @@ export function PromptsPage() { eyebrow={t('prompts.eyebrow')} title={t('prompts.title')} description={t('prompts.description')} + actions={( + + )} />
@@ -161,23 +184,6 @@ export function PromptsPage() {
- -

{editingPrompt ? t('prompts.edit') : t('prompts.create')}

- setEditingPrompt(null)} - /> - {categoriesQuery.error || modelsQuery.error ? ( - - ) : null} -
-
@@ -186,15 +192,26 @@ export function PromptsPage() {
{promptsQuery.isLoading ?

{t('common.loading')}

: null} - {error ? : null} + {error && !editorState ? : null} + {optionErrorMessage && !editorState ? : null} {promptsQuery.data ? ( ) : null}
+