diff --git a/rsconcept/backend/apps/prompt/__init__.py b/rsconcept/backend/apps/prompt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/prompt/admin.py b/rsconcept/backend/apps/prompt/admin.py new file mode 100644 index 00000000..36ec72fc --- /dev/null +++ b/rsconcept/backend/apps/prompt/admin.py @@ -0,0 +1,12 @@ +''' Admin view: Prompts for AI helper. ''' +from django.contrib import admin + +from . import models + + +@admin.register(models.PromptTemplate) +class PromptTemplateAdmin(admin.ModelAdmin): + ''' Admin model: PromptTemplate. ''' + list_display = ('id', 'label', 'owner', 'is_shared') + list_filter = ('is_shared', 'owner') + search_fields = ('label', 'description', 'text') diff --git a/rsconcept/backend/apps/prompt/apps.py b/rsconcept/backend/apps/prompt/apps.py new file mode 100644 index 00000000..14fdd553 --- /dev/null +++ b/rsconcept/backend/apps/prompt/apps.py @@ -0,0 +1,8 @@ +''' Application: Prompts for AI helper. ''' +from django.apps import AppConfig + + +class PromptConfig(AppConfig): + ''' Application config. ''' + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.prompt' diff --git a/rsconcept/backend/apps/prompt/migrations/0001_initial.py b/rsconcept/backend/apps/prompt/migrations/0001_initial.py new file mode 100644 index 00000000..c40d22a9 --- /dev/null +++ b/rsconcept/backend/apps/prompt/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.4 on 2025-07-13 13:11 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='PromptTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_shared', models.BooleanField(default=False, verbose_name='Общий доступ')), + ('label', models.CharField(max_length=255, verbose_name='Название')), + ('description', models.TextField(blank=True, verbose_name='Описание')), + ('text', models.TextField(blank=True, verbose_name='Содержание')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prompt_templates', to=settings.AUTH_USER_MODEL, verbose_name='Владелец')), + ], + ), + ] diff --git a/rsconcept/backend/apps/prompt/migrations/__init__.py b/rsconcept/backend/apps/prompt/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/backend/apps/prompt/models/PromptTemplate.py b/rsconcept/backend/apps/prompt/models/PromptTemplate.py new file mode 100644 index 00000000..5493522b --- /dev/null +++ b/rsconcept/backend/apps/prompt/models/PromptTemplate.py @@ -0,0 +1,46 @@ +''' Model: PromptTemplate for AI prompt storage and sharing. ''' + +from django.db import models + +from apps.users.models import User + + +class PromptTemplate(models.Model): + '''Represents an AI prompt template, which can be user-owned or shared globally.''' + owner = models.ForeignKey( + verbose_name='Владелец', + to=User, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='prompt_templates' + ) + is_shared = models.BooleanField( + verbose_name='Общий доступ', + default=False + ) + label = models.CharField( + verbose_name='Название', + max_length=255 + ) + description = models.TextField( + verbose_name='Описание', + blank=True + ) + text = models.TextField( + verbose_name='Содержание', + blank=True + ) + + def can_set_shared(self, user: User) -> bool: + '''Return True if the user can set is_shared=True (admin/staff only).''' + return user.is_superuser or user.is_staff + + def can_access(self, user: User) -> bool: + '''Return True if the user can access this template (shared or owner).''' + if self.is_shared: + return True + return self.owner == user + + def __str__(self) -> str: + return f'{self.label}' diff --git a/rsconcept/backend/apps/prompt/models/__init__.py b/rsconcept/backend/apps/prompt/models/__init__.py new file mode 100644 index 00000000..29f9fabf --- /dev/null +++ b/rsconcept/backend/apps/prompt/models/__init__.py @@ -0,0 +1,3 @@ +''' Django: Models for AI Prompts. ''' + +from .PromptTemplate import PromptTemplate diff --git a/rsconcept/backend/apps/prompt/serializers/__init__.py b/rsconcept/backend/apps/prompt/serializers/__init__.py new file mode 100644 index 00000000..7402793f --- /dev/null +++ b/rsconcept/backend/apps/prompt/serializers/__init__.py @@ -0,0 +1,2 @@ +''' Serializers for persistent data manipulation (AI Prompts). ''' +from .data_access import PromptTemplateSerializer diff --git a/rsconcept/backend/apps/prompt/serializers/data_access.py b/rsconcept/backend/apps/prompt/serializers/data_access.py new file mode 100644 index 00000000..29cd1ec3 --- /dev/null +++ b/rsconcept/backend/apps/prompt/serializers/data_access.py @@ -0,0 +1,39 @@ +''' Serializers for prompt template data access. ''' + +from rest_framework import serializers + +from shared import messages as msg + +from ..models import PromptTemplate + + +class PromptTemplateSerializer(serializers.ModelSerializer): + '''Serializer for PromptTemplate, enforcing permissions and ownership logic.''' + class Meta: + ''' serializer metadata. ''' + model = PromptTemplate + fields = ['id', 'owner', 'is_shared', 'label', 'description', 'text'] + read_only_fields = ['id', 'owner'] + + def validate_label(self, value): + user = self.context['request'].user + if PromptTemplate.objects.filter(owner=user, label=value).exists(): + raise serializers.ValidationError(msg.promptLabelTaken(value)) + return value + + def validate_is_shared(self, value): + user = self.context['request'].user + if value and not (user.is_superuser or user.is_staff): + raise serializers.ValidationError(msg.promptSharedPermissionDenied()) + return value + + def create(self, validated_data): + validated_data['owner'] = self.context['request'].user + return super().create(validated_data) + + def update(self, instance, validated_data): + user = self.context['request'].user + if 'is_shared' in validated_data: + 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) diff --git a/rsconcept/backend/apps/prompt/tests/__init__.py b/rsconcept/backend/apps/prompt/tests/__init__.py new file mode 100644 index 00000000..0202bb66 --- /dev/null +++ b/rsconcept/backend/apps/prompt/tests/__init__.py @@ -0,0 +1,2 @@ +''' Tests. ''' +from .t_prompts import * diff --git a/rsconcept/backend/apps/prompt/tests/t_prompts.py b/rsconcept/backend/apps/prompt/tests/t_prompts.py new file mode 100644 index 00000000..c3e94f09 --- /dev/null +++ b/rsconcept/backend/apps/prompt/tests/t_prompts.py @@ -0,0 +1,113 @@ +''' Testing API: Prompts. ''' +from rest_framework import status + +from shared.EndpointTester import EndpointTester, decl_endpoint + +from ..models import PromptTemplate + + +class TestPromptTemplateViewSet(EndpointTester): + ''' Testing PromptTemplate viewset. ''' + + def setUp(self): + super().setUp() + self.admin = self.user2 + self.admin.is_superuser = True + self.admin.save() + + + @decl_endpoint('/api/prompts/', method='post') + def test_create_prompt(self): + data = { + 'label': 'Test', + 'description': 'desc', + 'text': 'prompt text', + 'is_shared': False + } + response = self.executeCreated(data=data) + self.assertEqual(response.data['label'], 'Test') + self.assertEqual(response.data['owner'], self.user.pk) + + + @decl_endpoint('/api/prompts/', method='post') + def test_create_shared_prompt_by_admin(self): + self.client.force_authenticate(user=self.admin) + data = { + 'label': 'Shared', + 'description': 'desc', + 'text': 'prompt text', + 'is_shared': True + } + response = self.executeCreated(data=data) + self.assertTrue(response.data['is_shared']) + + + @decl_endpoint('/api/prompts/', method='post') + def test_create_shared_prompt_by_user_forbidden(self): + data = { + 'label': 'Shared', + 'description': 'desc', + 'text': 'prompt text', + 'is_shared': True + } + response = self.executeBadData(data=data) + self.assertIn('is_shared', response.data) + + + @decl_endpoint('/api/prompts/{item}/', method='patch') + def test_update_prompt_owner(self): + prompt = PromptTemplate.objects.create(owner=self.user, label='ToUpdate', description='', text='t') + response = self.executeOK(data={'label': 'Updated'}, item=prompt.id) + self.assertEqual(response.data['label'], 'Updated') + + + @decl_endpoint('/api/prompts/{item}/', method='patch') + def test_update_prompt_not_owner_forbidden(self): + prompt = PromptTemplate.objects.create(owner=self.admin, label='Other', description='', text='t') + response = self.executeForbidden(data={'label': 'Updated'}, item=prompt.id) + + + @decl_endpoint('/api/prompts/{item}/', method='delete') + def test_delete_prompt_owner(self): + prompt = PromptTemplate.objects.create(owner=self.user, label='ToDelete', description='', text='t') + self.executeNoContent(item=prompt.id) + + + @decl_endpoint('/api/prompts/{item}/', method='delete') + def test_delete_prompt_not_owner_forbidden(self): + prompt = PromptTemplate.objects.create(owner=self.admin, label='Other2', description='', text='t') + self.executeForbidden(item=prompt.id) + + + @decl_endpoint('/api/prompts/available/', method='get') + def test_available_endpoint(self): + PromptTemplate.objects.create( + owner=self.user, + label='Mine', + description='', + text='t' + ) + PromptTemplate.objects.create( + owner=self.admin, + label='Shared', + description='', + text='t', + is_shared=True + ) + response = self.executeOK() + labels = [item['label'] for item in response.data] + self.assertIn('Mine', labels) + self.assertIn('Shared', labels) + + + @decl_endpoint('/api/prompts/{item}/', method='patch') + def test_permissions_on_shared(self): + prompt = PromptTemplate.objects.create( + owner=self.admin, + label='Shared', + description='', + text='t', + is_shared=True + ) + self.client.force_authenticate(user=self.user) + response = self.executeForbidden(data={'label': 'Nope'}, item=prompt.id) diff --git a/rsconcept/backend/apps/prompt/urls.py b/rsconcept/backend/apps/prompt/urls.py new file mode 100644 index 00000000..9f3cc0a3 --- /dev/null +++ b/rsconcept/backend/apps/prompt/urls.py @@ -0,0 +1,9 @@ +''' Routing: Prompts for AI helper. ''' +from rest_framework.routers import DefaultRouter + +from .views import PromptTemplateViewSet + +router = DefaultRouter() +router.register('prompts', PromptTemplateViewSet, 'prompt-template') + +urlpatterns = router.urls diff --git a/rsconcept/backend/apps/prompt/views/__init__.py b/rsconcept/backend/apps/prompt/views/__init__.py new file mode 100644 index 00000000..24d0ca00 --- /dev/null +++ b/rsconcept/backend/apps/prompt/views/__init__.py @@ -0,0 +1,2 @@ +''' REST API: Endpoint processors for AI Prompts. ''' +from .prompts import PromptTemplateViewSet diff --git a/rsconcept/backend/apps/prompt/views/prompts.py b/rsconcept/backend/apps/prompt/views/prompts.py new file mode 100644 index 00000000..2d207f32 --- /dev/null +++ b/rsconcept/backend/apps/prompt/views/prompts.py @@ -0,0 +1,52 @@ +''' Views: PromptTemplate endpoints for AI prompt management. ''' + +from django.db import models +from drf_spectacular.utils import extend_schema +from rest_framework import permissions, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from ..models import PromptTemplate +from ..serializers import PromptTemplateSerializer + + +class IsOwnerOrAdmin(permissions.BasePermission): + '''Permission: Only owner or admin can modify, anyone can view shared.''' + + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return obj.is_shared or obj.owner == request.user + return obj.owner == request.user or request.user.is_staff or request.user.is_superuser + + +@extend_schema(tags=['Prompts']) +class PromptTemplateViewSet(viewsets.ModelViewSet): + '''ViewSet: CRUD and listing for PromptTemplate, with sharing logic.''' + queryset = PromptTemplate.objects.all() + serializer_class = PromptTemplateSerializer + permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin] + + def get_queryset(self): + user = self.request.user + if self.action == 'available': + return PromptTemplate.objects.none() + return PromptTemplate.objects.filter(models.Q(owner=user) | models.Q(is_shared=True)).distinct() + + def get_object(self): + obj = PromptTemplate.objects.get(pk=self.kwargs['pk']) + self.check_object_permissions(self.request, obj) + return obj + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + @extend_schema(summary='List user-owned and shared prompt templates') + @action(detail=False, methods=['get'], url_path='available') + def available(self, request): + '''Return user-owned and shared prompt templates.''' + user = request.user + owned = PromptTemplate.objects.filter(owner=user) + shared = PromptTemplate.objects.filter(is_shared=True) + templates = (owned | shared).distinct() + serializer = self.get_serializer(templates, many=True) + return Response(serializer.data) diff --git a/rsconcept/backend/apps/rsform/admin.py b/rsconcept/backend/apps/rsform/admin.py index 204b7776..d2d6d48c 100644 --- a/rsconcept/backend/apps/rsform/admin.py +++ b/rsconcept/backend/apps/rsform/admin.py @@ -4,11 +4,9 @@ from django.contrib import admin from . import models +@admin.register(models.Constituenta) class ConstituentaAdmin(admin.ModelAdmin): ''' Admin model: Constituenta. ''' ordering = ['schema', 'order'] list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved'] search_fields = ['term_resolved', 'definition_resolved'] - - -admin.site.register(models.Constituenta, ConstituentaAdmin) diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py index 40231ad1..4301f96d 100644 --- a/rsconcept/backend/project/settings.py +++ b/rsconcept/backend/project/settings.py @@ -76,6 +76,7 @@ INSTALLED_APPS = [ 'apps.library', 'apps.rsform', 'apps.oss', + 'apps.prompt', 'drf_spectacular', 'drf_spectacular_sidecar', diff --git a/rsconcept/backend/project/urls.py b/rsconcept/backend/project/urls.py index 30314095..dc68ad78 100644 --- a/rsconcept/backend/project/urls.py +++ b/rsconcept/backend/project/urls.py @@ -11,6 +11,7 @@ urlpatterns = [ path('api/', include('apps.library.urls')), path('api/', include('apps.rsform.urls')), path('api/', include('apps.oss.urls')), + path('api/', include('apps.prompt.urls')), path('users/', include('apps.users.urls')), path('schema', SpectacularAPIView.as_view(), name='schema'), path('redoc', SpectacularRedocView.as_view()), diff --git a/rsconcept/backend/shared/messages.py b/rsconcept/backend/shared/messages.py index a317348e..c1a6963b 100644 --- a/rsconcept/backend/shared/messages.py +++ b/rsconcept/backend/shared/messages.py @@ -144,3 +144,19 @@ def passwordsNotMatch(): def emailAlreadyTaken(): return 'Пользователь с данным email уже существует' + + +def promptLabelTaken(label: str): + return f'Шаблон с меткой "{label}" уже существует у пользователя.' + + +def promptNotOwner(): + return 'Вы не являетесь владельцем этого шаблона.' + + +def promptSharedPermissionDenied(): + return 'Только администратор может сделать шаблон общедоступным.' + + +def promptNotFound(): + return 'Шаблон не найден.' diff --git a/rsconcept/frontend/src/features/ai/backend/types.ts b/rsconcept/frontend/src/features/ai/backend/types.ts index 1bfda4b9..c7ec91eb 100644 --- a/rsconcept/frontend/src/features/ai/backend/types.ts +++ b/rsconcept/frontend/src/features/ai/backend/types.ts @@ -2,7 +2,10 @@ export interface IPromptTemplate { id: number; owner: number | null; + is_shared: boolean; label: string; description: string; text: string; } + +// ========= SCHEMAS ======== diff --git a/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx b/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx index a46ea50d..3f02da5d 100644 --- a/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx +++ b/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx @@ -13,12 +13,14 @@ const mockPrompts: IPromptTemplate[] = [ id: 1, owner: null, label: 'Greeting', + is_shared: true, description: 'A simple greeting prompt.', text: 'Hello, ${name}! How can I assist you today?' }, { id: 2, owner: null, + is_shared: true, label: 'Summary', description: 'Summarize the following text.', text: 'Please summarize the following: ${text}' diff --git a/rsconcept/frontend/src/features/ai/labels.ts b/rsconcept/frontend/src/features/ai/labels.ts new file mode 100644 index 00000000..3ea42e9c --- /dev/null +++ b/rsconcept/frontend/src/features/ai/labels.ts @@ -0,0 +1,13 @@ +import { PromptVariableType } from './models/prompting'; + +const describePromptVariableRecord: Record = { + [PromptVariableType.BLOCK]: 'Текущий блок операционной схемы', + [PromptVariableType.OSS]: 'Текущая операционная схема', + [PromptVariableType.SCHEMA]: 'Текущая концептуальный схема', + [PromptVariableType.CONSTITUENTA]: 'Текущая конституента' +}; + +/** Retrieves description for {@link PromptVariableType}. */ +export function describePromptVariable(itemType: PromptVariableType): string { + return describePromptVariableRecord[itemType] ?? `UNKNOWN VARIABLE TYPE: ${itemType}`; +} diff --git a/rsconcept/frontend/src/features/ai/models/prompting-api.test.ts b/rsconcept/frontend/src/features/ai/models/prompting-api.test.ts new file mode 100644 index 00000000..eebd145b --- /dev/null +++ b/rsconcept/frontend/src/features/ai/models/prompting-api.test.ts @@ -0,0 +1,39 @@ +import { extractPromptVariables } from './prompting-api'; + +describe('extractPromptVariables', () => { + it('extracts a single variable', () => { + expect(extractPromptVariables('Hello {{name}}!')).toEqual(['name']); + }); + + it('extracts multiple variables', () => { + expect(extractPromptVariables('Hi {{firstName}}, your ID is {{user.id}}.')).toEqual(['firstName', 'user.id']); + }); + + it('extracts variables with hyphens and dots', () => { + expect(extractPromptVariables('Welcome {{user-name}} and {{user.name}}!')).toEqual(['user-name', 'user.name']); + }); + + it('returns empty array if no variables', () => { + expect(extractPromptVariables('No variables here!')).toEqual([]); + }); + + it('ignores invalid variable patterns', () => { + expect(extractPromptVariables('Hello {name}, {{name!}}, {{123}}, {{user_name}}')).toEqual([]); + }); + + it('extracts repeated variables', () => { + expect(extractPromptVariables('Repeat: {{foo}}, again: {{foo}}')).toEqual(['foo', 'foo']); + }); + + it('works with adjacent variables', () => { + expect(extractPromptVariables('{{a}}{{b}}{{c}}')).toEqual(['a', 'b', 'c']); + }); + + it('returns empty array for empty string', () => { + expect(extractPromptVariables('')).toEqual([]); + }); + + it('extracts variables at string boundaries', () => { + expect(extractPromptVariables('{{start}} middle {{end}}')).toEqual(['start', 'end']); + }); +}); diff --git a/rsconcept/frontend/src/features/ai/models/prompting-api.ts b/rsconcept/frontend/src/features/ai/models/prompting-api.ts index e69de29b..7a7143b6 100644 --- a/rsconcept/frontend/src/features/ai/models/prompting-api.ts +++ b/rsconcept/frontend/src/features/ai/models/prompting-api.ts @@ -0,0 +1,12 @@ +/** Extracts a list of variables (as string[]) from a target string. + * Note: Variables are wrapped in {{...}} and can include a-zA-Z, hyphen, and dot inside curly braces. + * */ +export function extractPromptVariables(target: string): string[] { + const regex = /\{\{([a-zA-Z.-]+)\}\}/g; + const result: string[] = []; + let match: RegExpExecArray | null; + while ((match = regex.exec(target)) !== null) { + result.push(match[1]); + } + return result; +} diff --git a/rsconcept/frontend/src/features/ai/models/prompting.ts b/rsconcept/frontend/src/features/ai/models/prompting.ts index 5c4d07e7..df7adef4 100644 --- a/rsconcept/frontend/src/features/ai/models/prompting.ts +++ b/rsconcept/frontend/src/features/ai/models/prompting.ts @@ -1,29 +1,29 @@ /** Represents prompt variable type. */ export const PromptVariableType = { BLOCK: 'block', - BLOCK_TITLE: 'block.title', - BLOCK_DESCRIPTION: 'block.description', - BLOCK_CONTENTS: 'block.contents', + // BLOCK_TITLE: 'block.title', + // BLOCK_DESCRIPTION: 'block.description', + // BLOCK_CONTENTS: 'block.contents', OSS: 'oss', - OSS_CONTENTS: 'oss.contents', - OSS_ALIAS: 'oss.alias', - OSS_TITLE: 'oss.title', - OSS_DESCRIPTION: 'oss.description', + // OSS_CONTENTS: 'oss.contents', + // OSS_ALIAS: 'oss.alias', + // OSS_TITLE: 'oss.title', + // OSS_DESCRIPTION: 'oss.description', SCHEMA: 'schema', - SCHEMA_ALIAS: 'schema.alias', - SCHEMA_TITLE: 'schema.title', - SCHEMA_DESCRIPTION: 'schema.description', - SCHEMA_THESAURUS: 'schema.thesaurus', - SCHEMA_GRAPH: 'schema.graph', - SCHEMA_TYPE_GRAPH: 'schema.type-graph', + // SCHEMA_ALIAS: 'schema.alias', + // SCHEMA_TITLE: 'schema.title', + // SCHEMA_DESCRIPTION: 'schema.description', + // SCHEMA_THESAURUS: 'schema.thesaurus', + // SCHEMA_GRAPH: 'schema.graph', + // SCHEMA_TYPE_GRAPH: 'schema.type-graph', - CONSTITUENTA: 'constituent', - CONSTITUENTA_ALIAS: 'constituent.alias', - CONSTITUENTA_CONVENTION: 'constituent.convention', - CONSTITUENTA_DEFINITION: 'constituent.definition', - CONSTITUENTA_DEFINITION_FORMAL: 'constituent.definition-formal', - CONSTITUENTA_EXPRESSION_TREE: 'constituent.expression-tree' + CONSTITUENTA: 'constituenta' + // CONSTITUENTA_ALIAS: 'constituent.alias', + // CONSTITUENTA_CONVENTION: 'constituent.convention', + // CONSTITUENTA_DEFINITION: 'constituent.definition', + // CONSTITUENTA_DEFINITION_FORMAL: 'constituent.definition-formal', + // CONSTITUENTA_EXPRESSION_TREE: 'constituent.expression-tree' } as const; export type PromptVariableType = (typeof PromptVariableType)[keyof typeof PromptVariableType]; diff --git a/rsconcept/frontend/src/features/ai/stores/ai-context.ts b/rsconcept/frontend/src/features/ai/stores/ai-context.ts new file mode 100644 index 00000000..56fca6d5 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/stores/ai-context.ts @@ -0,0 +1,64 @@ +import { create } from 'zustand'; + +import { type IBlock, type IOperationSchema } from '@/features/oss/models/oss'; +import { type IConstituenta, type IRSForm } from '@/features/rsform'; + +import { PromptVariableType } from '../models/prompting'; + +interface AIContextStore { + currentOSS: IOperationSchema | null; + setCurrentOSS: (value: IOperationSchema | null) => void; + + currentSchema: IRSForm | null; + setCurrentSchema: (value: IRSForm | null) => void; + + currentBlock: IBlock | null; + setCurrentBlock: (value: IBlock | null) => void; + + currentConstituenta: IConstituenta | null; + setCurrentConstituenta: (value: IConstituenta | null) => void; +} + +export const useAIStore = create()(set => ({ + currentOSS: null, + setCurrentOSS: value => set({ currentOSS: value }), + + currentSchema: null, + setCurrentSchema: value => set({ currentSchema: value }), + + currentBlock: null, + setCurrentBlock: value => set({ currentBlock: value }), + + currentConstituenta: null, + setCurrentConstituenta: value => set({ currentConstituenta: value }) +})); + +/** Returns a selector function for Zustand based on variable type */ +export function makeVariableSelector(variableType: PromptVariableType) { + switch (variableType) { + case PromptVariableType.OSS: + return (state: AIContextStore) => ({ currentOSS: state.currentOSS }); + case PromptVariableType.SCHEMA: + return (state: AIContextStore) => ({ currentSchema: state.currentSchema }); + case PromptVariableType.BLOCK: + return (state: AIContextStore) => ({ currentBlock: state.currentBlock }); + case PromptVariableType.CONSTITUENTA: + return (state: AIContextStore) => ({ currentConstituenta: state.currentConstituenta }); + default: + return () => ({}); + } +} + +/** Evaluates a prompt variable */ +export function evaluatePromptVariable(variableType: PromptVariableType, context: Partial): string { + switch (variableType) { + case PromptVariableType.OSS: + return context.currentOSS?.title ?? ''; + case PromptVariableType.SCHEMA: + return context.currentSchema?.title ?? ''; + case PromptVariableType.BLOCK: + return context.currentBlock?.title ?? ''; + case PromptVariableType.CONSTITUENTA: + return context.currentConstituenta?.alias ?? ''; + } +} diff --git a/rsconcept/frontend/src/features/ai/stores/use-available-variables.tsx b/rsconcept/frontend/src/features/ai/stores/use-available-variables.tsx new file mode 100644 index 00000000..e04b6a8d --- /dev/null +++ b/rsconcept/frontend/src/features/ai/stores/use-available-variables.tsx @@ -0,0 +1,17 @@ +import { PromptVariableType } from '../models/prompting'; + +import { useAIStore } from './ai-context'; + +export function useAvailableVariables(): PromptVariableType[] { + const hasCurrentOSS = useAIStore(state => !!state.currentOSS); + const hasCurrentSchema = useAIStore(state => !!state.currentSchema); + const hasCurrentBlock = useAIStore(state => !!state.currentBlock); + const hasCurrentConstituenta = useAIStore(state => !!state.currentConstituenta); + + return [ + ...(hasCurrentOSS ? [PromptVariableType.OSS] : []), + ...(hasCurrentSchema ? [PromptVariableType.SCHEMA] : []), + ...(hasCurrentBlock ? [PromptVariableType.BLOCK] : []), + ...(hasCurrentConstituenta ? [PromptVariableType.CONSTITUENTA] : []) + ]; +}