From 1d11bd4ab51f4bfbc7419308762ee3a1889f5713 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:46:28 +0300 Subject: [PATCH] F: Implement backend hooks for frontend --- .../apps/prompt/serializers/__init__.py | 2 +- .../apps/prompt/serializers/data_access.py | 9 +++ .../backend/apps/prompt/tests/t_prompts.py | 2 + .../backend/apps/prompt/views/prompts.py | 4 +- .../frontend/src/backend/configuration.ts | 1 + .../frontend/src/features/ai/backend/api.ts | 70 +++++++++++++++++++ .../frontend/src/features/ai/backend/types.ts | 59 +++++++++++++--- .../ai/backend/use-available-templates.tsx | 17 +++++ .../ai/backend/use-create-prompt-template.tsx | 20 ++++++ .../ai/backend/use-delete-prompt-template.tsx | 21 ++++++ .../ai/backend/use-prompt-template.tsx | 17 +++++ .../ai/backend/use-update-prompt-template.tsx | 22 ++++++ .../src/features/rsform/backend/types.ts | 4 +- 13 files changed, 234 insertions(+), 14 deletions(-) create mode 100644 rsconcept/frontend/src/features/ai/backend/api.ts create mode 100644 rsconcept/frontend/src/features/ai/backend/use-available-templates.tsx create mode 100644 rsconcept/frontend/src/features/ai/backend/use-create-prompt-template.tsx create mode 100644 rsconcept/frontend/src/features/ai/backend/use-delete-prompt-template.tsx create mode 100644 rsconcept/frontend/src/features/ai/backend/use-prompt-template.tsx create mode 100644 rsconcept/frontend/src/features/ai/backend/use-update-prompt-template.tsx diff --git a/rsconcept/backend/apps/prompt/serializers/__init__.py b/rsconcept/backend/apps/prompt/serializers/__init__.py index 7402793f..de9e47e7 100644 --- a/rsconcept/backend/apps/prompt/serializers/__init__.py +++ b/rsconcept/backend/apps/prompt/serializers/__init__.py @@ -1,2 +1,2 @@ ''' Serializers for persistent data manipulation (AI Prompts). ''' -from .data_access import PromptTemplateSerializer +from .data_access import PromptTemplateListSerializer, PromptTemplateSerializer diff --git a/rsconcept/backend/apps/prompt/serializers/data_access.py b/rsconcept/backend/apps/prompt/serializers/data_access.py index 29cd1ec3..603229b5 100644 --- a/rsconcept/backend/apps/prompt/serializers/data_access.py +++ b/rsconcept/backend/apps/prompt/serializers/data_access.py @@ -37,3 +37,12 @@ class PromptTemplateSerializer(serializers.ModelSerializer): if validated_data['is_shared'] and not (user.is_superuser or user.is_staff): raise serializers.ValidationError(msg.promptSharedPermissionDenied()) return super().update(instance, validated_data) + + +class PromptTemplateListSerializer(serializers.ModelSerializer): + '''Serializer for listing PromptTemplates without the 'text' field.''' + class Meta: + ''' serializer metadata. ''' + model = PromptTemplate + fields = ['id', 'owner', 'is_shared', 'label', 'description'] + read_only_fields = ['id', 'owner'] diff --git a/rsconcept/backend/apps/prompt/tests/t_prompts.py b/rsconcept/backend/apps/prompt/tests/t_prompts.py index c3e94f09..86182170 100644 --- a/rsconcept/backend/apps/prompt/tests/t_prompts.py +++ b/rsconcept/backend/apps/prompt/tests/t_prompts.py @@ -98,6 +98,8 @@ class TestPromptTemplateViewSet(EndpointTester): labels = [item['label'] for item in response.data] self.assertIn('Mine', labels) self.assertIn('Shared', labels) + for item in response.data: + self.assertNotIn('text', item) @decl_endpoint('/api/prompts/{item}/', method='patch') diff --git a/rsconcept/backend/apps/prompt/views/prompts.py b/rsconcept/backend/apps/prompt/views/prompts.py index 2d207f32..9a39c44c 100644 --- a/rsconcept/backend/apps/prompt/views/prompts.py +++ b/rsconcept/backend/apps/prompt/views/prompts.py @@ -7,7 +7,7 @@ from rest_framework.decorators import action from rest_framework.response import Response from ..models import PromptTemplate -from ..serializers import PromptTemplateSerializer +from ..serializers import PromptTemplateListSerializer, PromptTemplateSerializer class IsOwnerOrAdmin(permissions.BasePermission): @@ -48,5 +48,5 @@ class PromptTemplateViewSet(viewsets.ModelViewSet): owned = PromptTemplate.objects.filter(owner=user) shared = PromptTemplate.objects.filter(is_shared=True) templates = (owned | shared).distinct() - serializer = self.get_serializer(templates, many=True) + serializer = PromptTemplateListSerializer(templates, many=True) return Response(serializer.data) diff --git a/rsconcept/frontend/src/backend/configuration.ts b/rsconcept/frontend/src/backend/configuration.ts index efa5c4fa..4f5b8544 100644 --- a/rsconcept/frontend/src/backend/configuration.ts +++ b/rsconcept/frontend/src/backend/configuration.ts @@ -16,6 +16,7 @@ export const KEYS = { library: 'library', users: 'users', cctext: 'cctext', + prompts: 'prompts', global_mutation: 'global_mutation', composite: { diff --git a/rsconcept/frontend/src/features/ai/backend/api.ts b/rsconcept/frontend/src/features/ai/backend/api.ts new file mode 100644 index 00000000..5cd38ed6 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/backend/api.ts @@ -0,0 +1,70 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/api-transport'; +import { DELAYS, KEYS } from '@/backend/configuration'; +import { infoMsg } from '@/utils/labels'; + +import { + type ICreatePromptTemplateDTO, + type IPromptTemplateDTO, + type IPromptTemplateListDTO, + type IUpdatePromptTemplateDTO, + schemaPromptTemplate, + schemaPromptTemplateList +} from './types'; + +export const promptsApi = { + baseKey: KEYS.prompts, + + getAvailableTemplatesQueryOptions: () => + queryOptions({ + queryKey: [KEYS.prompts, 'available'] as const, + staleTime: DELAYS.staleShort, + queryFn: meta => + axiosGet({ + schema: schemaPromptTemplateList, + endpoint: '/api/prompts/available/', + options: { signal: meta.signal } + }) + }), + + getPromptTemplateQueryOptions: (id: number) => + queryOptions({ + queryKey: [KEYS.prompts, id], + staleTime: DELAYS.staleShort, + queryFn: meta => + axiosGet({ + schema: schemaPromptTemplate, + endpoint: `/api/prompts/${id}/`, + options: { signal: meta.signal } + }) + }), + + createPromptTemplate: (data: ICreatePromptTemplateDTO) => + axiosPost({ + schema: schemaPromptTemplate, + endpoint: '/api/prompts/', + request: { + data: data, + successMessage: infoMsg.changesSaved + } + }), + + updatePromptTemplate: (id: number, data: IUpdatePromptTemplateDTO) => + axiosPatch({ + schema: schemaPromptTemplate, + endpoint: `/api/prompts/${id}/`, + request: { + data: data, + successMessage: infoMsg.changesSaved + } + }), + + deletePromptTemplate: (id: number) => + axiosDelete({ + endpoint: `/api/prompts/${id}/`, + request: { + successMessage: infoMsg.changesSaved + } + }) +} as const; diff --git a/rsconcept/frontend/src/features/ai/backend/types.ts b/rsconcept/frontend/src/features/ai/backend/types.ts index c7ec91eb..8f0d4fdc 100644 --- a/rsconcept/frontend/src/features/ai/backend/types.ts +++ b/rsconcept/frontend/src/features/ai/backend/types.ts @@ -1,11 +1,54 @@ +import { z } from 'zod'; + /** Represents AI prompt. */ -export interface IPromptTemplate { - id: number; - owner: number | null; - is_shared: boolean; - label: string; - description: string; - text: string; -} +export type IPromptTemplate = IPromptTemplateDTO; + +export type IPromptTemplateInfo = z.infer; + +/** Full prompt template as returned by backend. */ +export type IPromptTemplateDTO = z.infer; + +/** List item for available prompt templates (no text field). */ +export type IPromptTemplateListDTO = z.infer; + +/** Data for creating a prompt template. */ +export type ICreatePromptTemplateDTO = z.infer; + +/** Data for updating a prompt template. */ +export type IUpdatePromptTemplateDTO = z.infer; // ========= SCHEMAS ======== + +export const schemaPromptTemplate = z.strictObject({ + id: z.number(), + owner: z.number().nullable(), + label: z.string(), + description: z.string(), + text: z.string(), + is_shared: z.boolean() +}); + +export const schemaCreatePromptTemplate = schemaPromptTemplate.pick({ + label: true, + description: true, + text: true, + is_shared: true +}); + +export const schemaUpdatePromptTemplate = schemaPromptTemplate.pick({ + owner: true, + label: true, + description: true, + text: true, + is_shared: true +}); + +export const schemaPromptTemplateInfo = schemaPromptTemplate.pick({ + id: true, + owner: true, + label: true, + description: true, + is_shared: true +}); + +export const schemaPromptTemplateList = schemaPromptTemplateInfo.array(); diff --git a/rsconcept/frontend/src/features/ai/backend/use-available-templates.tsx b/rsconcept/frontend/src/features/ai/backend/use-available-templates.tsx new file mode 100644 index 00000000..0767f58e --- /dev/null +++ b/rsconcept/frontend/src/features/ai/backend/use-available-templates.tsx @@ -0,0 +1,17 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { promptsApi } from './api'; + +export function useAvailableTemplates() { + const { data, isLoading, error } = useQuery({ + ...promptsApi.getAvailableTemplatesQueryOptions() + }); + return { data, isLoading, error }; +} + +export function useAvailableTemplatesSuspense() { + const { data } = useSuspenseQuery({ + ...promptsApi.getAvailableTemplatesQueryOptions() + }); + return { data }; +} diff --git a/rsconcept/frontend/src/features/ai/backend/use-create-prompt-template.tsx b/rsconcept/frontend/src/features/ai/backend/use-create-prompt-template.tsx new file mode 100644 index 00000000..2fe39e82 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/backend/use-create-prompt-template.tsx @@ -0,0 +1,20 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { promptsApi } from './api'; + +export function useCreatePromptTemplate() { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [promptsApi.baseKey, 'create'], + mutationFn: promptsApi.createPromptTemplate, + onSuccess: () => { + void client.invalidateQueries({ queryKey: [promptsApi.baseKey] }); + } + }); + return { + createPromptTemplate: mutation.mutateAsync, + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +} diff --git a/rsconcept/frontend/src/features/ai/backend/use-delete-prompt-template.tsx b/rsconcept/frontend/src/features/ai/backend/use-delete-prompt-template.tsx new file mode 100644 index 00000000..06d764e9 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/backend/use-delete-prompt-template.tsx @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { promptsApi } from './api'; + +export function useDeletePromptTemplate() { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [promptsApi.baseKey, 'delete'], + mutationFn: promptsApi.deletePromptTemplate, + onSuccess: (_data, id) => { + void client.invalidateQueries({ queryKey: [promptsApi.baseKey] }); + void client.invalidateQueries({ queryKey: [promptsApi.baseKey, id] }); + } + }); + return { + deletePromptTemplate: mutation.mutateAsync, + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +} diff --git a/rsconcept/frontend/src/features/ai/backend/use-prompt-template.tsx b/rsconcept/frontend/src/features/ai/backend/use-prompt-template.tsx new file mode 100644 index 00000000..73f18b29 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/backend/use-prompt-template.tsx @@ -0,0 +1,17 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { promptsApi } from './api'; + +export function usePromptTemplate(id: number) { + const { data, isLoading, error } = useQuery({ + ...promptsApi.getPromptTemplateQueryOptions(id) + }); + return { data, isLoading, error }; +} + +export function usePromptTemplateSuspense(id: number) { + const { data } = useSuspenseQuery({ + ...promptsApi.getPromptTemplateQueryOptions(id) + }); + return { data }; +} diff --git a/rsconcept/frontend/src/features/ai/backend/use-update-prompt-template.tsx b/rsconcept/frontend/src/features/ai/backend/use-update-prompt-template.tsx new file mode 100644 index 00000000..2a18fe8a --- /dev/null +++ b/rsconcept/frontend/src/features/ai/backend/use-update-prompt-template.tsx @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { promptsApi } from './api'; +import { type IUpdatePromptTemplateDTO } from './types'; + +export function useUpdatePromptTemplate() { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [promptsApi.baseKey, 'update'], + mutationFn: ({ id, data }: { id: number; data: IUpdatePromptTemplateDTO }) => + promptsApi.updatePromptTemplate(id, data), + onSuccess: (_, variables) => { + void client.invalidateQueries({ queryKey: [promptsApi.baseKey, variables.id] }); + } + }); + return { + updatePromptTemplate: mutation.mutateAsync, + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +} diff --git a/rsconcept/frontend/src/features/rsform/backend/types.ts b/rsconcept/frontend/src/features/rsform/backend/types.ts index 399c6958..8e6ff89f 100644 --- a/rsconcept/frontend/src/features/rsform/backend/types.ts +++ b/rsconcept/frontend/src/features/rsform/backend/types.ts @@ -89,9 +89,7 @@ export interface ICheckConstituentaDTO { /** Represents data, used in merging multiple {@link IConstituenta}. */ export type ISubstitutionsDTO = z.infer; -/** - * Represents Constituenta list. - */ +/** Represents Constituenta list. */ export interface IConstituentaList { items: number[]; }