F: Implementing PromptEdit pt1
This commit is contained in:
parent
2beed1c1c6
commit
12c202adff
|
@ -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-primary'} />;
|
||||||
|
} else {
|
||||||
|
return <IconPrivate size={size} className={className ?? 'text-primary'} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import { useAvailableTemplatesSuspense } from '../backend/use-available-template
|
||||||
import { useCreatePromptTemplate } from '../backend/use-create-prompt-template';
|
import { useCreatePromptTemplate } from '../backend/use-create-prompt-template';
|
||||||
|
|
||||||
export interface DlgCreatePromptTemplateProps {
|
export interface DlgCreatePromptTemplateProps {
|
||||||
onCreate: (data: RO<IPromptTemplateDTO>) => void;
|
onCreate?: (data: RO<IPromptTemplateDTO>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DlgCreatePromptTemplate() {
|
export function DlgCreatePromptTemplate() {
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
|
|
||||||
|
|
||||||
interface TabEditTemplateProps {
|
|
||||||
activeID: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TabEditTemplate({ activeID }: TabEditTemplateProps) {
|
|
||||||
const { promptTemplate } = usePromptTemplateSuspense(activeID);
|
|
||||||
|
|
||||||
return <div className='pt-8 border rounded'>Prompt Editor {promptTemplate.label}</div>;
|
|
||||||
}
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
|
import { Checkbox, TextArea, TextInput } from '@/components/input';
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type IPromptTemplate,
|
||||||
|
type IUpdatePromptTemplateDTO,
|
||||||
|
schemaUpdatePromptTemplate
|
||||||
|
} from '../../../backend/types';
|
||||||
|
|
||||||
|
interface FormPromptTemplateProps {
|
||||||
|
promptTemplate: IPromptTemplate;
|
||||||
|
disabled: boolean;
|
||||||
|
onSubmit: (data: IUpdatePromptTemplateDTO) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Form for editing a prompt template. */
|
||||||
|
export function FormPromptTemplate({
|
||||||
|
promptTemplate,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
onSubmit,
|
||||||
|
onReset: _onReset
|
||||||
|
}: FormPromptTemplateProps) {
|
||||||
|
const { user } = useAuthSuspense();
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset({
|
||||||
|
owner: promptTemplate.owner,
|
||||||
|
label: promptTemplate.label,
|
||||||
|
description: promptTemplate.description,
|
||||||
|
text: promptTemplate.text,
|
||||||
|
is_shared: promptTemplate.is_shared
|
||||||
|
});
|
||||||
|
}, [promptTemplate, reset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={cn('flex flex-col gap-3 px-6', className)} onSubmit={event => void handleSubmit(onSubmit)(event)}>
|
||||||
|
<TextInput id='prompt_label' label='Название' {...register('label')} disabled={disabled} />
|
||||||
|
<TextArea id='prompt_description' label='Описание' {...register('description')} disabled={disabled} />
|
||||||
|
<TextArea id='prompt_text' label='Содержание' {...register('text')} disabled={disabled} />
|
||||||
|
{user.is_staff ? (
|
||||||
|
<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={disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { TabEditTemplate } from './tab-edit-template';
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { promptText } from '@/utils/labels';
|
||||||
|
|
||||||
|
import { type IUpdatePromptTemplateDTO } from '../../../backend/types';
|
||||||
|
import { useDeletePromptTemplate } from '../../../backend/use-delete-prompt-template';
|
||||||
|
import { usePromptTemplateSuspense } from '../../../backend/use-prompt-template';
|
||||||
|
import { useUpdatePromptTemplate } from '../../../backend/use-update-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 { updatePromptTemplate, isPending: isSaving } = useUpdatePromptTemplate();
|
||||||
|
const { deletePromptTemplate, isPending: isDeleting } = useDeletePromptTemplate();
|
||||||
|
const [isModified, setIsModified] = useState(false);
|
||||||
|
const [formKey, setFormKey] = useState(0);
|
||||||
|
|
||||||
|
function handleSave(data: IUpdatePromptTemplateDTO) {
|
||||||
|
void updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
|
||||||
|
setIsModified(false);
|
||||||
|
setFormKey(k => k + 1); // force form reset
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
setFormKey(k => k + 1);
|
||||||
|
setIsModified(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
if (window.confirm(promptText.deleteTemplate)) {
|
||||||
|
void deletePromptTemplate(promptTemplate.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerFormSubmit() {
|
||||||
|
const form = document.getElementById('prompt-template-edit-form') as HTMLFormElement | null;
|
||||||
|
if (form) {
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='pt-8 rounded bg-background relative'>
|
||||||
|
<ToolbarTemplate
|
||||||
|
className='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'
|
||||||
|
disabled={isSaving || isDeleting}
|
||||||
|
isModified={isModified}
|
||||||
|
onSave={triggerFormSubmit}
|
||||||
|
onReset={handleReset}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
<FormPromptTemplate
|
||||||
|
className='mt-12 xs:mt-4 w-100 md:w-180 min-w-70'
|
||||||
|
key={formKey}
|
||||||
|
promptTemplate={promptTemplate}
|
||||||
|
disabled={isSaving || isDeleting}
|
||||||
|
onSubmit={handleSave}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { MiniButton } from '@/components/control';
|
||||||
|
import { IconDestroy, IconReset, IconSave } from '@/components/icons';
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
|
||||||
|
interface ToolbarTemplateProps {
|
||||||
|
disabled: boolean;
|
||||||
|
isModified: boolean;
|
||||||
|
onSave: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toolbar for prompt template editing. */
|
||||||
|
export function ToolbarTemplate({ disabled, isModified, onSave, onReset, onDelete, className }: ToolbarTemplateProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('cc-icons items-start outline-hidden', className)}>
|
||||||
|
<MiniButton
|
||||||
|
title='Сохранить изменения'
|
||||||
|
aria-label='Сохранить изменения'
|
||||||
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
|
onClick={onSave}
|
||||||
|
disabled={disabled || !isModified}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Сбросить изменения'
|
||||||
|
aria-label='Сбросить изменения'
|
||||||
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
|
onClick={onReset}
|
||||||
|
disabled={disabled || !isModified}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Удалить шаблон'
|
||||||
|
aria-label='Удалить шаблон'
|
||||||
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
|
onClick={onDelete}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,13 +1,20 @@
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { type IPromptTemplate } from '@/features/ai/backend/types';
|
import { type IPromptTemplate } from '@/features/ai/backend/types';
|
||||||
|
import { useLabelUser } from '@/features/users';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
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 { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { useAvailableTemplatesSuspense } from '../../backend/use-available-templates';
|
import { useAvailableTemplatesSuspense } from '../../backend/use-available-templates';
|
||||||
|
import { BadgeSharedTemplate } from '../../components/badge-shared-template';
|
||||||
|
|
||||||
import { PromptTabID } from './templates-tabs';
|
import { PromptTabID } from './templates-tabs';
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<RO<IPromptTemplate>>();
|
||||||
|
|
||||||
interface TabListTemplatesProps {
|
interface TabListTemplatesProps {
|
||||||
activeID: number | null;
|
activeID: number | null;
|
||||||
}
|
}
|
||||||
|
@ -15,21 +22,90 @@ interface TabListTemplatesProps {
|
||||||
export function TabListTemplates({ activeID }: TabListTemplatesProps) {
|
export function TabListTemplates({ activeID }: TabListTemplatesProps) {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { items } = useAvailableTemplatesSuspense();
|
const { items } = useAvailableTemplatesSuspense();
|
||||||
console.log(activeID);
|
const showCreatePromptTemplate = useDialogsStore(state => state.showCreatePromptTemplate);
|
||||||
|
const getUserLabel = useLabelUser();
|
||||||
|
|
||||||
function handleRowDoubleClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) {
|
function handleRowDoubleClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) {
|
||||||
router.push({ path: urls.prompt_template(row.id, PromptTabID.EDIT), newTab: event.ctrlKey || event.metaKey });
|
router.push({ path: urls.prompt_template(row.id, PromptTabID.EDIT), newTab: event.ctrlKey || event.metaKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRowClicked(row: RO<IPromptTemplate>, event: React.MouseEvent<Element>) {
|
||||||
|
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 (
|
return (
|
||||||
|
<div className='pt-7 relative'>
|
||||||
<DataTable
|
<DataTable
|
||||||
|
noFooter
|
||||||
|
enableSorting
|
||||||
data={items as IPromptTemplate[]}
|
data={items as IPromptTemplate[]}
|
||||||
columns={[
|
columns={columns}
|
||||||
{ accessorKey: 'label', header: 'Name' },
|
className='w-full h-full border-x border-b'
|
||||||
{ accessorKey: 'description', header: 'Description' }
|
onRowClicked={handleRowClicked}
|
||||||
]}
|
|
||||||
className='pt-8'
|
|
||||||
onRowDoubleClicked={handleRowDoubleClicked}
|
onRowDoubleClicked={handleRowDoubleClicked}
|
||||||
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
|
noDataComponent={
|
||||||
|
<NoData>
|
||||||
|
<p>Список пуст</p>
|
||||||
|
<p>
|
||||||
|
<TextURL text='Создать шаблон запроса...' onClick={handleCreateNew} />
|
||||||
|
</p>
|
||||||
|
</NoData>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ export function TemplatesTabs({ activeID, tab }: TemplatesTabsProps) {
|
||||||
<TabList className='absolute z-sticky flex border-b-2 border-x-2 divide-x-2 bg-background'>
|
<TabList className='absolute z-sticky flex border-b-2 border-x-2 divide-x-2 bg-background'>
|
||||||
<MenuTemplates />
|
<MenuTemplates />
|
||||||
<TabLabel label='Список' />
|
<TabLabel label='Список' />
|
||||||
<TabLabel label='Редактор' />
|
<TabLabel label='Шаблон' />
|
||||||
<TabLabel label='Переменные' />
|
<TabLabel label='Переменные' />
|
||||||
</TabList>
|
</TabList>
|
||||||
<div className='overflow-x-hidden'>
|
<div className='overflow-x-hidden'>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -129,7 +129,7 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
|
||||||
}, [isDirty, setIsModified]);
|
}, [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 });
|
||||||
});
|
});
|
||||||
|
|
|
@ -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