mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-16 05:40:37 +03:00
F: Implement prompt editing pt1
This commit is contained in:
parent
9a47ad5609
commit
d1dd37cc06
|
@ -17,7 +17,10 @@ class PromptTemplateSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def validate_label(self, value):
|
def validate_label(self, value):
|
||||||
user = self.context['request'].user
|
user = self.context['request'].user
|
||||||
if PromptTemplate.objects.filter(owner=user, label=value).exists():
|
queryset = PromptTemplate.objects.filter(owner=user, label=value)
|
||||||
|
if self.instance is not None:
|
||||||
|
queryset = queryset.exclude(pk=self.instance.pk)
|
||||||
|
if queryset.exists():
|
||||||
raise serializers.ValidationError(msg.promptLabelTaken(value))
|
raise serializers.ValidationError(msg.promptLabelTaken(value))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
|
@ -143,6 +143,11 @@ const DlgImportSchema = React.lazy(() =>
|
||||||
const DlgAIPromptDialog = React.lazy(() =>
|
const DlgAIPromptDialog = React.lazy(() =>
|
||||||
import('@/features/ai/dialogs/dlg-ai-prompt').then(module => ({ default: module.DlgAIPromptDialog }))
|
import('@/features/ai/dialogs/dlg-ai-prompt').then(module => ({ default: module.DlgAIPromptDialog }))
|
||||||
);
|
);
|
||||||
|
const DlgCreatePromptTemplate = React.lazy(() =>
|
||||||
|
import('@/features/ai/dialogs/dlg-create-prompt-template').then(module => ({
|
||||||
|
default: module.DlgCreatePromptTemplate
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
export const GlobalDialogs = () => {
|
export const GlobalDialogs = () => {
|
||||||
const active = useDialogsStore(state => state.active);
|
const active = useDialogsStore(state => state.active);
|
||||||
|
@ -213,5 +218,7 @@ export const GlobalDialogs = () => {
|
||||||
return <DlgImportSchema />;
|
return <DlgImportSchema />;
|
||||||
case DialogType.AI_PROMPT:
|
case DialogType.AI_PROMPT:
|
||||||
return <DlgAIPromptDialog />;
|
return <DlgAIPromptDialog />;
|
||||||
|
case DialogType.CREATE_PROMPT_TEMPLATE:
|
||||||
|
return <DlgCreatePromptTemplate />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createBrowserRouter } from 'react-router';
|
import { createBrowserRouter } from 'react-router';
|
||||||
|
|
||||||
|
import { prefetchAvailableTemplates } from '@/features/ai/backend/use-available-templates';
|
||||||
import { prefetchAuth } from '@/features/auth/backend/use-auth';
|
import { prefetchAuth } from '@/features/auth/backend/use-auth';
|
||||||
import { LoginPage } from '@/features/auth/pages/login-page';
|
import { LoginPage } from '@/features/auth/pages/login-page';
|
||||||
import { HomePage } from '@/features/home/home-page';
|
import { HomePage } from '@/features/home/home-page';
|
||||||
|
@ -85,6 +86,11 @@ export const Router = createBrowserRouter([
|
||||||
path: `${routes.database_schema}`,
|
path: `${routes.database_schema}`,
|
||||||
lazy: () => import('@/features/home/database-schema-page')
|
lazy: () => import('@/features/home/database-schema-page')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: routes.prompt_templates,
|
||||||
|
loader: prefetchAvailableTemplates,
|
||||||
|
lazy: () => import('@/features/ai/pages/prompt-templates-page')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
element: <NotFoundPage />
|
element: <NotFoundPage />
|
||||||
|
|
|
@ -39,6 +39,8 @@ export const urls = {
|
||||||
library_filter: (strategy: string) => `/library?filter=${strategy}`,
|
library_filter: (strategy: string) => `/library?filter=${strategy}`,
|
||||||
create_schema: `/${routes.create_schema}`,
|
create_schema: `/${routes.create_schema}`,
|
||||||
prompt_templates: `/${routes.prompt_templates}`,
|
prompt_templates: `/${routes.prompt_templates}`,
|
||||||
|
prompt_template: (active: number | null, tab: number) =>
|
||||||
|
`/prompt-templates?tab=${tab}${active ? `&active=${active}` : ''}`,
|
||||||
manuals: `/${routes.manuals}`,
|
manuals: `/${routes.manuals}`,
|
||||||
help_topic: (topic: string) => `/manuals?topic=${topic}`,
|
help_topic: (topic: string) => `/manuals?topic=${topic}`,
|
||||||
schema: (id: number | string, version?: number | string) =>
|
schema: (id: number | string, version?: number | string) =>
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { queryClient } from '@/backend/query-client';
|
||||||
|
|
||||||
import { promptsApi } from './api';
|
import { promptsApi } from './api';
|
||||||
|
|
||||||
export function useAvailableTemplates() {
|
export function useAvailableTemplates() {
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
...promptsApi.getAvailableTemplatesQueryOptions()
|
...promptsApi.getAvailableTemplatesQueryOptions()
|
||||||
});
|
});
|
||||||
return { data, isLoading, error };
|
return { items: data, isLoading, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAvailableTemplatesSuspense() {
|
export function useAvailableTemplatesSuspense() {
|
||||||
const { data } = useSuspenseQuery({
|
const { data } = useSuspenseQuery({
|
||||||
...promptsApi.getAvailableTemplatesQueryOptions()
|
...promptsApi.getAvailableTemplatesQueryOptions()
|
||||||
});
|
});
|
||||||
return { data };
|
return { items: data };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prefetchAvailableTemplates() {
|
||||||
|
return queryClient.prefetchQuery(promptsApi.getAvailableTemplatesQueryOptions());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { promptsApi } from './api';
|
import { promptsApi } from './api';
|
||||||
|
|
||||||
export function useCreatePromptTemplate() {
|
export function useCreatePromptTemplate() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [promptsApi.baseKey, 'create'],
|
mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'create'],
|
||||||
mutationFn: promptsApi.createPromptTemplate,
|
mutationFn: promptsApi.createPromptTemplate,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void client.invalidateQueries({ queryKey: [promptsApi.baseKey] });
|
void client.invalidateQueries({ queryKey: [promptsApi.baseKey] });
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { promptsApi } from './api';
|
import { promptsApi } from './api';
|
||||||
|
|
||||||
export function useDeletePromptTemplate() {
|
export function useDeletePromptTemplate() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [promptsApi.baseKey, 'delete'],
|
mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'delete'],
|
||||||
mutationFn: promptsApi.deletePromptTemplate,
|
mutationFn: promptsApi.deletePromptTemplate,
|
||||||
onSuccess: (_data, id) => {
|
onSuccess: (_, id) => {
|
||||||
void client.invalidateQueries({ queryKey: [promptsApi.baseKey] });
|
void client.invalidateQueries({ queryKey: [promptsApi.baseKey] });
|
||||||
void client.invalidateQueries({ queryKey: [promptsApi.baseKey, id] });
|
void client.invalidateQueries({ queryKey: [promptsApi.baseKey, id] });
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { useIsMutating } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
import { promptsApi } from './api';
|
||||||
|
|
||||||
|
export const useMutatingPrompts = () => {
|
||||||
|
const countMutations = useIsMutating({ mutationKey: [KEYS.global_mutation, promptsApi.baseKey] });
|
||||||
|
return countMutations !== 0;
|
||||||
|
};
|
|
@ -1,17 +1,23 @@
|
||||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { queryClient } from '@/backend/query-client';
|
||||||
|
|
||||||
import { promptsApi } from './api';
|
import { promptsApi } from './api';
|
||||||
|
|
||||||
export function usePromptTemplate(id: number) {
|
export function usePromptTemplate(id: number) {
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
...promptsApi.getPromptTemplateQueryOptions(id)
|
...promptsApi.getPromptTemplateQueryOptions(id)
|
||||||
});
|
});
|
||||||
return { data, isLoading, error };
|
return { promptTemplate: data, isLoading, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePromptTemplateSuspense(id: number) {
|
export function usePromptTemplateSuspense(id: number) {
|
||||||
const { data } = useSuspenseQuery({
|
const { data } = useSuspenseQuery({
|
||||||
...promptsApi.getPromptTemplateQueryOptions(id)
|
...promptsApi.getPromptTemplateQueryOptions(id)
|
||||||
});
|
});
|
||||||
return { data };
|
return { promptTemplate: data };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prefetchPromptTemplate({ itemID }: { itemID: number }) {
|
||||||
|
return queryClient.prefetchQuery(promptsApi.getPromptTemplateQueryOptions(itemID));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { promptsApi } from './api';
|
import { promptsApi } from './api';
|
||||||
import { type IUpdatePromptTemplateDTO } from './types';
|
import { type IPromptTemplateDTO, type IUpdatePromptTemplateDTO } from './types';
|
||||||
|
|
||||||
export function useUpdatePromptTemplate() {
|
export function useUpdatePromptTemplate() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [promptsApi.baseKey, 'update'],
|
mutationKey: [KEYS.global_mutation, promptsApi.baseKey, 'update'],
|
||||||
mutationFn: ({ id, data }: { id: number; data: IUpdatePromptTemplateDTO }) =>
|
mutationFn: ({ id, data }: { id: number; data: IUpdatePromptTemplateDTO }) =>
|
||||||
promptsApi.updatePromptTemplate(id, data),
|
promptsApi.updatePromptTemplate(id, data),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (data: IPromptTemplateDTO) => {
|
||||||
void client.invalidateQueries({ queryKey: [promptsApi.baseKey, variables.id] });
|
client.setQueryData(promptsApi.getPromptTemplateQueryOptions(data.id).queryKey, data);
|
||||||
|
client.setQueryData(promptsApi.getAvailableTemplatesQueryOptions().queryKey, prev =>
|
||||||
|
prev?.map(item => (item.id === data.id ? data : item))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { IconSharedTemplate } from './icon-shared-template';
|
||||||
|
|
||||||
|
interface BadgeSharedTemplateProps {
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays location icon with a full text tooltip.
|
||||||
|
*/
|
||||||
|
export function BadgeSharedTemplate({ value }: BadgeSharedTemplateProps) {
|
||||||
|
return (
|
||||||
|
<div className='pl-2' data-tooltip-id={globalIDs.tooltip} data-tooltip-content={value ? 'Общий' : 'Личный'}>
|
||||||
|
<IconSharedTemplate value={value} size='1.25rem' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { type DomIconProps, IconPrivate, IconPublic } from '@/components/icons';
|
||||||
|
|
||||||
|
/** Icon for shared template flag. */
|
||||||
|
export function IconSharedTemplate({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
|
if (value) {
|
||||||
|
return <IconPublic size={size} className={className ?? 'text-constructive'} />;
|
||||||
|
} else {
|
||||||
|
return <IconPrivate size={size} className={className ?? 'text-primary'} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
|
import { Checkbox, TextArea, TextInput } from '@/components/input';
|
||||||
|
import { ModalForm } from '@/components/modal';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
|
import { type ICreatePromptTemplateDTO, type IPromptTemplateDTO, schemaCreatePromptTemplate } from '../backend/types';
|
||||||
|
import { useAvailableTemplatesSuspense } from '../backend/use-available-templates';
|
||||||
|
import { useCreatePromptTemplate } from '../backend/use-create-prompt-template';
|
||||||
|
|
||||||
|
export interface DlgCreatePromptTemplateProps {
|
||||||
|
onCreate?: (data: RO<IPromptTemplateDTO>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DlgCreatePromptTemplate() {
|
||||||
|
const { onCreate } = useDialogsStore(state => state.props as DlgCreatePromptTemplateProps);
|
||||||
|
const { createPromptTemplate } = useCreatePromptTemplate();
|
||||||
|
const { items: templates } = useAvailableTemplatesSuspense();
|
||||||
|
const { user } = useAuthSuspense();
|
||||||
|
|
||||||
|
const { handleSubmit, control, register } = useForm<ICreatePromptTemplateDTO>({
|
||||||
|
resolver: zodResolver(schemaCreatePromptTemplate),
|
||||||
|
defaultValues: {
|
||||||
|
label: '',
|
||||||
|
description: '',
|
||||||
|
text: '',
|
||||||
|
is_shared: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const label = useWatch({ control, name: 'label' });
|
||||||
|
const isValid = label !== '' && !templates.find(template => template.label === label);
|
||||||
|
|
||||||
|
function onSubmit(data: ICreatePromptTemplateDTO) {
|
||||||
|
void createPromptTemplate(data).then(onCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalForm
|
||||||
|
header='Создание шаблона'
|
||||||
|
submitText='Создать'
|
||||||
|
canSubmit={isValid}
|
||||||
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
|
submitInvalidTooltip='Введите уникальное название шаблона'
|
||||||
|
className='cc-column w-140 max-h-120 py-2 px-6'
|
||||||
|
>
|
||||||
|
<TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' />
|
||||||
|
<TextArea id='dlg_prompt_description' {...register('description')} label='Описание' />
|
||||||
|
{user.is_staff ? (
|
||||||
|
<Controller
|
||||||
|
name='is_shared'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
id='dlg_prompt_is_shared'
|
||||||
|
label='Общий шаблон'
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
ref={field.ref}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
export { DlgAIPromptDialog } from './dlg-ai-prompt';
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { PromptTemplatesPage as Component } from './prompt-templates-page';
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
|
import { MiniButton } from '@/components/control';
|
||||||
|
import { IconNewItem } from '@/components/icons';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
import { PromptTabID } from './templates-tabs';
|
||||||
|
|
||||||
|
export function MenuTemplates() {
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const showCreatePromptTemplate = useDialogsStore(state => state.showCreatePromptTemplate);
|
||||||
|
|
||||||
|
function handleNewTemplate() {
|
||||||
|
showCreatePromptTemplate({
|
||||||
|
onCreate: data => router.push({ path: urls.prompt_template(data.id, PromptTabID.EDIT) })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex border-r-2 px-2'>
|
||||||
|
<MiniButton
|
||||||
|
noHover
|
||||||
|
noPadding
|
||||||
|
title='Новый шаблон'
|
||||||
|
icon={<IconNewItem size='1.25rem' />}
|
||||||
|
className='h-full text-muted-foreground hover:text-constructive cc-animate-color bg-transparent'
|
||||||
|
onClick={handleNewTemplate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { isAxiosError } from 'axios';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useBlockNavigation } from '@/app';
|
||||||
|
import { routes } from '@/app/urls';
|
||||||
|
import { RequireAuth } from '@/features/auth/components/require-auth';
|
||||||
|
|
||||||
|
import { TextURL } from '@/components/control';
|
||||||
|
import { type ErrorData } from '@/components/info-error';
|
||||||
|
import { useQueryStrings } from '@/hooks/use-query-strings';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
|
import { PromptTabID, TemplatesTabs } from './templates-tabs';
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
tab: z.preprocess(v => (v ? Number(v) : undefined), z.nativeEnum(PromptTabID).default(PromptTabID.LIST)),
|
||||||
|
active: z.preprocess(v => (v ? Number(v) : undefined), z.number().nullable().default(null))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function PromptTemplatesPage() {
|
||||||
|
const query = useQueryStrings();
|
||||||
|
|
||||||
|
const urlData = paramsSchema.parse({
|
||||||
|
tab: query.get('tab'),
|
||||||
|
active: query.get('active')
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
FallbackComponent={({ error }) => <ProcessError error={error as ErrorData} itemID={urlData.active} />}
|
||||||
|
>
|
||||||
|
<RequireAuth>
|
||||||
|
<TemplatesTabs activeID={urlData.active} tab={urlData.tab} />
|
||||||
|
</RequireAuth>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Internals =========
|
||||||
|
function ProcessError({ error, itemID }: { error: ErrorData; itemID?: number | null }): React.ReactElement | null {
|
||||||
|
if (isAxiosError(error) && error.response) {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||||
|
<p>{`Шаблон запроса с указанным идентификатором ${itemID} отсутствует`}</p>
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<TextURL text='Список шаблонов' href={`/${routes.prompt_templates}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error as Error;
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
|
||||||
|
import { useUpdatePromptTemplate } from '@/features/ai/backend/use-update-prompt-template';
|
||||||
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
|
import { Checkbox, TextArea, TextInput } from '@/components/input';
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type IPromptTemplate,
|
||||||
|
type IUpdatePromptTemplateDTO,
|
||||||
|
schemaUpdatePromptTemplate
|
||||||
|
} from '../../../backend/types';
|
||||||
|
|
||||||
|
interface FormPromptTemplateProps {
|
||||||
|
promptTemplate: IPromptTemplate;
|
||||||
|
isMutable: boolean;
|
||||||
|
className?: string;
|
||||||
|
toggleReset: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Form for editing a prompt template. */
|
||||||
|
export function FormPromptTemplate({ promptTemplate, className, isMutable, toggleReset }: FormPromptTemplateProps) {
|
||||||
|
const { user } = useAuthSuspense();
|
||||||
|
const isProcessing = useMutatingPrompts();
|
||||||
|
const setIsModified = useModificationStore(state => state.setIsModified);
|
||||||
|
const { updatePromptTemplate } = useUpdatePromptTemplate();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
register,
|
||||||
|
formState: { isDirty, errors }
|
||||||
|
} = useForm<IUpdatePromptTemplateDTO>({
|
||||||
|
resolver: zodResolver(schemaUpdatePromptTemplate),
|
||||||
|
defaultValues: {
|
||||||
|
owner: promptTemplate.owner,
|
||||||
|
label: promptTemplate.label,
|
||||||
|
description: promptTemplate.description,
|
||||||
|
text: promptTemplate.text,
|
||||||
|
is_shared: promptTemplate.is_shared
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevReset = useRef(toggleReset);
|
||||||
|
const prevTemplate = useRef(promptTemplate);
|
||||||
|
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
|
||||||
|
prevTemplate.current = promptTemplate;
|
||||||
|
prevReset.current = toggleReset;
|
||||||
|
reset({
|
||||||
|
owner: promptTemplate.owner,
|
||||||
|
label: promptTemplate.label,
|
||||||
|
description: promptTemplate.description,
|
||||||
|
text: promptTemplate.text,
|
||||||
|
is_shared: promptTemplate.is_shared
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevDirty = useRef(isDirty);
|
||||||
|
if (prevDirty.current !== isDirty) {
|
||||||
|
prevDirty.current = isDirty;
|
||||||
|
setIsModified(isDirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit(data: IUpdatePromptTemplateDTO) {
|
||||||
|
return updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
|
||||||
|
setIsModified(false);
|
||||||
|
reset({ ...data });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id={globalIDs.prompt_editor}
|
||||||
|
className={cn('flex flex-col gap-3 px-6', className)}
|
||||||
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id='prompt_label'
|
||||||
|
label='Название' //
|
||||||
|
{...register('label')}
|
||||||
|
error={errors.label}
|
||||||
|
disabled={isProcessing || !isMutable}
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
id='prompt_description'
|
||||||
|
label='Описание' //
|
||||||
|
{...register('description')}
|
||||||
|
error={errors.description}
|
||||||
|
disabled={isProcessing || !isMutable}
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
id='prompt_text'
|
||||||
|
label='Содержание' //
|
||||||
|
{...register('text')}
|
||||||
|
error={errors.text}
|
||||||
|
disabled={isProcessing || !isMutable}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name='is_shared'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
id='prompt_is_shared'
|
||||||
|
label='Общий шаблон'
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
ref={field.ref}
|
||||||
|
disabled={isProcessing || !isMutable || !user.is_staff}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { TabEditTemplate } from './tab-edit-template';
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { usePromptTemplateSuspense } from '../../../backend/use-prompt-template';
|
||||||
|
|
||||||
|
import { FormPromptTemplate } from './form-prompt-template';
|
||||||
|
import { ToolbarTemplate } from './toolbar-template';
|
||||||
|
|
||||||
|
interface TabEditTemplateProps {
|
||||||
|
activeID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabEditTemplate({ activeID }: TabEditTemplateProps) {
|
||||||
|
const { promptTemplate } = usePromptTemplateSuspense(activeID);
|
||||||
|
const isModified = useModificationStore(state => state.isModified);
|
||||||
|
const setIsModified = useModificationStore(state => state.setIsModified);
|
||||||
|
const { user } = useAuthSuspense();
|
||||||
|
const isMutable = user.is_staff || promptTemplate.owner === user.id;
|
||||||
|
const [toggleReset, setToggleReset] = useState(false);
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
setToggleReset(t => !t);
|
||||||
|
setIsModified(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerFormSubmit() {
|
||||||
|
const form = document.getElementById(globalIDs.prompt_editor) as HTMLFormElement | null;
|
||||||
|
if (form) {
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||||
|
if (isModified) {
|
||||||
|
triggerFormSubmit();
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='pt-8 rounded bg-background relative' tabIndex={-1} onKeyDown={handleInput}>
|
||||||
|
{isMutable ? (
|
||||||
|
<ToolbarTemplate
|
||||||
|
activeID={activeID}
|
||||||
|
className={clsx(
|
||||||
|
'cc-tab-tools cc-animate-position',
|
||||||
|
'right-1/2 translate-x-0 xs:right-4 xs:-translate-x-1/2 md:right-1/2 md:translate-x-0'
|
||||||
|
)}
|
||||||
|
onSave={triggerFormSubmit}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<FormPromptTemplate
|
||||||
|
className='mt-12 xs:mt-4 w-100 md:w-180 min-w-70'
|
||||||
|
isMutable={isMutable}
|
||||||
|
promptTemplate={promptTemplate}
|
||||||
|
toggleReset={toggleReset}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
import { useDeletePromptTemplate } from '@/features/ai/backend/use-delete-prompt-template';
|
||||||
|
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
|
||||||
|
|
||||||
|
import { MiniButton } from '@/components/control';
|
||||||
|
import { IconDestroy, IconReset, IconSave } from '@/components/icons';
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
import { promptText } from '@/utils/labels';
|
||||||
|
import { isMac, prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { PromptTabID } from '../templates-tabs';
|
||||||
|
|
||||||
|
interface ToolbarTemplateProps {
|
||||||
|
activeID: number;
|
||||||
|
onSave: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toolbar for prompt template editing. */
|
||||||
|
export function ToolbarTemplate({ activeID, onSave, onReset, className }: ToolbarTemplateProps) {
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const { deletePromptTemplate } = useDeletePromptTemplate();
|
||||||
|
const isProcessing = useMutatingPrompts();
|
||||||
|
const isModified = useModificationStore(state => state.isModified);
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
if (window.confirm(promptText.deleteTemplate)) {
|
||||||
|
void deletePromptTemplate(activeID).then(() =>
|
||||||
|
router.pushAsync({ path: urls.prompt_template(null, PromptTabID.LIST) })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('cc-icons items-start outline-hidden', className)}>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml={prepareTooltip('Сохранить изменения', isMac() ? 'Cmd + S' : 'Ctrl + S')}
|
||||||
|
aria-label='Сохранить изменения'
|
||||||
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
|
onClick={onSave}
|
||||||
|
disabled={isProcessing || !isModified}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Сбросить изменения'
|
||||||
|
aria-label='Сбросить изменения'
|
||||||
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
|
onClick={onReset}
|
||||||
|
disabled={isProcessing || !isModified}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Удалить шаблон'
|
||||||
|
aria-label='Удалить шаблон'
|
||||||
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isProcessing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
import { type IPromptTemplate } from '@/features/ai/backend/types';
|
||||||
|
import { useLabelUser } from '@/features/users';
|
||||||
|
|
||||||
|
import { TextURL } from '@/components/control';
|
||||||
|
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
|
||||||
|
import { NoData } from '@/components/view';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
|
import { useAvailableTemplatesSuspense } from '../../backend/use-available-templates';
|
||||||
|
import { BadgeSharedTemplate } from '../../components/badge-shared-template';
|
||||||
|
|
||||||
|
import { PromptTabID } from './templates-tabs';
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<RO<IPromptTemplate>>();
|
||||||
|
|
||||||
|
interface TabListTemplatesProps {
|
||||||
|
activeID: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabListTemplates({ activeID }: TabListTemplatesProps) {
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const { items } = useAvailableTemplatesSuspense();
|
||||||
|
const showCreatePromptTemplate = useDialogsStore(state => state.showCreatePromptTemplate);
|
||||||
|
const getUserLabel = useLabelUser();
|
||||||
|
|
||||||
|
function handleRowDoubleClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
router.push({ path: urls.prompt_template(row.id, PromptTabID.EDIT), newTab: event.ctrlKey || event.metaKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRowClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) {
|
||||||
|
if (row.id === activeID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push({ path: urls.prompt_template(row.id, PromptTabID.LIST), newTab: event.ctrlKey || event.metaKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateNew() {
|
||||||
|
showCreatePromptTemplate({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor('is_shared', {
|
||||||
|
id: 'is_shared',
|
||||||
|
header: '',
|
||||||
|
size: 50,
|
||||||
|
minSize: 50,
|
||||||
|
maxSize: 50,
|
||||||
|
enableSorting: true,
|
||||||
|
cell: props => <BadgeSharedTemplate value={props.getValue()} />,
|
||||||
|
sortingFn: 'text'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('label', {
|
||||||
|
id: 'label',
|
||||||
|
header: 'Название',
|
||||||
|
size: 200,
|
||||||
|
minSize: 200,
|
||||||
|
maxSize: 200,
|
||||||
|
enableSorting: true,
|
||||||
|
cell: props => <span className='min-w-20'>{props.getValue()}</span>,
|
||||||
|
sortingFn: 'text'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('description', {
|
||||||
|
id: 'description',
|
||||||
|
header: 'Описание',
|
||||||
|
size: 1200,
|
||||||
|
minSize: 200,
|
||||||
|
maxSize: 1200,
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: 'text'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('owner', {
|
||||||
|
id: 'owner',
|
||||||
|
header: 'Владелец',
|
||||||
|
size: 400,
|
||||||
|
minSize: 100,
|
||||||
|
maxSize: 400,
|
||||||
|
cell: props => getUserLabel(props.getValue()),
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: 'text'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const conditionalRowStyles: IConditionalStyle<RO<IPromptTemplate>>[] = [
|
||||||
|
{
|
||||||
|
when: (template: RO<IPromptTemplate>) => template.id === activeID,
|
||||||
|
className: 'bg-selected'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='pt-7 relative'>
|
||||||
|
<DataTable
|
||||||
|
noFooter
|
||||||
|
enableSorting
|
||||||
|
data={items as IPromptTemplate[]}
|
||||||
|
columns={columns}
|
||||||
|
className='w-full h-full border-x border-b'
|
||||||
|
onRowClicked={handleRowClicked}
|
||||||
|
onRowDoubleClicked={handleRowDoubleClicked}
|
||||||
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
|
noDataComponent={
|
||||||
|
<NoData>
|
||||||
|
<p>Список пуст</p>
|
||||||
|
<p>
|
||||||
|
<TextURL text='Создать шаблон запроса...' onClick={handleCreateNew} />
|
||||||
|
</p>
|
||||||
|
</NoData>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function TabViewVariables() {
|
||||||
|
return <div className='pt-8 border rounded'>View all variables</div>;
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
|
import { Loader } from '@/components/loader';
|
||||||
|
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||||
|
|
||||||
|
import { MenuTemplates } from './menu-templates';
|
||||||
|
import { TabEditTemplate } from './tab-edit-template';
|
||||||
|
import { TabListTemplates } from './tab-list-templates';
|
||||||
|
import { TabViewVariables } from './tab-view-variables';
|
||||||
|
|
||||||
|
export const PromptTabID = {
|
||||||
|
LIST: 0,
|
||||||
|
EDIT: 1,
|
||||||
|
VARIABLES: 2
|
||||||
|
} as const;
|
||||||
|
export type PromptTabID = (typeof PromptTabID)[keyof typeof PromptTabID];
|
||||||
|
|
||||||
|
interface TemplatesTabsProps {
|
||||||
|
activeID: number | null;
|
||||||
|
tab: PromptTabID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabLoader() {
|
||||||
|
return (
|
||||||
|
<div className='h-20 mt-8 w-full flex items-center'>
|
||||||
|
<Loader scale={4} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplatesTabs({ activeID, tab }: TemplatesTabsProps) {
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
|
||||||
|
function onSelectTab(index: number, last: number, event: Event) {
|
||||||
|
if (last === index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.type == 'keydown') {
|
||||||
|
const kbEvent = event as KeyboardEvent;
|
||||||
|
if (kbEvent.altKey) {
|
||||||
|
if (kbEvent.code === 'ArrowLeft') {
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
} else if (kbEvent.code === 'ArrowRight') {
|
||||||
|
router.forward();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
router.replace({ path: urls.prompt_template(activeID, index as PromptTabID) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs selectedIndex={tab} onSelect={onSelectTab} className='relative flex flex-col min-w-fit items-center'>
|
||||||
|
<TabList className='absolute z-sticky flex border-b-2 border-x-2 divide-x-2 bg-background'>
|
||||||
|
<MenuTemplates />
|
||||||
|
<TabLabel label='Список' />
|
||||||
|
<TabLabel label='Шаблон' />
|
||||||
|
<TabLabel label='Переменные' />
|
||||||
|
</TabList>
|
||||||
|
<div className='overflow-x-hidden'>
|
||||||
|
<TabPanel>
|
||||||
|
<Suspense fallback={<TabLoader />}>
|
||||||
|
<TabListTemplates activeID={activeID} />
|
||||||
|
</Suspense>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
{activeID ? (
|
||||||
|
<Suspense fallback={<TabLoader />}>
|
||||||
|
{' '}
|
||||||
|
<TabEditTemplate activeID={activeID} />{' '}
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<TabViewVariables />
|
||||||
|
</TabPanel>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ export function useLibraryColumns() {
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
sortingFn: 'text'
|
sortingFn: 'text'
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor(item => item.owner ?? 0, {
|
columnHelper.accessor('owner', {
|
||||||
id: 'owner',
|
id: 'owner',
|
||||||
header: 'Владелец',
|
header: 'Владелец',
|
||||||
size: 400,
|
size: 400,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
@ -47,12 +47,10 @@ export function FormOSS() {
|
||||||
const readOnly = useWatch({ control, name: 'read_only' });
|
const readOnly = useWatch({ control, name: 'read_only' });
|
||||||
|
|
||||||
const prevDirty = useRef(isDirty);
|
const prevDirty = useRef(isDirty);
|
||||||
useEffect(() => {
|
if (prevDirty.current !== isDirty) {
|
||||||
if (prevDirty.current !== isDirty) {
|
prevDirty.current = isDirty;
|
||||||
prevDirty.current = isDirty;
|
setIsModified(isDirty);
|
||||||
setIsModified(isDirty);
|
}
|
||||||
}
|
|
||||||
}, [isDirty, setIsModified]);
|
|
||||||
|
|
||||||
function onSubmit(data: IUpdateLibraryItemDTO) {
|
function onSubmit(data: IUpdateLibraryItemDTO) {
|
||||||
return updateOss(data).then(() => reset({ ...data }));
|
return updateOss(data).then(() => reset({ ...data }));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
@ -121,15 +121,13 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevDirty = useRef(isDirty);
|
const prevDirty = useRef(isDirty);
|
||||||
useEffect(() => {
|
if (prevDirty.current !== isDirty) {
|
||||||
if (prevDirty.current !== isDirty) {
|
prevDirty.current = isDirty;
|
||||||
prevDirty.current = isDirty;
|
setIsModified(isDirty);
|
||||||
setIsModified(isDirty);
|
}
|
||||||
}
|
|
||||||
}, [isDirty, setIsModified]);
|
|
||||||
|
|
||||||
function onSubmit(data: IUpdateConstituentaDTO) {
|
function onSubmit(data: IUpdateConstituentaDTO) {
|
||||||
return cstUpdate({ itemID: schema.id, data }).then(() => {
|
void cstUpdate({ itemID: schema.id, data }).then(() => {
|
||||||
setIsModified(false);
|
setIsModified(false);
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
@ -71,12 +71,10 @@ export function FormRSForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevDirty = useRef(isDirty);
|
const prevDirty = useRef(isDirty);
|
||||||
useEffect(() => {
|
if (prevDirty.current !== isDirty) {
|
||||||
if (prevDirty.current !== isDirty) {
|
prevDirty.current = isDirty;
|
||||||
prevDirty.current = isDirty;
|
setIsModified(isDirty);
|
||||||
setIsModified(isDirty);
|
}
|
||||||
}
|
|
||||||
}, [isDirty, setIsModified]);
|
|
||||||
|
|
||||||
function handleSelectVersion(version: CurrentVersion) {
|
function handleSelectVersion(version: CurrentVersion) {
|
||||||
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });
|
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { type DlgAIPromptDialogProps } from '@/features/ai/dialogs/dlg-ai-prompt';
|
import { type DlgAIPromptDialogProps } from '@/features/ai/dialogs/dlg-ai-prompt';
|
||||||
|
import { type DlgCreatePromptTemplateProps } from '@/features/ai/dialogs/dlg-create-prompt-template';
|
||||||
import { type DlgChangeLocationProps } from '@/features/library/dialogs/dlg-change-location';
|
import { type DlgChangeLocationProps } from '@/features/library/dialogs/dlg-change-location';
|
||||||
import { type DlgCloneLibraryItemProps } from '@/features/library/dialogs/dlg-clone-library-item';
|
import { type DlgCloneLibraryItemProps } from '@/features/library/dialogs/dlg-clone-library-item';
|
||||||
import { type DlgCreateVersionProps } from '@/features/library/dialogs/dlg-create-version';
|
import { type DlgCreateVersionProps } from '@/features/library/dialogs/dlg-create-version';
|
||||||
|
@ -69,7 +70,9 @@ export const DialogType = {
|
||||||
SHOW_TERM_GRAPH: 28,
|
SHOW_TERM_GRAPH: 28,
|
||||||
CREATE_SCHEMA: 29,
|
CREATE_SCHEMA: 29,
|
||||||
IMPORT_SCHEMA: 30,
|
IMPORT_SCHEMA: 30,
|
||||||
AI_PROMPT: 31
|
|
||||||
|
AI_PROMPT: 31,
|
||||||
|
CREATE_PROMPT_TEMPLATE: 32
|
||||||
} as const;
|
} as const;
|
||||||
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
|
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
|
||||||
|
|
||||||
|
@ -113,6 +116,7 @@ interface DialogsStore {
|
||||||
showCreateSchema: (props: DlgCreateSchemaProps) => void;
|
showCreateSchema: (props: DlgCreateSchemaProps) => void;
|
||||||
showImportSchema: (props: DlgImportSchemaProps) => void;
|
showImportSchema: (props: DlgImportSchemaProps) => void;
|
||||||
showAIPrompt: (props: DlgAIPromptDialogProps) => void;
|
showAIPrompt: (props: DlgAIPromptDialogProps) => void;
|
||||||
|
showCreatePromptTemplate: (props: DlgCreatePromptTemplateProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDialogsStore = create<DialogsStore>()(set => ({
|
export const useDialogsStore = create<DialogsStore>()(set => ({
|
||||||
|
@ -155,5 +159,6 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
|
||||||
showEditCst: props => set({ active: DialogType.EDIT_CONSTITUENTA, props: props }),
|
showEditCst: props => set({ active: DialogType.EDIT_CONSTITUENTA, props: props }),
|
||||||
showCreateSchema: props => set({ active: DialogType.CREATE_SCHEMA, props: props }),
|
showCreateSchema: props => set({ active: DialogType.CREATE_SCHEMA, props: props }),
|
||||||
showImportSchema: props => set({ active: DialogType.IMPORT_SCHEMA, props: props }),
|
showImportSchema: props => set({ active: DialogType.IMPORT_SCHEMA, props: props }),
|
||||||
showAIPrompt: (props: DlgAIPromptDialogProps) => set({ active: DialogType.AI_PROMPT, props: props })
|
showAIPrompt: (props: DlgAIPromptDialogProps) => set({ active: DialogType.AI_PROMPT, props: props }),
|
||||||
|
showCreatePromptTemplate: props => set({ active: DialogType.CREATE_PROMPT_TEMPLATE, props: props })
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
/* React Tooltip */
|
/* React Tooltip */
|
||||||
--rt-color-white: var(--color-input);
|
--rt-color-white: var(--color-input);
|
||||||
--rt-color-dark: var(--color-foreground);
|
--rt-color-dark: var(--color-foreground);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Theme */
|
/* Dark Theme */
|
||||||
|
|
|
@ -78,6 +78,7 @@ export const globalIDs = {
|
||||||
email_tooltip: 'email_tooltip',
|
email_tooltip: 'email_tooltip',
|
||||||
library_item_editor: 'library_item_editor',
|
library_item_editor: 'library_item_editor',
|
||||||
constituenta_editor: 'constituenta_editor',
|
constituenta_editor: 'constituenta_editor',
|
||||||
|
prompt_editor: 'prompt_editor',
|
||||||
graph_schemas: 'graph_schemas_tooltip',
|
graph_schemas: 'graph_schemas_tooltip',
|
||||||
user_dropdown: 'user_dropdown',
|
user_dropdown: 'user_dropdown',
|
||||||
ai_dropdown: 'ai_dropdown'
|
ai_dropdown: 'ai_dropdown'
|
||||||
|
|
|
@ -82,6 +82,7 @@ export const promptText = {
|
||||||
promptUnsaved: 'Присутствуют несохраненные изменения. Продолжить без их учета?',
|
promptUnsaved: 'Присутствуют несохраненные изменения. Продолжить без их учета?',
|
||||||
deleteLibraryItem: 'Вы уверены, что хотите удалить данную схему?',
|
deleteLibraryItem: 'Вы уверены, что хотите удалить данную схему?',
|
||||||
deleteBlock: 'Вы уверены, что хотите удалить данный блок?',
|
deleteBlock: 'Вы уверены, что хотите удалить данный блок?',
|
||||||
|
deleteTemplate: 'Вы уверены, что хотите удалить данный шаблон?',
|
||||||
deleteOSS:
|
deleteOSS:
|
||||||
'Внимание!!\nУдаление операционной схемы приведет к удалению всех операций и собственных концептуальных схем.\nДанное действие нельзя отменить.\nВы уверены, что хотите удалить данную ОСС?',
|
'Внимание!!\nУдаление операционной схемы приведет к удалению всех операций и собственных концептуальных схем.\nДанное действие нельзя отменить.\nВы уверены, что хотите удалить данную ОСС?',
|
||||||
generateWordforms: 'Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?',
|
generateWordforms: 'Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user